Font Icons can be used in both KronoGraph and KeyLines.
This story uses Font Awesome icons on nodes and links in the KeyLines chart, and on entities and events in the KronoGraph timeline.
See also:
- Event fontIcon API
- Entity glyph API
- <a href='#' onclick="parent.toShowcasePlayground('integration', true);">Integration Playground</a>
Font Icons can be used in both KronoGraph and ReGraph.
This story uses Font Awesome icons on nodes and links in the ReGraph chart, and on entities and events in the KronoGraph timeline.
See also:
- Event fontIcon API
- Entity glyph API
- <a href='#' onclick="parent.toShowcasePlayground('integration', true);">Integration Playground</a>
import { createTimeline } from "kronograph";
import KeyLines from "keylines";
import { isEmpty } from "lodash";
import { chartData, timelineData, timelineOptions, chartOptions } from "./data";
async function loadFonts() {
document.fonts.load('900 16px "Font Awesome 5 Free"');
document.fonts.load('500 16px "Font Awesome 5 Brands"');
await document.fonts.ready;
}
async function main() {
async function applySelection(selection) {
chart.selection(selection);
const selectedNodeIds = selection.filter((itemId) => chart.getItem(itemId).type === "node");
timeline.focus(selectedNodeIds);
const neighborsOfSelection = await chart.graph().neighbours(selection);
if (isEmpty(selection)) {
// If nothing is selected, nothing is faded.
chart.foreground(() => true);
} else {
// Fade nodes that are neither themselves selected nor neighboring a selected node.
chart.foreground(
(node) => selection.includes(node.id) || neighborsOfSelection.nodes.includes(node.id)
);
}
}
async function timelineRangeHandler() {
// When timeline range changes, filter the items in the KeyLines chart.
const { entities: visibleEntityIds } = timeline.getInRangeItems({ focus: false });
const filterOptions = { type: "node", animate: false };
const { shown, hidden } = await chart.filter(
(item) => visibleEntityIds[item.id],
filterOptions
);
if (!isEmpty(shown.nodes) || !isEmpty(hidden.nodes)) {
await chart.layout("organic");
}
}
await loadFonts();
const timeline = createTimeline({ container: "timeline", options: timelineOptions });
const chart = await KeyLines.create({ container: "chart", options: chartOptions });
// When the focus on the timeline changes, update the KeyLines chart.
timeline.on("focus", ({ focus }) => applySelection(focus));
timeline.on("range", timelineRangeHandler);
chart.on("selection-change", () => applySelection(chart.selection()));
// prevent selection of links
chart.on("click", (ev) => {
if (ev.id) {
const item = chart.getItem(ev.id);
if (item.type === "link") {
ev.preventDefault();
}
}
});
timeline.set(timelineData);
timeline.fit();
await chart.load(chartData);
await chart.layout();
await chart.zoom("fit");
}
main(); import keyBy from "lodash/keyBy";
import mapValues from "lodash/mapValues";
const colorPalette = {
lightBlueGray: "#e5e8eb",
darkBlueGray: "#242c32",
darkBlueGrayAlpha: "rgba(36, 44, 50, 0.8)",
blueGray: "#606d7b",
orange: "#f58a3d",
purple: "#7069FA",
red: "#ef4e4e",
blue: "#4098D7",
yellow: "#f7d06e",
transparent: "rgba(0, 0, 0, 0)",
};
export const timelineOptions = {
entities: { glyphPosition: "right" },
focus: {
backgroundColor: colorPalette.blueGray,
},
scales: { showAtBottom: false },
};
export const chartOptions = {
backgroundAlpha: 0.3,
backColour: colorPalette.darkBlueGray,
selectionColour: colorPalette.blueGray,
navigation: { shown: false },
controlTheme: "dark",
handMode: true,
minZoom: 0.2,
overview: false,
iconFontFamily: "Font Awesome 5 Free",
imageAlignment: {
"\uf39f": { e: 1.4 },
"\uf3d0": { e: 1.4 },
"\uf42e": { e: 1.4 },
"\uf15a": { e: 1.4 },
"\uf232": { e: 1.4 },
},
};
const nodesInfo = [
{
id: "1",
name: "Alexander",
fontIcon: "fas fa-user-graduate",
fontChar: "\uf501",
color: colorPalette.orange,
},
{
id: "2",
name: "Bernadette",
fontIcon: "fas fa-user-ninja",
fontChar: "\uf504",
color: colorPalette.purple,
},
{
id: "3",
name: "Deirdre",
fontIcon: "fas fa-user-secret",
fontChar: "\uf21b",
color: colorPalette.red,
},
{
id: "4",
name: "Irene",
fontIcon: "fas fa-user-tie",
fontChar: "\uf508",
color: colorPalette.blue,
},
{
id: "5",
name: "John",
fontIcon: "fas fa-user-astronaut",
fontChar: "\uf4fb",
color: colorPalette.yellow,
},
];
const linksInfo = [
{ id: "a1", id1: "1", id2: "2", timestamp: 1625518983000, icon: "\uf39f" /* facebook message */ },
{ id: "a2", id1: "1", id2: "3", timestamp: 1625571591000, icon: "\uf3d0" /* monero */ },
{ id: "a3", id1: "3", id2: "4", timestamp: 1625681838000, icon: "\uf42e" /* ethereum */ },
{ id: "a4", id1: "3", id2: "5", timestamp: 1625723792000, icon: "\uf15a" /* bitcoin */ },
{ id: "a5", id1: "3", id2: "1", timestamp: 1625773792000, icon: "\uf232" /* whatsapp message */ },
];
const nodes = nodesInfo.map(({ name, fontIcon, color }, idx) => {
return {
type: "node",
id: `${idx + 1}`,
c: color,
t: name, // label text
tc: false, // don't center the label
fi: { t: fontIcon },
fc: colorPalette.lightBlueGray, // label text color
fbc: colorPalette.darkBlueGrayAlpha, // label text background color
};
});
const links = linksInfo.map(({ id, id1, id2, timestamp, icon }) => {
const { color: endColor } = nodesInfo.find((node) => node.id === id1);
const { color: startColor } = nodesInfo.find((node) => node.id === id2);
return {
id,
id1,
id2,
type: "link",
c: startColor, // color at id1 end
c2: endColor, // color at id2 end
a2: true, // show arrow at id2 end
w: 1.5, // width
d: { time: new Date(timestamp) },
g: [{ c: endColor, b: colorPalette.transparent, fi: { t: icon, ff: "Font Awesome 5 Brands" } }], // glyph with fonticon
};
});
export const chartData = {
type: "LinkChart",
items: [...nodes, ...links],
};
const entities = mapValues(keyBy(nodesInfo, "id"), ({ name, fontChar, color }) => {
return {
color,
label: name,
glyph: { fontIcon: { text: fontChar } },
};
});
const events = mapValues(keyBy(linksInfo, "id"), ({ id1, id2, timestamp, icon }) => {
return {
entityIds: [id1, id2],
time: new Date(timestamp),
fontIcon: { text: icon },
};
});
const entityTypes = {
default: {
glyph: { fontIcon: { fontFamily: "Font Awesome 5 Free", fontWeight: 900, scale: 1.4 } },
},
};
const eventTypes = { default: { fontIcon: { fontFamily: "Font Awesome 5 Brands", scale: 1.5 } } };
export const timelineData = { events, entities, entityTypes, eventTypes }; <!doctype html>
<html>
<head>
<link rel="stylesheet" href="@ci/theme/kg/css/examples.css" />
</head>
<body>
<div style="display: flex; flex-direction: column; height: 100%">
<div id="chart" style="flex: 1"></div>
<div id="timeline" style="flex: 1"></div>
</div>
<script type="module" src="./code.js"></script>
</body>
</html> import React, { useState, useRef, useEffect } from "react";
import { createRoot } from "react-dom/client";
import Timeline from "kronograph/react/Timeline";
import { Chart } from "regraph";
import { neighbors } from "regraph/analysis";
import { pickBy, mapValues } from "lodash";
import { chartData, timelineData, timelineOptions, chartOptions } from "./data";
function isLink(item) {
return item.id1 && item.id2;
}
function preventLinkSelection(event) {
const item = chartData[event.id];
if (item && isLink(item)) {
event.preventDefault();
}
}
export const Demo = () => {
const [focusedItems, setFocusedItems] = useState({});
const [chartItems, setChartItems] = useState(chartData);
const [fontsReady, setFontsReady] = useState(false);
const timeline = useRef(null);
async function loadFonts() {
document.fonts.load('900 16px "Font Awesome 5 Free"');
document.fonts.load('500 16px "Font Awesome 5 Brands"');
await document.fonts.ready;
setFontsReady(true);
}
useEffect(() => {
loadFonts();
}, []);
async function chartInteractionHandler({ id, selected, setStyle }) {
if (selected) {
const neighborsOfSelected = await neighbors(chartItems, id);
// Fade everything except for neighbors of the selected item, and the
// selected item itself.
setStyle(
mapValues(chartItems, (_item, itemId) => {
return {
fade: !neighborsOfSelected[itemId] && itemId !== id,
};
})
);
}
}
function timelineChangeHandler({ range, focus }) {
// When timeline range changes, filter the items in the ReGraph chart.
if (range) {
const { entities, events } = timeline.current.getInRangeItems({ focus: false });
setChartItems(pickBy(chartData, (_item, id) => events[id] || entities[id]));
}
// In the ReGraph chart, select all items that are focused in the timeline.
if (focus) {
setFocusedItems(Object.fromEntries(focus.map((id) => [id, true])));
}
}
function chartChangeHandler({ selection }) {
// In the timeline, focus all items that are selected in the ReGraph chart.
if (selection) {
setFocusedItems(selection);
}
}
if (!fontsReady) {
return null;
}
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<Chart
id="chart"
items={chartItems}
selection={focusedItems}
options={chartOptions}
onChange={chartChangeHandler}
onClick={preventLinkSelection}
onItemInteraction={chartInteractionHandler}
/>
<Timeline
id="timeline"
{...timelineData}
ref={timeline}
focus={Object.keys(focusedItems)}
options={timelineOptions}
onTimelineChange={timelineChangeHandler}
/>
</div>
);
};
const root = createRoot(document.getElementById("my-timeline"));
root.render(<Demo />); import mapValues from "lodash/mapValues";
const colorPalette = {
lightBlueGray: "#e5e8eb",
darkBlueGray: "#242c32",
darkBlueGrayAlpha: "rgba(36, 44, 50, 0.8)",
blueGray: "#606d7b",
orange: "#f58a3d",
purple: "#7069FA",
red: "#ef4e4e",
blue: "#4098D7",
yellow: "#f7d06e",
};
const nodesInfo = {
1: {
name: "Alexander",
fontIcon: "fas fa-user-graduate",
fontChar: "\uf501",
color: colorPalette.orange,
},
2: {
name: "Bernadette",
fontIcon: "fas fa-user-ninja",
fontChar: "\uf504",
color: colorPalette.purple,
},
3: {
name: "Deirdre",
fontIcon: "fas fa-user-secret",
fontChar: "\uf21b",
color: colorPalette.red,
},
4: { name: "Irene", fontIcon: "fas fa-user-tie", fontChar: "\uf508", color: colorPalette.blue },
5: {
name: "John",
fontIcon: "fas fa-user-astronaut",
fontChar: "\uf4fb",
color: colorPalette.yellow,
},
};
const linksInfo = {
a1: { id1: "1", id2: "2", timestamp: 1625518983000, fontIcon: "\uf39f" /* facebook message */ },
a2: { id1: "1", id2: "3", timestamp: 1625571591000, fontIcon: "\uf3d0" /* monero */ },
a3: { id1: "3", id2: "4", timestamp: 1625681838000, fontIcon: "\uf42e" /* ethereum */ },
a4: { id1: "3", id2: "5", timestamp: 1625723792000, fontIcon: "\uf15a" /* bitcoin */ },
a5: { id1: "3", id2: "1", timestamp: 1625773792000, fontIcon: "\uf232" /* whatsapp message */ },
};
const nodes = mapValues(nodesInfo, ({ name, fontIcon, color }) => {
return {
label: {
text: name,
center: false,
backgroundColor: colorPalette.darkBlueGrayAlpha,
color: colorPalette.lightBlueGray,
},
fontIcon: { text: fontIcon },
color,
};
});
const links = mapValues(linksInfo, ({ id1, id2, timestamp, fontIcon }) => {
const startColor = nodes[id2].color;
const endColor = nodes[id1].color;
return {
id1,
id2,
color: startColor,
end2: { color: endColor, arrow: true },
data: { time: new Date(timestamp) },
glyphs: [
{
fontIcon: { text: fontIcon, fontFamily: "Font Awesome 5 Brands" },
color: startColor,
},
],
width: 1.5,
};
});
const entityTypes = {
default: {
glyph: { fontIcon: { fontFamily: "Font Awesome 5 Free", fontWeight: 900, scale: 1.4 } },
},
};
const eventTypes = { default: { fontIcon: { fontFamily: "Font Awesome 5 Brands", scale: 1.5 } } };
const entities = mapValues(nodesInfo, ({ name, fontChar, color }) => {
return {
label: name,
color,
glyph: { fontIcon: { text: fontChar } },
};
});
const events = mapValues(linksInfo, ({ id1, id2, timestamp, fontIcon }) => {
return {
time: new Date(timestamp),
entityIds: [id1, id2],
fontIcon: { text: fontIcon },
};
});
export const chartData = { ...nodes, ...links };
export const timelineData = { events, entities, entityTypes, eventTypes };
export const timelineOptions = {
focus: { backgroundColor: colorPalette.blueGray },
scales: { showAtBottom: false },
entities: { glyphPosition: "right" },
};
export const chartOptions = {
backgroundColor: colorPalette.darkBlueGray,
selection: { color: colorPalette.blueGray },
navigation: { visible: false },
minZoom: 0.2,
overview: false,
controlTheme: "dark",
iconFontFamily: "Font Awesome 5 Free",
imageAlignment: {
"\uf39f": { size: 1.4 },
"\uf3d0": { size: 1.4 },
"\uf42e": { size: 1.4 },
"\uf15a": { size: 1.4 },
"\uf232": { size: 1.4 },
},
}; <!doctype html>
<html>
<head>
<link rel="stylesheet" href="@ci/theme/kg/css/examples.css" />
</head>
<body>
<div id="my-timeline" style="height: 100vh"></div>
<script type="module" src="./code.jsx"></script>
</body>
</html>