import { createTimeline } from "kronograph";
import KeyLines from "keylines";
import {
kronoGraphData,
chartData,
createTable,
createTableData,
} from "./data";
/*
Load your own KronoGraph formatted data in the 'Data' tab of this playground
to see the entities and events represented on a KeyLines chart, and the events
listed in the table.
*/
let timeline;
let chart;
// The last hovered chart and timeline item.
let lastHoveredChartItem = null;
let lastHoveredTimelineItem = null;
// Ids of the currently highlighted chart items.
let chartHighlight = new Set();
// Default color and width values.
const highlightColor = "#7b8793";
const selectionColor = "rgba(93, 173, 226, 0.5)";
const aggregateLinkColor = "#808080";
const defaultEventWidth = 1;
const hoverEventWidth = 4;
// Options for the KeyLines chart.
const chartOptions = {
aggregateLinks: true,
backColour: "#252c32",
handMode: true,
hover: 0,
navigation: false,
overview: false,
selectionColour: selectionColor,
selectedLink: { c: selectionColor },
};
// Options for the KronoGraph timeline.
const timelineOptions = {
scales: {
showAtTop: false,
},
focus: {
backgroundColor: selectionColor,
},
showLensHighlight: false,
highlightColor,
hover: 0,
};
// Multiple events between two entities on the timeline are represented on the chart as an aggregated link between two nodes to help declutter the chart.
// The following two utility functions assist in mapping between an aggregate link and its corresponding timeline events for use in various interactions.
function getAggregateChildren(id) {
const aggregateInfo = chart.getAggregateInfo(id);
if (aggregateInfo !== null) {
return aggregateInfo.aggregateChildren;
}
return null;
}
function getParentAggregate(id) {
const aggregateInfo = chart.getAggregateInfo(id);
if (aggregateInfo !== null) {
return chart.getAggregateInfo(id).parentAggregate;
}
return null;
}
function onEventsTableHover(id) {
if (id !== null) {
// Reveal any events over the timeline heatmap and increase their width.
setTimelineEventWidths([id]);
timeline.reveal(id);
// Highlight the aggregate link on the chart.
if (chart.getItem(id) !== null) {
setChartHighlightFromTimelineEvent({
id,
targetType: "event",
eventIds: [id],
});
}
} else {
setChartHighlight([]);
setTimelineHighlight(null);
}
}
// Update the table with the current events in range.
function updateTable() {
const rowEntries = createTableData(timeline);
createTable(rowEntries, onEventsTableHover);
}
// Foreground nodes on the chart if they are in the timeline range.
function updateChartForeground() {
const { entities } = timeline.getInRangeItems();
chart.foreground(({ id }) => entities[id] === true);
}
// Update the events table and the chart foregrounding when the timeline range changes.
function onTimelineRange() {
updateTable();
updateChartForeground();
}
// Update the chart selection if an entity is focused.
function onTimelineFocus({ focus }) {
if (focus.length === 0) {
clearSelection();
} else {
onSelectedNodeOrEntity(focus[0]);
}
}
// Select an event by clicking on it, and select the corresponding aggregate link on the chart.
async function onTimelineClick({ id, targetType }) {
if (targetType === "event") {
if (timeline.eventSelection()[0] === id) {
// If clicking on the selected event, unselect it.
clearSelection();
} else {
timeline.eventSelection(id);
timeline.focus(id);
if (chart.getItem(id) !== null) {
const parentLink = getParentAggregate(id);
chart.selection(parentLink);
foregroundSelectedLink(id);
}
}
} else if (
targetType !== "focus" &&
targetType !== "pin" &&
timeline.focus().length === 0 &&
timeline.eventSelection().length === 0
) {
// Don't change the default focus and pin interactions.
clearSelection();
}
}
// When hovering over an aggregate link on the chart, or an event on the timeline, highlight the corresponding table row.
function highlightTableRow(previousIds, newIds) {
if (previousIds !== null) {
previousIds.forEach((id) => {
const tableRow = document.getElementById(id);
if (tableRow !== null) {
tableRow.classList.remove("hovered");
}
});
}
if (newIds !== null) {
newIds.forEach((id) => {
const tableRow = document.getElementById(id);
if (tableRow !== null) {
tableRow.classList.add("hovered");
}
});
}
}
function onTimelineHover({ id, targetType, eventIds }) {
if (lastHoveredTimelineItem === id) {
return;
}
// Highlight the timeline events. The entities will be highlighted by default.
const allEventIds = Object.keys(kronoGraphData.events);
if (targetType === "event" || targetType === "eventSummary") {
// Increase the width of hovered events.
const newEvents = { ...kronoGraphData.events };
allEventIds.forEach((eventId) => {
newEvents[eventId].lineWidth =
id === eventId ? hoverEventWidth : defaultEventWidth;
});
updateTimeline({ ...kronoGraphData, events: newEvents });
// Update highlighting for corresponding table row for the event.
highlightTableRow([lastHoveredTimelineItem], [id]);
} else if (
lastHoveredTimelineItem &&
allEventIds.includes(lastHoveredTimelineItem)
) {
// Set the timeline events to the default width.
const newEvents = { ...kronoGraphData.events };
allEventIds.forEach((eventId) => {
newEvents[eventId].lineWidth = defaultEventWidth;
});
updateTimeline({ ...kronoGraphData, events: newEvents });
// Update highlighting for corresponding table row for the event.
highlightTableRow([lastHoveredTimelineItem], [id]);
}
lastHoveredTimelineItem = id;
// Highlight the corresponding node or link on the chart.
setChartHighlightFromTimelineEvent({ id, targetType, eventIds });
}
// Update styling of aggregate links when they are created.
function onChartAggregateLinks({ change, links, id }) {
if (change !== "deleted") {
// Create a glyph to show the number of child links.
const glyphs = [
{
c: "white",
b: aggregateLinkColor,
t: links.length,
fc: aggregateLinkColor,
},
];
chart.setProperties({
id,
w: Math.min(links.length, 6),
c: aggregateLinkColor,
g: glyphs,
});
}
}
function onChartHover({ id }) {
if (lastHoveredChartItem === id) {
return;
}
// Update highlighting for corresponding table row for the link.
highlightTableRow(
getAggregateChildren(lastHoveredChartItem),
getAggregateChildren(id),
);
lastHoveredChartItem = id;
// Update styling on the timeline and chart.
const item = id === null ? null : chart.getItem(id);
if (item === null) {
setChartHighlight([]);
setTimelineHighlight(null);
} else {
setChartHighlight([id]);
setTimelineHighlight(id);
}
}
function onChartSelectionChange() {
const selection = chart.selection();
if (selection.length === 0) {
clearSelection();
} else {
const selectedItem = selection[0];
if (chart.getItem(selectedItem).type === "node") {
onSelectedNodeOrEntity(selectedItem);
} else if (chart.getItem(selectedItem).type === "link") {
onSelectedLink(selectedItem);
}
}
}
function setUpInteractions() {
// Handling interactions on the timeline.
timeline.on("range", onTimelineRange);
timeline.on("focus", onTimelineFocus);
timeline.on("click", onTimelineClick);
timeline.on("hover", onTimelineHover);
// Handling interactions on the chart.
chart.on("link-aggregation", onChartAggregateLinks);
chart.on("hover", onChartHover);
chart.on("selection-change", onChartSelectionChange);
}
function setChartHighlightFromTimelineEvent({ id, targetType, eventIds }) {
if (id === null) {
setChartHighlight([]);
return;
}
// Highlight a link on the chart.
if (targetType === "event" || targetType === "eventSummary") {
const chartLinkIds = [];
eventIds.forEach((eventId) => {
if (chart.getItem(eventId) !== null) {
chartLinkIds.push(eventId);
}
});
if (chartLinkIds.length > 0) {
const parentLinks = chartLinkIds.map((linkId) =>
getParentAggregate(linkId),
);
setChartHighlight(parentLinks);
}
}
// Highlight nodes on the chart.
else if (targetType === "entity") {
if (chart.getItem(id) !== null) {
setChartHighlight([id]);
}
}
}
// When hovering over an aggregate link on the chart or a row on the table,
// highlight the event/s on the timeline by increasing their width.
function setTimelineEventWidths(linkIds) {
const allEventIds = Object.keys(kronoGraphData.events);
// Create a copy of the current timeline events.
const newEvents = { ...kronoGraphData.events };
if (linkIds.length > 0) {
// Update the lineWidth property for the event that should be highlighted.
allEventIds.forEach((eventId) => {
newEvents[eventId].lineWidth = linkIds.includes(eventId)
? hoverEventWidth
: defaultEventWidth;
});
} else {
// Revert all lineWidths to be the default.
allEventIds.forEach((eventId) => {
newEvents[eventId].lineWidth = defaultEventWidth;
});
}
updateTimeline({ ...kronoGraphData, events: newEvents });
}
// Foreground and fit the selected link and its connecting nodes.
async function foregroundSelectedLink(linkChildren) {
const { nodes } = await chart.graph().neighbours(linkChildren, { hops: 1 });
await chart.zoom("fit", { animate: true, time: 500, ids: nodes });
await chart.foreground(({ id }) => nodes.includes(id));
}
function onSelectedLink(selectedLink) {
// Update the chart for the selected link.
const linkChildren = getAggregateChildren(selectedLink);
foregroundSelectedLink(linkChildren);
// Update the timeline with the selected events.
timeline.eventSelection(linkChildren);
timeline.focus(linkChildren);
timeline.fit();
// Update the events table.
updateTable();
}
function clearSelection() {
// Clear the timeline focus and selection.
timeline.reveal([]);
timeline.focus([]);
timeline.eventSelection([]);
// Clear the chart selection and foregrounding.
chart.selection([]);
chart.foreground(() => true);
chart.zoom("fit");
timeline.fit();
}
async function onSelectedNodeOrEntity(selectedId) {
if (chart.getItem(selectedId) !== null) {
// Foreground the selected node and its neighbours.
chart.selection(selectedId);
const { nodes } = await chart.graph().neighbours(selectedId, { hops: 1 });
const itemsToFocus = [...nodes, selectedId];
await chart.foreground(({ id }) => itemsToFocus.includes(id));
await chart.zoom("fit", { animate: true, time: 500, ids: itemsToFocus });
} else {
chart.selection([]);
}
// Focus the corresponding entity on the timeline.
timeline.focus(selectedId);
timeline.eventSelection([]);
timeline.fit();
updateTable();
}
// Highlight the specified items on the chart, and remove highlighting of other items.
function setChartHighlight(ids) {
const newHighlight = new Set(ids);
// Set the properties of hovered over items.
const toAdd = ids
.filter((id) => !chartHighlight.has(id))
.map((id) => {
const item = chart.getItem(id);
if (item.type === "node") {
return {
id,
ha0: { c: highlightColor, r: 30, w: 6 },
e: 1.15 * item.e,
};
}
const childLinks = getAggregateChildren(id).length;
return {
id,
w: Math.min(childLinks * 5, 10),
g: [{ c: highlightColor, b: "white", t: childLinks, fc: "white" }],
};
});
// Revert the properties of hovered off items.
const toRemove = Array.from(chartHighlight)
.filter((id) => !newHighlight.has(id))
.map((id) => {
const item = chart.getItem(id);
if (item.type === "node") {
return { id, ha0: undefined, e: item.e / 1.2 };
}
const childLinks = getAggregateChildren(id).length;
return {
id,
w: Math.min(childLinks, 6),
g: [
{
c: "white",
b: aggregateLinkColor,
t: childLinks,
fc: aggregateLinkColor,
},
],
};
});
chart.setProperties([...toAdd, ...toRemove]);
chartHighlight = newHighlight;
}
function setTimelineHighlight(id) {
if (id === null) {
timeline.highlight([]);
timeline.reveal([]);
setTimelineEventWidths([]);
return;
}
if (chart.getItem(id).type === "node") {
timeline.highlight([id]);
// Remove any highlighting from the events.
timeline.reveal([]);
setTimelineEventWidths([]);
} else {
const eventIds = chart.getAggregateInfo(id).aggregateChildren;
setTimelineEventWidths(eventIds);
timeline.reveal(eventIds);
}
}
function updateTimeline(data) {
const { events, entities, entityTypes, eventTypes, timeSeriesCharts } = data;
timeline.set({
events,
entities,
entityTypes: entityTypes ?? {},
eventTypes: eventTypes ?? {},
timeSeriesCharts: timeSeriesCharts ?? {},
});
}
async function sizeChartNodes() {
// Size nodes based on betweenness.
const betweenness = await chart.graph().betweenness({});
const sizes = Object.keys(betweenness).map((id) => {
return { id, e: (1 + betweenness[id]) ** 2 };
});
await chart.animateProperties(sizes, { time: 100 });
}
async function main() {
// Create and load data to the KronoGraph timeline.
timeline = createTimeline({
container: "my-timeline",
options: timelineOptions,
});
updateTimeline(kronoGraphData);
if (kronoGraphData.markers !== undefined) {
// Load any markers.
timeline.markers(kronoGraphData.markers);
}
if (kronoGraphData.annotations !== undefined) {
// Load any annotations.
timeline.annotations(kronoGraphData.annotations);
}
timeline.fit();
// Create and load data to the KeyLines chart.
chart = await KeyLines.create({ container: "chart", options: chartOptions });
// Populate the events table with the events and markers in range.
updateTable();
// Load the chart data and size based on centrality measure.
await chart.load(chartData);
await sizeChartNodes();
// Layout the chart.
await chart.layout("organic");
setUpInteractions();
}
// The timeline markers use Font Awesome icons (declared in the HTML <head>), so wait for the font before drawing them.
document.fonts.load('900 16px "Font Awesome 5 Free"');
document.fonts.ready.then(main);/*
To see your data displayed in the playground, update the below `kronoGraphData` object with the following properties:
Timeline data (see the API documentation for the correct format for these):
- entities
- events
- eventTypes
- entityTypes
- time series charts
- markers
- annotations
In addition, there are two further optional properties to customize which
events and entities are shown on the chart. Remove these if all entities or events
should be shown on the chart.
- entityTypeForChart
All entities of this type will be shown as nodes on the chart.
- eventTypeForChart
All events of this type will be shown as links on the chart.
*/
export const kronoGraphData = {
entities: {
indiv0: {
type: "Individuals",
label: "Hector Dickinson",
},
indiv1: {
type: "Individuals",
label: "Franco Abernathy",
},
indiv2: {
type: "Individuals",
label: "Christine Zboncak",
},
indiv3: {
type: "Individuals",
label: "Josh Feil",
},
indiv4: {
type: "Individuals",
label: "Addison Emmerich",
},
indiv5: {
type: "Individuals",
label: "Vivienne Frami",
},
indiv6: {
type: "Individuals",
label: "Preston Kihn",
},
indiv7: {
type: "Individuals",
label: "Nikko Kuphal",
},
indiv8: {
type: "Individuals",
label: "Margaret Hoeger",
},
indiv9: {
type: "Individuals",
label: "Alex Heaney",
},
indiv10: {
type: "Individuals",
label: "Giovanny Barton",
},
indiv11: {
type: "Individuals",
label: "Gunnar Fadel",
},
indiv12: {
type: "Individuals",
label: "Clarabelle Upton",
},
indiv13: {
type: "Individuals",
label: "Obie Ritchie",
},
indiv14: {
type: "Individuals",
label: "Lexi Hilll",
},
indiv15: {
type: "Individuals",
label: "Tierra Wilderman",
},
indiv16: {
type: "Individuals",
label: "Eriberto Dare",
},
indiv17: {
type: "Individuals",
label: "Deon Abshire",
},
indiv18: {
type: "Individuals",
label: "Milton Powlowski",
},
indiv19: {
type: "Individuals",
label: "Zetta Medhurst",
},
indiv20: {
type: "Individuals",
label: "Eloise Pollich",
},
indiv21: {
type: "Individuals",
label: "Celia Olson",
},
indiv22: {
type: "Individuals",
label: "Chanelle Macejkovic",
},
indiv23: {
type: "Individuals",
label: "Casandra Wyman",
},
indiv24: {
type: "Individuals",
label: "Kurtis Koepp",
},
indiv25: {
type: "Individuals",
label: "Jaqueline Nicolas",
},
indiv26: {
type: "Individuals",
label: "Jailyn Grant",
},
indiv27: {
type: "Individuals",
label: "Darron Ritchie",
},
indiv28: {
type: "Individuals",
label: "Russel Lindgren",
},
indiv29: {
type: "Individuals",
label: "Janick Quitzon",
},
indiv30: {
type: "Individuals",
label: "Amparo Crooks",
},
indiv31: {
type: "Individuals",
label: "Zackary Leuschke",
},
inst30: {
type: "Institutions",
label: "Gleason LLC",
},
inst31: {
type: "Institutions",
label: "Schultz, Yost and Kovacek",
},
inst32: {
type: "Institutions",
label: "Gerlach, Conroy and Kessler",
},
inst33: {
type: "Institutions",
label: "Ebert - Hoppe",
},
inst34: {
type: "Institutions",
label: "Doyle - Feeney",
},
inst35: {
type: "Institutions",
label: "Crooks LLC",
},
inst36: {
type: "Institutions",
label: "Schiller Inc",
},
inst37: {
type: "Institutions",
label: "West, Kassulke and Mraz",
},
megacorp: {
type: "Stocks",
label: "MegaCorp MEGC",
},
},
events: {
trade3: {
data: {
value: 97,
},
type: "sell",
entityIds: ["inst30", "megacorp"],
time: 1544863280051,
},
trade2: {
data: {
value: 66,
},
type: "sell",
entityIds: ["inst33", "megacorp"],
time: 1544932562760,
},
trade362: {
data: {
value: 52,
},
type: "sell",
entityIds: ["indiv9", "megacorp"],
time: 1545961198105,
},
trade366: {
data: {
value: 53,
},
type: "sell",
entityIds: ["indiv11", "megacorp"],
time: 1546062480608,
},
trade357: {
data: {
value: 87,
},
type: "buy",
entityIds: ["indiv6", "megacorp"],
time: 1546118620438,
},
trade350: {
data: {
value: 75,
},
type: "sell",
entityIds: ["indiv31", "megacorp"],
time: 1546235895647,
},
trade0: {
data: {
value: 76,
},
type: "buy",
entityIds: ["inst36", "megacorp"],
time: 1546318633467,
},
trade355: {
data: {
value: 61,
},
type: "sell",
entityIds: ["indiv13", "megacorp"],
time: 1546411946097,
},
trade354: {
data: {
value: 80,
},
type: "buy",
entityIds: ["indiv13", "megacorp"],
time: 1546416171344,
},
trade351: {
data: {
value: 56,
},
type: "sell",
entityIds: ["indiv28", "megacorp"],
time: 1546454343534,
},
comms91: {
entityIds: ["indiv10", "indiv5"],
time: 1546637022571,
type: "comms",
},
trade4: {
data: {
value: 56,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1546819595253,
},
trade353: {
data: {
value: 133,
},
type: "sell",
entityIds: ["indiv17", "megacorp"],
time: 1546949377153,
},
trade1: {
data: {
value: 119,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1546953492596,
},
trade356: {
data: {
value: 51,
},
type: "sell",
entityIds: ["indiv9", "megacorp"],
time: 1547138296150,
},
trade9: {
data: {
value: 73,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1547616817135,
},
trade358: {
data: {
value: 101,
},
type: "buy",
entityIds: ["indiv6", "megacorp"],
time: 1547671625200,
},
trade359: {
data: {
value: 130,
},
type: "buy",
entityIds: ["indiv8", "megacorp"],
time: 1547901839308,
},
comms54: {
entityIds: ["indiv16", "indiv14"],
time: 1547914525138,
type: "comms",
},
trade6: {
data: {
value: 149,
},
type: "buy",
entityIds: ["inst31", "megacorp"],
time: 1548006809426,
},
comms28: {
entityIds: ["indiv12", "indiv6"],
time: 1548049675272,
type: "comms",
},
comms92: {
entityIds: ["indiv1", "indiv2"],
time: 1548147977362,
type: "comms",
},
comms94: {
entityIds: ["indiv3", "indiv4"],
time: 1548230526817,
type: "comms",
},
trade379: {
data: {
value: 95,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1548240527111,
},
trade7: {
data: {
value: 96,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1548287469584,
},
trade352: {
data: {
value: 133,
},
type: "sell",
entityIds: ["indiv3", "megacorp"],
time: 1548477393064,
},
trade373: {
data: {
value: 98,
},
type: "sell",
entityIds: ["indiv1", "megacorp"],
time: 1548497234705,
},
trade364: {
data: {
value: 100,
},
type: "buy",
entityIds: ["indiv9", "megacorp"],
time: 1548524365754,
},
trade15: {
data: {
value: 77,
},
type: "buy",
entityIds: ["inst30", "megacorp"],
time: 1548558551151,
},
comms41: {
entityIds: ["indiv11", "indiv12"],
time: 1548610786657,
type: "comms",
},
trade365: {
data: {
value: 133,
},
type: "sell",
entityIds: ["indiv24", "megacorp"],
time: 1548706163359,
},
trade10: {
data: {
value: 94,
},
type: "sell",
entityIds: ["inst33", "megacorp"],
time: 1548756505257,
},
trade368: {
data: {
value: 99,
},
type: "sell",
entityIds: ["indiv3", "megacorp"],
time: 1548790331486,
},
trade8: {
data: {
value: 121,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1548857821042,
},
comms36: {
entityIds: ["indiv9", "indiv10"],
time: 1548861810958,
type: "comms",
},
trade369: {
data: {
value: 96,
},
type: "buy",
entityIds: ["indiv28", "megacorp"],
time: 1548948036836,
},
trade360: {
data: {
value: 106,
},
type: "buy",
entityIds: ["indiv17", "megacorp"],
time: 1548999059744,
},
trade361: {
data: {
value: 101,
},
type: "buy",
entityIds: ["indiv13", "megacorp"],
time: 1549005439784,
},
trade371: {
data: {
value: 95,
},
type: "buy",
entityIds: ["indiv25", "megacorp"],
time: 1549012395858,
},
comms61: {
entityIds: ["indiv21", "indiv18"],
time: 1549014056271,
type: "comms",
},
trade12: {
data: {
value: 81,
},
type: "buy",
entityIds: ["inst36", "megacorp"],
time: 1549041007770,
},
trade370: {
data: {
value: 90,
},
type: "buy",
entityIds: ["indiv25", "megacorp"],
time: 1549131796749,
},
trade11: {
data: {
value: 98,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1549215075302,
},
trade374: {
data: {
value: 99,
},
type: "buy",
entityIds: ["indiv19", "megacorp"],
time: 1549229001558,
},
trade5: {
data: {
value: 131,
},
type: "sell",
entityIds: ["inst30", "megacorp"],
time: 1549348177806,
},
trade372: {
data: {
value: 139,
},
type: "sell",
entityIds: ["indiv6", "megacorp"],
time: 1549413983235,
},
trade388: {
data: {
value: 83,
},
type: "sell",
entityIds: ["indiv26", "megacorp"],
time: 1549634315501,
},
comms76: {
entityIds: ["indiv27", "indiv29"],
time: 1549787487498,
type: "comms",
},
trade386: {
data: {
value: 58,
},
type: "sell",
entityIds: ["indiv27", "megacorp"],
time: 1549817193806,
},
trade375: {
data: {
value: 95,
},
type: "sell",
entityIds: ["indiv1", "megacorp"],
time: 1549835055727,
},
trade367: {
data: {
value: 125,
},
type: "sell",
entityIds: ["indiv25", "megacorp"],
time: 1549925638037,
},
trade378: {
data: {
value: 82,
},
type: "sell",
entityIds: ["indiv1", "megacorp"],
time: 1549929025705,
},
trade376: {
data: {
value: 83,
},
type: "buy",
entityIds: ["indiv11", "megacorp"],
time: 1549954115199,
},
comms64: {
entityIds: ["indiv23", "indiv19"],
time: 1549958971354,
type: "comms",
},
trade377: {
data: {
value: 121,
},
type: "sell",
entityIds: ["indiv1", "megacorp"],
time: 1550116482939,
},
trade14: {
data: {
value: 96,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1550198143577,
},
trade363: {
data: {
value: 128,
},
type: "buy",
entityIds: ["indiv29", "megacorp"],
time: 1550205623089,
},
trade13: {
data: {
value: 117,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1550285702957,
},
trade23: {
data: {
value: 87,
},
type: "sell",
entityIds: ["inst35", "megacorp"],
time: 1550369730674,
},
comms6: {
entityIds: ["indiv5", "indiv3"],
time: 1550380741503,
type: "comms",
},
trade20: {
data: {
value: 66,
},
type: "sell",
entityIds: ["inst31", "megacorp"],
time: 1550381531060,
},
trade393: {
data: {
value: 78,
},
type: "sell",
entityIds: ["indiv28", "megacorp"],
time: 1550533992626,
},
trade382: {
data: {
value: 58,
},
type: "sell",
entityIds: ["indiv13", "megacorp"],
time: 1550537856984,
},
trade380: {
data: {
value: 123,
},
type: "sell",
entityIds: ["indiv31", "megacorp"],
time: 1550549443723,
},
comms62: {
entityIds: ["indiv17", "indiv18"],
time: 1550570602856,
type: "comms",
},
comms32: {
entityIds: ["indiv11", "indiv7"],
time: 1550642171658,
type: "comms",
},
trade24: {
data: {
value: 72,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1550686265676,
},
trade381: {
data: {
value: 128,
},
type: "sell",
entityIds: ["indiv8", "megacorp"],
time: 1550707812147,
},
comms60: {
entityIds: ["indiv15", "indiv17"],
time: 1550760224316,
type: "comms",
},
comms66: {
entityIds: ["indiv31", "indiv28"],
time: 1550774656969,
type: "comms",
},
trade16: {
data: {
value: 87,
},
type: "buy",
entityIds: ["inst35", "megacorp"],
time: 1550817061586,
},
comms98: {
entityIds: ["indiv3", "indiv4"],
time: 1550910233770,
type: "comms",
},
trade384: {
data: {
value: 94,
},
type: "sell",
entityIds: ["indiv22", "megacorp"],
time: 1550920358021,
},
trade385: {
data: {
value: 65,
},
type: "sell",
entityIds: ["indiv12", "megacorp"],
time: 1551185545858,
},
trade17: {
data: {
value: 127,
},
type: "buy",
entityIds: ["inst37", "megacorp"],
time: 1551260943342,
},
trade383: {
data: {
value: 121,
},
type: "sell",
entityIds: ["indiv0", "megacorp"],
time: 1551392504914,
},
trade26: {
data: {
value: 53,
},
type: "buy",
entityIds: ["inst32", "megacorp"],
time: 1551422021586,
},
trade399: {
data: {
value: 54,
},
type: "sell",
entityIds: ["indiv4", "megacorp"],
time: 1551496824364,
},
comms15: {
entityIds: ["indiv4", "indiv3"],
time: 1551556509009,
type: "comms",
},
comms10: {
entityIds: ["indiv4", "indiv3"],
time: 1551601437264,
type: "comms",
},
trade387: {
data: {
value: 118,
},
type: "sell",
entityIds: ["indiv2", "megacorp"],
time: 1551611750874,
},
comms73: {
entityIds: ["indiv25", "indiv26"],
time: 1551616091026,
type: "comms",
},
trade19: {
data: {
value: 88,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1551642966529,
},
trade392: {
data: {
value: 73,
},
type: "sell",
entityIds: ["indiv28", "megacorp"],
time: 1551646156864,
},
comms88: {
entityIds: ["indiv8", "indiv17"],
time: 1551928887516,
type: "comms",
},
comms99: {
entityIds: ["indiv2", "indiv1"],
time: 1552103082493,
type: "comms",
},
trade394: {
data: {
value: 91,
},
type: "sell",
entityIds: ["indiv23", "megacorp"],
time: 1552125924511,
},
trade389: {
data: {
value: 117,
},
type: "sell",
entityIds: ["indiv4", "megacorp"],
time: 1552191597649,
},
comms488: {
type: "comms",
entityIds: ["indiv31", "indiv30"],
time: 1552348800000,
},
trade484: {
data: {
value: 73,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552366080000,
},
trade485: {
data: {
value: 57,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552383360000,
},
trade486: {
data: {
value: 104,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552400640000,
},
trade487: {
data: {
value: 127,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552417920000,
},
trade94: {
data: {
value: 70,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1552435352076,
},
trade105: {
data: {
value: 149,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1552435354397,
},
trade63: {
data: {
value: 147,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1552435405092,
},
trade104: {
data: {
value: 66,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1552435648746,
},
trade456: {
data: {
value: 52,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552435714150,
},
trade92: {
data: {
value: 132,
},
type: "sell",
entityIds: ["inst35", "megacorp"],
time: 1552435930605,
},
trade136: {
data: {
value: 119,
},
type: "sell",
entityIds: ["inst36", "megacorp"],
time: 1552436058968,
},
trade83: {
data: {
value: 124,
},
type: "sell",
entityIds: ["inst31", "megacorp"],
time: 1552436102040,
},
trade458: {
data: {
value: 109,
},
type: "sell",
entityIds: ["indiv9", "megacorp"],
time: 1552436776655,
},
trade128: {
data: {
value: 146,
},
type: "buy",
entityIds: ["inst34", "megacorp"],
time: 1552436908736,
},
trade130: {
data: {
value: 58,
},
type: "sell",
entityIds: ["inst36", "megacorp"],
time: 1552437817417,
},
trade99: {
data: {
value: 61,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1552438365823,
},
trade454: {
data: {
value: 53,
},
type: "sell",
entityIds: ["indiv10", "megacorp"],
time: 1552438725318,
},
trade140: {
data: {
value: 68,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1552438828919,
},
trade453: {
data: {
value: 76,
},
type: "sell",
entityIds: ["indiv12", "megacorp"],
time: 1552439085436,
},
trade67: {
data: {
value: 106,
},
type: "sell",
entityIds: ["inst30", "megacorp"],
time: 1552440950631,
},
trade72: {
data: {
value: 103,
},
type: "sell",
// ...truncated 3911 lines<!doctype html>
<html>
<head>
<link rel="stylesheet" href="@ci/theme/kg/css/examples.css">
<link
rel="stylesheet"
href="@fortawesome/[email protected]/css/fontawesome.css"
>
<link
rel="stylesheet"
href="@fortawesome/[email protected]/css/solid.css"
>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="showcase-container playground">
<div class="row">
<div class="panel-left-item row-item">
<div id="table-container"></div>
</div>
<div class="panel-right-item row-item">
<div id="chart"></div>
</div>
</div>
<div class="row">
<div id="my-timeline" class="panel-bottom-item"></div>
</div>
</div>
<script type="module" src="./code.js"></script>
</body>
</html>.showcase-container {
display: flex;
height: 100vh;
width: 100%;
flex-direction: column;
}
.row {
flex-direction: row;
width: 100%;
max-height: 50%;
min-height: 50%;
}
.panel-left-item {
border-right: 1px solid #e4e7ea;
}
.panel-right-item {
position: relative;
}
.panel-bottom-item {
border-top: 1px solid #e4e7ea;
}
.row-item {
flex: 1;
height: 100%;
}
#chart {
height: 100%;
width: 100%;
position: absolute;
}
#my-timeline {
flex: 1;
height: 100%;
}
#table-container {
overflow: scroll;
height: 100%;
background-color: #252c32;
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Hide scrollbar for Chrome, Safari and Opera */
#table-container::-webkit-scrollbar {
display: none;
}
table {
width: 100%;
border-collapse: collapse;
color: #e4e7ea;
font-size: 11px;
}
th,
td {
padding: 6px;
text-align: left;
}
th {
background-color: #252c32;
position: sticky;
top: 0;
z-index: 1;
}
tr:hover,
tr.hovered {
background-color: #2a333c;
}
.default-badge {
display: flex;
background-color: red;
color: black;
padding: 2px 4px;
border-radius: 3px;
flex: 1;
text-align: center;
justify-content: center;
align-items: center;
}import React, { useEffect, useReducer, useRef } from "react";
import { createRoot } from "react-dom/client";
import Timeline from "kronograph/react/Timeline";
import { Chart } from "regraph";
import {
kronoGraphData,
chartData,
createTableData,
tableColumns,
getBadgeBackground,
} from "./data";
import { betweenness, neighbors } from "regraph/analysis";
/*
Load your own KronoGraph formatted data in the 'Data' tab of this playground
to see the entities and events represented on a ReGraph chart, and the events
listed in the table.
*/
const {
entities,
events: originalEvents,
markers,
entityTypes,
eventTypes,
annotations,
} = kronoGraphData;
// Default color and width values.
const selectionColor = "rgba(93, 173, 226, 0.5)";
const highlightColor = "#7b8793";
const aggregateLinkColor = "#808080";
const hoverEventWidth = 4;
const defaultLinkWidth = 3;
// Options for the ReGraph chart.
const chartOptions = {
navigation: false,
backgroundColor: "#252c32",
overview: false,
handMode: true,
selection: {
color: selectionColor,
},
hoverDelay: 0,
};
// Options for the KronoGraph timeline.
const timelineOptions = {
scales: {
showAtTop: false,
},
focus: {
backgroundColor: selectionColor,
},
highlightColor,
hover: 0,
showLensHighlight: false,
};
// Utility functions used in the Demo component.
function eventIsSelected(selection, eventIds) {
const set = new Set(selection);
for (const event of eventIds) {
if (set.has(event)) {
return true;
}
}
return false;
}
function linkIsSelected(selection) {
return Object.keys(selection).some((s) => s.startsWith("_aggregate"));
}
function isNode(item) {
return !("id1" in item);
}
async function getNodesToForegroundAndFit(id) {
const neighborsOfNode = await neighbors(chartData, id);
return {
...Object.fromEntries(
Object.entries(neighborsOfNode)
.filter(([, node]) => isNode(node))
.map(([nodeId]) => [nodeId, { fade: false }]),
),
[id]: { fade: false },
};
}
const initialState = {
chartState: {
hoveredEntity: undefined,
hoveredEvent: undefined,
scores: undefined,
positions: undefined,
toFit: undefined,
selection: {},
},
timelineState: {
events: originalEvents,
highlight: "",
focus: [],
reveal: [],
selection: [],
range: [],
},
inRangeItems: undefined,
tableData: [],
};
// Reducer function that handles chart and timeline state.
function reducer(state, action) {
switch (action.type) {
case "hover_entity_on_timeline": {
return {
...state,
timelineState: {
...state.timelineState,
events: originalEvents,
},
chartState: {
...state.chartState,
hoveredEntity: action.id,
hoveredEvent: undefined,
},
};
}
case "hover_event_on_timeline": {
return {
...state,
chartState: {
...state.chartState,
hoveredEntity: undefined,
hoveredEvent: action.id,
},
timelineState: {
...state.timelineState,
events: {
...originalEvents,
[action.id]: {
...originalEvents[action.id],
lineWidth: hoverEventWidth,
},
},
},
};
}
case "reset_timeline_hover": {
return {
...state,
timelineState: {
...state.timelineState,
events: originalEvents,
},
chartState: {
...state.chartState,
hoveredEntity: undefined,
hoveredEvent: undefined,
},
};
}
case "click_event_on_timeline": {
return {
...state,
timelineState: {
...state.timelineState,
focus: action.eventIds,
selection: action.eventIds,
highlight: [],
},
chartState: {
...state.chartState,
toFit: action.toFit,
selection: action.selection,
},
};
}
case "reset_timeline_click": {
return {
...state,
timelineState: {
...state.timelineState,
focus: [],
selection: [],
},
chartState: {
...state.chartState,
hoveredEvent: undefined,
selection: {},
toFit: "all",
},
};
}
case "timeline_range_change": {
return {
...state,
timelineState: {
...state.timelineState,
range: action.range,
},
inRangeItems: action.inRangeItems,
tableData: createTableData(
action.inRangeItems,
action.markers,
action.range,
),
};
}
case "timeline_focus": {
return {
...state,
timelineState: {
...state.timelineState,
focus: action.focus,
},
chartState: {
...state.chartState,
selection: action.selection,
toFit: action.toFit,
},
inRangeItems: action.inRangeItems,
};
}
case "chart_link_select": {
return {
...state,
timelineState: {
...state.timelineState,
focus: Object.keys(action.links),
selection: Object.keys(action.links),
},
chartState: {
...state.chartState,
toFit: action.toFit,
},
};
}
case "chart_node_select": {
return {
...state,
timelineState: {
...state.timelineState,
focus: [action.id],
selection: [],
},
chartState: {
...state.chartState,
toFit: action.toFit,
},
};
}
case "chart_link_hover": {
return {
...state,
chartState: {
...state.chartState,
},
timelineState: {
...state.timelineState,
highlight: [],
reveal: Object.keys(action.links),
events: {
...state.timelineState.events,
...Object.fromEntries(
Object.keys(action.links).map((id) => [
id,
{
...state.timelineState.events[id],
lineWidth: hoverEventWidth,
},
]),
),
},
},
};
}
case "chart_node_hover": {
return {
...state,
chartState: {
...state.chartState,
},
timelineState: {
...state.timelineState,
highlight: action.id,
reveal: [],
events: originalEvents,
},
};
}
case "chart_on_pointer_move_reset_highlight": {
return {
...state,
timelineState: {
...state.timelineState,
highlight: [],
},
};
}
case "chart_on_pointer_move_reset_events": {
return {
...state,
timelineState: {
...state.timelineState,
reveal: [],
events: originalEvents,
},
};
}
case "chart_on_click_reset_focus": {
return {
...state,
chartState: {
...state.chartState,
toFit: action.toFit,
},
timelineState: {
...state.timelineState,
focus: [],
},
};
}
case "chart_on_click_reset_timeline_selection": {
return {
...state,
chartState: {
...state.chartState,
toFit: action.toFit,
},
timelineState: {
...state.timelineState,
selection: [],
},
};
}
case "chart_on_click_reset_selection": {
return {
...state,
chartState: {
...state.chartState,
selection: {},
toFit: action.toFit,
},
};
}
case "table_row_hover": {
return {
...state,
timelineState: {
...state.timelineState,
events: {
...originalEvents,
[action.id]: {
...originalEvents[action.id],
lineWidth: hoverEventWidth,
},
},
reveal: [action.id],
},
chartState: {
...state.chartState,
hoveredEvent: action.id,
},
};
}
case "reset_table_row_hover": {
return {
...state,
timelineState: {
...state.timelineState,
events: originalEvents,
reveal: [],
},
chartState: {
...state.chartState,
hoveredEvent: undefined,
},
};
}
case "chart_set_betweenness_scores": {
return {
...state,
chartState: {
...state.chartState,
scores: Object.fromEntries(
Object.entries(action.scores).map(([id, score]) => [
id,
(1 + score) ** 2,
]),
),
},
};
}
case "chart_set_positions": {
return {
...state,
chartState: {
...state.chartState,
positions: action.positions,
},
};
}
case "chart_set_selection": {
return {
...state,
chartState: {
...state.chartState,
selection: action.selection,
},
};
}
case "chart_fit_complete": {
return {
...state,
chartState: {
...state.chartState,
toFit: undefined,
},
};
}
default: {
throw Error(`Unknown action: ${action.type}`);
}
}
}
// Table of the events and markers in range on the timeline.
function Table({ tableData, onRowHover, reveal, chartState }) {
return (
<div id="table-container">
<table id="currentTable" onMouseLeave={() => onRowHover()}>
<thead>
<tr>
{tableColumns.map((column, i) => (
<th key={`table-header-${i}`}>{column.header.toUpperCase()}</th>
))}
</tr>
</thead>
<tbody>
{tableData.map((row, i) => (
<tr
key={`table-row-${i}`}
className={
reveal.find((r) => r === row.id) ||
row.id === chartState.hoveredEvent
? "hovered"
: ""
}
onMouseOver={() => onRowHover(row.id)}
>
{tableColumns.map((column, c) =>
column.accessor === "type" ? (
<td key={`table-data-${i}-${c}`}>
<span
className="default-badge"
style={{
backgroundColor: getBadgeBackground(row, column),
}}
>
{row[column.accessor] ?? "event"}
</span>
</td>
) : (
<td key={`table-data-${i}-${c}`}>{row[column.accessor]}</td>
),
)}
</tr>
))}
</tbody>
</table>
</div>
);
}
export function Demo() {
// Chart and timeline instance methods.
const chartRef = useRef({});
const timelineRef = useRef({});
// Keep aggregated links in a ref so we can access their setStyle methods later.
const aggregatedLinks = useRef({});
// Remove warnings from the playground.
const [state, untypedDispatch] = useReducer(reducer, initialState);
const dispatch = (action) => untypedDispatch.call(null, action);
const {
chartState,
timelineState: { events, highlight, focus, reveal, selection, range },
inRangeItems,
tableData,
} = state;
function setAggregatedLinkWidth(width) {
Object.values(aggregatedLinks.current).forEach((aggregate) =>
aggregate.setStyle({ width }),
);
}
function getAggregateLinkById(id) {
return Object.values(aggregatedLinks.current).filter((l) =>
Object.keys(l?.links ?? {}).find((a) => a === id),
)[0];
}
// Callback invoked when hovering over any part of the timeline.
function handleTimelineHover({ targetType, id }) {
// Set hoveredEntity in the chartState to emphasise the corresponding node.
if (targetType === "entity") {
setAggregatedLinkWidth(defaultLinkWidth);
dispatch({ type: "hover_entity_on_timeline", id });
// Increase the event line width and set hoveredEvent in chartState.
} else if (targetType === "event") {
dispatch({ type: "hover_event_on_timeline", id });
// Reset the hover state and event widths if no event is selected and hovering something other than and event or entity.
} else if (chartState.hoveredEntity || chartState.hoveredEvent) {
setAggregatedLinkWidth(defaultLinkWidth);
dispatch({ type: "reset_timeline_hover" });
}
}
// Callback invoked when clicking on the timeline.
async function handleTimelineClick({ targetType, eventIds }) {
if (targetType === "event") {
if (eventIsSelected(selection, eventIds)) {
return dispatch({ type: "reset_timeline_click" });
}
const aggregate = getAggregateLinkById(eventIds[0]);
const linkNeighbors = await neighbors(
chartData,
Object.keys(aggregate?.links ?? {}),
);
const selectedEventNeighbors =
Object.entries(linkNeighbors ?? {})
.filter(([, node]) => isNode(node))
.map(([id]) => id) ?? [];
dispatch({
type: "click_event_on_timeline",
eventIds,
toFit: selectedEventNeighbors,
selection: aggregate?.id ? { [aggregate.id]: true } : {},
});
// If currently focused or an event is selected.
} else if (
(targetType === "focus" && focus.length) ||
linkIsSelected(chartState.selection)
) {
dispatch({ type: "reset_timeline_click" });
// Nothing is currently selected.
} else if (!Object.keys(chartState.selection).length) {
timelineRef.current.fit();
}
}
// Callback invoked when any state of the timeline changes.
async function handleTimelineChange(event) {
if (event.range) {
dispatch({
type: "timeline_range_change",
range: event.range,
inRangeItems: timelineRef.current?.getInRangeItems(),
markers,
});
}
if (event.focus?.length) {
const toFocus = event.focus.length === 0 ? [] : event.focus[0];
const validChartItem = chartRef.current.getItemInfo(toFocus);
const nodesToForeground = validChartItem
? await getNodesToForegroundAndFit(toFocus)
: [];
const toFit =
toFocus.length === 0 || !validChartItem
? "all"
: [...Object.keys(nodesToForeground), toFocus];
dispatch({
type: "timeline_focus",
focus: toFocus,
toFit,
inRangeItems: timelineRef.current?.getInRangeItems(),
selection: event.focus[0] ? { [event.focus[0]]: true } : {},
});
}
}
// Invoked after link aggregation calculations are run. Used to add the number glyphs to links.
function handleChartAggregateLinks(props) {
const { links: linkDict, setStyle, id, change } = props;
if (change !== "deleted") {
aggregatedLinks.current = {
...aggregatedLinks.current,
[id]: {
...props,
},
};
const links = Object.values(linkDict);
const glyphs = [
{
color: "white",
border: {
color: aggregateLinkColor,
},
label: {
text: links.length,
color: aggregateLinkColor,
},
},
];
const style = {
width: Math.min(links.length, 6),
color: aggregateLinkColor,
glyphs,
};
setStyle(style);
}
}
// Invoked when any chart items are interacted with. Handles node/link hovering and selection.
async function handleChartItemInteraction(props) {
const { hovered, id, aggregate, setStyle, item, selected } = props;
// True when the node or link is selected.
if (hovered && selected && !chartState.selection[id]) {
// Fit when selecting to ensure entity and neighbors wll be included in the timeline.
if (!chartState.selection[id]) {
timelineRef.current.fit();
}
// If the selected item is a link.
if (aggregate) {
const linkNeighbors = await neighbors(
chartData,
Object.keys(aggregate.links),
);
const foregroundNodes = Object.fromEntries(
Object.entries(linkNeighbors)
.filter(([, node]) => isNode(node))
.map(([nodeId]) => [nodeId, { fade: false }]),
);
setStyle(foregroundNodes);
dispatch({
type: "chart_link_select",
id,
links: aggregate.links,
toFit: Object.keys(foregroundNodes),
});
// If the selected item is a node.
} else {
const foregroundNodes = await getNodesToForegroundAndFit(id);
setStyle(foregroundNodes);
dispatch({
type: "chart_node_select",
id,
toFit: [...Object.keys(foregroundNodes), id],
});
}
// If the selected item is being hovered.
} else if (hovered) {
if (aggregate) {
dispatch({ type: "chart_link_hover", links: aggregate.links });
setStyle({
[id]: {
width: Math.min(Object.keys(aggregate.links).length * 5, 10),
},
});
} else {
dispatch({ type: "chart_node_hover", id });
setStyle({
[id]: {
halos: [
{
color: highlightColor,
radius: 30,
width: 6,
},
],
size: item.size * 1.15,
},
});
}
}
}
// Invoked when the mouse moves in the chart. Handles reseting timeline styling after moving off a node/link.
function handleChartOnPointerMove({ id }) {
// If the pointer is moving over an empty part of the chart and we haven't selected a node.
if (id === null) {
if (highlight) {
dispatch({ type: "chart_on_pointer_move_reset_highlight" });
}
if (reveal.length) {
dispatch({ type: "chart_on_pointer_move_reset_events" });
}
}
}
// Invoked when clicking anywhere on the chart. Handles reseting chart and timeline when clicking away from a node/link.
function handleChartOnClick({ id }) {
// If an empty part of the chart is clicked.
if (id === null) {
if (focus.length) {
dispatch({ type: "chart_on_click_reset_focus", toFit: "all" });
timelineRef.current.fit();
}
if (selection.length) {
dispatch({
type: "chart_on_click_reset_timeline_selection",
toFit: "all",
});
timelineRef.current.fit();
}
dispatch({ type: "chart_on_click_reset_selection", toFit: "all" });
}
}
// Invoked when the chart state changes.
function handleChartOnChange({ positions, selection: chartSelection }) {
// Save the chart positions after the first layout to prevent the chart moving on hover.
if (!chartState.positions) {
dispatch({ type: "chart_set_positions", positions });
}
// Save the current chart selection.
if (chartSelection) {
dispatch({ type: "chart_set_selection", selection: chartSelection });
}
}
function handleChartOnUpdateComplete() {
if (chartState.toFit !== undefined) {
dispatch({ type: "chart_fit_complete" });
}
}
// Invoked when hovering over a table row. Handles higlighting link on the chart, and corresponding event on the timeline.
function handleTableRowHover(id) {
// If we are over a row and that row isn't a marker.
if (id && !id.includes("marker")) {
if (chartState.hoveredEvent !== id) {
setAggregatedLinkWidth(defaultLinkWidth);
dispatch({ type: "table_row_hover", id });
}
// If the id is null the table is no longer being hovered and the highlight can be reset.
} else {
setAggregatedLinkWidth(defaultLinkWidth);
dispatch({ type: "reset_table_row_hover" });
}
}
// Setting the focus prop on the timeline does not raise a range change event, so get the in range items after the focus has been applied.
useEffect(() => {
dispatch({
type: "timeline_range_change",
range,
inRangeItems: timelineRef.current.getInRangeItems(),
markers,
});
}, [focus, chartState.selection]);
// When the state changes, check if we need to fit the chart. The fit needs to happen after chart updates have been applied.
useEffect(() => {
if (chartState.toFit) {
chartRef.current.fit(chartState.toFit);
}
}, [state]);
// Set betweenness when the chart data is loaded.
if (chartData && !chartState.scores) {
betweenness(chartData, { value: "count" }).then((scores) =>
dispatch({ type: "chart_set_betweenness_scores", scores }),
);
}
// Runs on each render and applies any state based styling to the chart items.
const styledChartItems = Object.fromEntries(
Object.entries(chartData).map(([id, item]) => {
const updatedItem = { ...item };
if (isNode(item)) {
// Sets the node size based on betweenness.
if (chartState.scores) {
updatedItem.size = chartState.scores[id];
}
// If no event is selected then set fade to false.
if (!linkIsSelected(chartState.selection)) {
updatedItem.fade = false;
}
// Fade if an event is selected and the item id is not selected.
if (
linkIsSelected(chartState.selection) &&
!Object.keys(chartState.selection).find((f) => f.includes(id))
) {
updatedItem.fade = true;
}
// Fade if node is not in range on the timeline.
if (inRangeItems && !inRangeItems.entities[id]) {
updatedItem.fade = true;
}
// Add a halo if the entity if hovered.
if (chartState.hoveredEntity && chartState.hoveredEntity === id) {
updatedItem.halos = [
{
color: highlightColor,
radius: 30,
width: 6,
},
];
updatedItem.size *= 1.15;
}
// Add a halo if the entity is focused.
if (chartState.selection[id]) {
updatedItem.halos = [
{
color: selectionColor,
radius: 30,
width: 6,
},
];
updatedItem.size *= 1.15;
}
// If the item is a link.
} else {
// Fade if link is not in range on the timeline.
if (inRangeItems && !inRangeItems.events[id]) {
updatedItem.fade = true;
}
// If the link is hovered.
if (chartState.hoveredEvent) {
// Find the associated aggregate link.
const aggregate = Object.values(aggregatedLinks.current).filter((l) =>
Object.keys(l.links).find(
(a) => a === id && a === chartState.hoveredEvent,
),
)[0];
// Set the width of aggregate link.
if (aggregate) {
aggregate.setStyle({
width: Object.keys(aggregate.links).length * 5,
});
}
}
}
return [id, updatedItem];
}),
);
return (
<div className="showcase-container">
<div className="row">
<div className="panel-left-item row-item">
<Table
tableData={tableData}
reveal={reveal}
chartState={chartState}
onRowHover={handleTableRowHover}
/>
</div>
<div className="panel-right-item row-item">
<Chart
id="chart"
ref={chartRef}
items={styledChartItems}
options={chartOptions}
style={{ position: "absolute" }}
positions={chartState.positions}
selection={chartState.selection}
onLinkAggregation={handleChartAggregateLinks}
onItemInteraction={handleChartItemInteraction}
onPointerMove={handleChartOnPointerMove}
onClick={handleChartOnClick}
onChange={handleChartOnChange}
onUpdateComplete={handleChartOnUpdateComplete}
aggregateLinks
/>
</div>
</div>
<div className="row">
<Timeline
id="my-timeline"
className="panel-bottom-item"
ref={timelineRef}
markers={markers}
events={events}
entities={entities}
eventTypes={eventTypes}
entityTypes={entityTypes}
options={timelineOptions}
highlight={highlight}
reveal={reveal}
focus={focus}
eventSelection={selection}
annotations={annotations}
onTimelineHover={handleTimelineHover}
onTimelineChange={handleTimelineChange}
onTimelineClick={handleTimelineClick}
/>
</div>
</div>
);
}
const root = createRoot(document.getElementById("app"));
// The timeline markers use Font Awesome icons (declared in the HTML <head>), so wait for the font before drawing them.
document.fonts.load('900 16px "Font Awesome 5 Free"');
document.fonts.ready.then(() => root.render(<Demo />));/*
To see your data displayed in the playground, update the below `kronoGraphData` object with the following properties:
Timeline data (see the API documentation for the correct format for these):
- entities
- events
- eventTypes
- entityTypes
- time series charts
- markers
- annotations
In addition, there are two further optional properties to customize which
events and entities are shown on the chart. Remove these if all entities or events
should be shown on the chart.
- entityTypeForChart
All entities of this type will be shown as nodes on the chart.
- eventTypeForChart
All events of this type will be shown as links on the chart.
*/
export const kronoGraphData = {
entities: {
indiv0: {
type: "Individuals",
label: "Hector Dickinson",
},
indiv1: {
type: "Individuals",
label: "Franco Abernathy",
},
indiv2: {
type: "Individuals",
label: "Christine Zboncak",
},
indiv3: {
type: "Individuals",
label: "Josh Feil",
},
indiv4: {
type: "Individuals",
label: "Addison Emmerich",
},
indiv5: {
type: "Individuals",
label: "Vivienne Frami",
},
indiv6: {
type: "Individuals",
label: "Preston Kihn",
},
indiv7: {
type: "Individuals",
label: "Nikko Kuphal",
},
indiv8: {
type: "Individuals",
label: "Margaret Hoeger",
},
indiv9: {
type: "Individuals",
label: "Alex Heaney",
},
indiv10: {
type: "Individuals",
label: "Giovanny Barton",
},
indiv11: {
type: "Individuals",
label: "Gunnar Fadel",
},
indiv12: {
type: "Individuals",
label: "Clarabelle Upton",
},
indiv13: {
type: "Individuals",
label: "Obie Ritchie",
},
indiv14: {
type: "Individuals",
label: "Lexi Hilll",
},
indiv15: {
type: "Individuals",
label: "Tierra Wilderman",
},
indiv16: {
type: "Individuals",
label: "Eriberto Dare",
},
indiv17: {
type: "Individuals",
label: "Deon Abshire",
},
indiv18: {
type: "Individuals",
label: "Milton Powlowski",
},
indiv19: {
type: "Individuals",
label: "Zetta Medhurst",
},
indiv20: {
type: "Individuals",
label: "Eloise Pollich",
},
indiv21: {
type: "Individuals",
label: "Celia Olson",
},
indiv22: {
type: "Individuals",
label: "Chanelle Macejkovic",
},
indiv23: {
type: "Individuals",
label: "Casandra Wyman",
},
indiv24: {
type: "Individuals",
label: "Kurtis Koepp",
},
indiv25: {
type: "Individuals",
label: "Jaqueline Nicolas",
},
indiv26: {
type: "Individuals",
label: "Jailyn Grant",
},
indiv27: {
type: "Individuals",
label: "Darron Ritchie",
},
indiv28: {
type: "Individuals",
label: "Russel Lindgren",
},
indiv29: {
type: "Individuals",
label: "Janick Quitzon",
},
indiv30: {
type: "Individuals",
label: "Amparo Crooks",
},
indiv31: {
type: "Individuals",
label: "Zackary Leuschke",
},
inst30: {
type: "Institutions",
label: "Gleason LLC",
},
inst31: {
type: "Institutions",
label: "Schultz, Yost and Kovacek",
},
inst32: {
type: "Institutions",
label: "Gerlach, Conroy and Kessler",
},
inst33: {
type: "Institutions",
label: "Ebert - Hoppe",
},
inst34: {
type: "Institutions",
label: "Doyle - Feeney",
},
inst35: {
type: "Institutions",
label: "Crooks LLC",
},
inst36: {
type: "Institutions",
label: "Schiller Inc",
},
inst37: {
type: "Institutions",
label: "West, Kassulke and Mraz",
},
megacorp: {
type: "Stocks",
label: "MegaCorp MEGC",
},
},
events: {
trade3: {
data: {
value: 97,
},
type: "sell",
entityIds: ["inst30", "megacorp"],
time: 1544863280051,
},
trade2: {
data: {
value: 66,
},
type: "sell",
entityIds: ["inst33", "megacorp"],
time: 1544932562760,
},
trade362: {
data: {
value: 52,
},
type: "sell",
entityIds: ["indiv9", "megacorp"],
time: 1545961198105,
},
trade366: {
data: {
value: 53,
},
type: "sell",
entityIds: ["indiv11", "megacorp"],
time: 1546062480608,
},
trade357: {
data: {
value: 87,
},
type: "buy",
entityIds: ["indiv6", "megacorp"],
time: 1546118620438,
},
trade350: {
data: {
value: 75,
},
type: "sell",
entityIds: ["indiv31", "megacorp"],
time: 1546235895647,
},
trade0: {
data: {
value: 76,
},
type: "buy",
entityIds: ["inst36", "megacorp"],
time: 1546318633467,
},
trade355: {
data: {
value: 61,
},
type: "sell",
entityIds: ["indiv13", "megacorp"],
time: 1546411946097,
},
trade354: {
data: {
value: 80,
},
type: "buy",
entityIds: ["indiv13", "megacorp"],
time: 1546416171344,
},
trade351: {
data: {
value: 56,
},
type: "sell",
entityIds: ["indiv28", "megacorp"],
time: 1546454343534,
},
comms91: {
entityIds: ["indiv10", "indiv5"],
time: 1546637022571,
type: "comms",
},
trade4: {
data: {
value: 56,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1546819595253,
},
trade353: {
data: {
value: 133,
},
type: "sell",
entityIds: ["indiv17", "megacorp"],
time: 1546949377153,
},
trade1: {
data: {
value: 119,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1546953492596,
},
trade356: {
data: {
value: 51,
},
type: "sell",
entityIds: ["indiv9", "megacorp"],
time: 1547138296150,
},
trade9: {
data: {
value: 73,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1547616817135,
},
trade358: {
data: {
value: 101,
},
type: "buy",
entityIds: ["indiv6", "megacorp"],
time: 1547671625200,
},
trade359: {
data: {
value: 130,
},
type: "buy",
entityIds: ["indiv8", "megacorp"],
time: 1547901839308,
},
comms54: {
entityIds: ["indiv16", "indiv14"],
time: 1547914525138,
type: "comms",
},
trade6: {
data: {
value: 149,
},
type: "buy",
entityIds: ["inst31", "megacorp"],
time: 1548006809426,
},
comms28: {
entityIds: ["indiv12", "indiv6"],
time: 1548049675272,
type: "comms",
},
comms92: {
entityIds: ["indiv1", "indiv2"],
time: 1548147977362,
type: "comms",
},
comms94: {
entityIds: ["indiv3", "indiv4"],
time: 1548230526817,
type: "comms",
},
trade379: {
data: {
value: 95,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1548240527111,
},
trade7: {
data: {
value: 96,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1548287469584,
},
trade352: {
data: {
value: 133,
},
type: "sell",
entityIds: ["indiv3", "megacorp"],
time: 1548477393064,
},
trade373: {
data: {
value: 98,
},
type: "sell",
entityIds: ["indiv1", "megacorp"],
time: 1548497234705,
},
trade364: {
data: {
value: 100,
},
type: "buy",
entityIds: ["indiv9", "megacorp"],
time: 1548524365754,
},
trade15: {
data: {
value: 77,
},
type: "buy",
entityIds: ["inst30", "megacorp"],
time: 1548558551151,
},
comms41: {
entityIds: ["indiv11", "indiv12"],
time: 1548610786657,
type: "comms",
},
trade365: {
data: {
value: 133,
},
type: "sell",
entityIds: ["indiv24", "megacorp"],
time: 1548706163359,
},
trade10: {
data: {
value: 94,
},
type: "sell",
entityIds: ["inst33", "megacorp"],
time: 1548756505257,
},
trade368: {
data: {
value: 99,
},
type: "sell",
entityIds: ["indiv3", "megacorp"],
time: 1548790331486,
},
trade8: {
data: {
value: 121,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1548857821042,
},
comms36: {
entityIds: ["indiv9", "indiv10"],
time: 1548861810958,
type: "comms",
},
trade369: {
data: {
value: 96,
},
type: "buy",
entityIds: ["indiv28", "megacorp"],
time: 1548948036836,
},
trade360: {
data: {
value: 106,
},
type: "buy",
entityIds: ["indiv17", "megacorp"],
time: 1548999059744,
},
trade361: {
data: {
value: 101,
},
type: "buy",
entityIds: ["indiv13", "megacorp"],
time: 1549005439784,
},
trade371: {
data: {
value: 95,
},
type: "buy",
entityIds: ["indiv25", "megacorp"],
time: 1549012395858,
},
comms61: {
entityIds: ["indiv21", "indiv18"],
time: 1549014056271,
type: "comms",
},
trade12: {
data: {
value: 81,
},
type: "buy",
entityIds: ["inst36", "megacorp"],
time: 1549041007770,
},
trade370: {
data: {
value: 90,
},
type: "buy",
entityIds: ["indiv25", "megacorp"],
time: 1549131796749,
},
trade11: {
data: {
value: 98,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1549215075302,
},
trade374: {
data: {
value: 99,
},
type: "buy",
entityIds: ["indiv19", "megacorp"],
time: 1549229001558,
},
trade5: {
data: {
value: 131,
},
type: "sell",
entityIds: ["inst30", "megacorp"],
time: 1549348177806,
},
trade372: {
data: {
value: 139,
},
type: "sell",
entityIds: ["indiv6", "megacorp"],
time: 1549413983235,
},
trade388: {
data: {
value: 83,
},
type: "sell",
entityIds: ["indiv26", "megacorp"],
time: 1549634315501,
},
comms76: {
entityIds: ["indiv27", "indiv29"],
time: 1549787487498,
type: "comms",
},
trade386: {
data: {
value: 58,
},
type: "sell",
entityIds: ["indiv27", "megacorp"],
time: 1549817193806,
},
trade375: {
data: {
value: 95,
},
type: "sell",
entityIds: ["indiv1", "megacorp"],
time: 1549835055727,
},
trade367: {
data: {
value: 125,
},
type: "sell",
entityIds: ["indiv25", "megacorp"],
time: 1549925638037,
},
trade378: {
data: {
value: 82,
},
type: "sell",
entityIds: ["indiv1", "megacorp"],
time: 1549929025705,
},
trade376: {
data: {
value: 83,
},
type: "buy",
entityIds: ["indiv11", "megacorp"],
time: 1549954115199,
},
comms64: {
entityIds: ["indiv23", "indiv19"],
time: 1549958971354,
type: "comms",
},
trade377: {
data: {
value: 121,
},
type: "sell",
entityIds: ["indiv1", "megacorp"],
time: 1550116482939,
},
trade14: {
data: {
value: 96,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1550198143577,
},
trade363: {
data: {
value: 128,
},
type: "buy",
entityIds: ["indiv29", "megacorp"],
time: 1550205623089,
},
trade13: {
data: {
value: 117,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1550285702957,
},
trade23: {
data: {
value: 87,
},
type: "sell",
entityIds: ["inst35", "megacorp"],
time: 1550369730674,
},
comms6: {
entityIds: ["indiv5", "indiv3"],
time: 1550380741503,
type: "comms",
},
trade20: {
data: {
value: 66,
},
type: "sell",
entityIds: ["inst31", "megacorp"],
time: 1550381531060,
},
trade393: {
data: {
value: 78,
},
type: "sell",
entityIds: ["indiv28", "megacorp"],
time: 1550533992626,
},
trade382: {
data: {
value: 58,
},
type: "sell",
entityIds: ["indiv13", "megacorp"],
time: 1550537856984,
},
trade380: {
data: {
value: 123,
},
type: "sell",
entityIds: ["indiv31", "megacorp"],
time: 1550549443723,
},
comms62: {
entityIds: ["indiv17", "indiv18"],
time: 1550570602856,
type: "comms",
},
comms32: {
entityIds: ["indiv11", "indiv7"],
time: 1550642171658,
type: "comms",
},
trade24: {
data: {
value: 72,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1550686265676,
},
trade381: {
data: {
value: 128,
},
type: "sell",
entityIds: ["indiv8", "megacorp"],
time: 1550707812147,
},
comms60: {
entityIds: ["indiv15", "indiv17"],
time: 1550760224316,
type: "comms",
},
comms66: {
entityIds: ["indiv31", "indiv28"],
time: 1550774656969,
type: "comms",
},
trade16: {
data: {
value: 87,
},
type: "buy",
entityIds: ["inst35", "megacorp"],
time: 1550817061586,
},
comms98: {
entityIds: ["indiv3", "indiv4"],
time: 1550910233770,
type: "comms",
},
trade384: {
data: {
value: 94,
},
type: "sell",
entityIds: ["indiv22", "megacorp"],
time: 1550920358021,
},
trade385: {
data: {
value: 65,
},
type: "sell",
entityIds: ["indiv12", "megacorp"],
time: 1551185545858,
},
trade17: {
data: {
value: 127,
},
type: "buy",
entityIds: ["inst37", "megacorp"],
time: 1551260943342,
},
trade383: {
data: {
value: 121,
},
type: "sell",
entityIds: ["indiv0", "megacorp"],
time: 1551392504914,
},
trade26: {
data: {
value: 53,
},
type: "buy",
entityIds: ["inst32", "megacorp"],
time: 1551422021586,
},
trade399: {
data: {
value: 54,
},
type: "sell",
entityIds: ["indiv4", "megacorp"],
time: 1551496824364,
},
comms15: {
entityIds: ["indiv4", "indiv3"],
time: 1551556509009,
type: "comms",
},
comms10: {
entityIds: ["indiv4", "indiv3"],
time: 1551601437264,
type: "comms",
},
trade387: {
data: {
value: 118,
},
type: "sell",
entityIds: ["indiv2", "megacorp"],
time: 1551611750874,
},
comms73: {
entityIds: ["indiv25", "indiv26"],
time: 1551616091026,
type: "comms",
},
trade19: {
data: {
value: 88,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1551642966529,
},
trade392: {
data: {
value: 73,
},
type: "sell",
entityIds: ["indiv28", "megacorp"],
time: 1551646156864,
},
comms88: {
entityIds: ["indiv8", "indiv17"],
time: 1551928887516,
type: "comms",
},
comms99: {
entityIds: ["indiv2", "indiv1"],
time: 1552103082493,
type: "comms",
},
trade394: {
data: {
value: 91,
},
type: "sell",
entityIds: ["indiv23", "megacorp"],
time: 1552125924511,
},
trade389: {
data: {
value: 117,
},
type: "sell",
entityIds: ["indiv4", "megacorp"],
time: 1552191597649,
},
comms488: {
type: "comms",
entityIds: ["indiv31", "indiv30"],
time: 1552348800000,
},
trade484: {
data: {
value: 73,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552366080000,
},
trade485: {
data: {
value: 57,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552383360000,
},
trade486: {
data: {
value: 104,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552400640000,
},
trade487: {
data: {
value: 127,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552417920000,
},
trade94: {
data: {
value: 70,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1552435352076,
},
trade105: {
data: {
value: 149,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1552435354397,
},
trade63: {
data: {
value: 147,
},
type: "sell",
entityIds: ["inst32", "megacorp"],
time: 1552435405092,
},
trade104: {
data: {
value: 66,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1552435648746,
},
trade456: {
data: {
value: 52,
},
type: "sell",
entityIds: ["indiv30", "megacorp"],
time: 1552435714150,
},
trade92: {
data: {
value: 132,
},
type: "sell",
entityIds: ["inst35", "megacorp"],
time: 1552435930605,
},
trade136: {
data: {
value: 119,
},
type: "sell",
entityIds: ["inst36", "megacorp"],
time: 1552436058968,
},
trade83: {
data: {
value: 124,
},
type: "sell",
entityIds: ["inst31", "megacorp"],
time: 1552436102040,
},
trade458: {
data: {
value: 109,
},
type: "sell",
entityIds: ["indiv9", "megacorp"],
time: 1552436776655,
},
trade128: {
data: {
value: 146,
},
type: "buy",
entityIds: ["inst34", "megacorp"],
time: 1552436908736,
},
trade130: {
data: {
value: 58,
},
type: "sell",
entityIds: ["inst36", "megacorp"],
time: 1552437817417,
},
trade99: {
data: {
value: 61,
},
type: "sell",
entityIds: ["inst34", "megacorp"],
time: 1552438365823,
},
trade454: {
data: {
value: 53,
},
type: "sell",
entityIds: ["indiv10", "megacorp"],
time: 1552438725318,
},
trade140: {
data: {
value: 68,
},
type: "sell",
entityIds: ["inst37", "megacorp"],
time: 1552438828919,
},
trade453: {
data: {
value: 76,
},
type: "sell",
entityIds: ["indiv12", "megacorp"],
time: 1552439085436,
},
trade67: {
data: {
value: 106,
},
type: "sell",
entityIds: ["inst30", "megacorp"],
time: 1552440950631,
},
trade72: {
data: {
value: 103,
},
type: "sell",
// ...truncated 3848 lines<!doctype html>
<html>
<head>
<link rel="stylesheet" href="@ci/theme/kg/css/examples.css">
<link
rel="stylesheet"
href="@fortawesome/[email protected]/css/fontawesome.css"
>
<link
rel="stylesheet"
href="@fortawesome/[email protected]/css/solid.css"
>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="app" class="playground"></div>
<script type="module" src="./code.jsx"></script>
</body>
</html>.showcase-container {
display: flex;
height: 100vh;
width: 100%;
flex-direction: column;
}
.row {
flex-direction: row;
width: 100%;
max-height: 50%;
min-height: 50%;
}
.panel-left-item {
border-right: 1px solid #e4e7ea;
}
.panel-right-item {
position: relative;
}
.panel-bottom-item {
border-top: 1px solid #e4e7ea;
}
.row-item {
flex: 1;
height: 100%;
}
#chart {
height: 100%;
width: 100%;
position: absolute;
}
#my-timeline {
flex: 1;
height: 100%;
}
#table-container {
overflow: scroll;
height: 100%;
background-color: #252c32;
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Hide scrollbar for Chrome, Safari and Opera */
#table-container::-webkit-scrollbar {
display: none;
}
table {
width: 100%;
border-collapse: collapse;
color: #e4e7ea;
font-size: 11px;
}
th,
td {
padding: 6px;
text-align: left;
}
th {
background-color: #252c32;
position: sticky;
top: 0;
z-index: 1;
}
tr:hover,
tr.hovered {
background-color: #2a333c;
}
.default-badge {
display: flex;
background-color: red;
color: black;
padding: 2px 4px;
border-radius: 3px;
flex: 1;
text-align: center;
justify-content: center;
align-items: center;
}