Getting Started

Introduction

KronoGraph is a JavaScript library for building time-based visualizations.

To see how KronoGraph works with KeyLines and ReGraph , explore the Integration Playground.

If you need help with converting your data into the correct KronoGraph format, see the Handling Data section.

If you would like an introduction to the basic principles of building a timeline and get up-and-running with KronoGraph, follow this guide. We are going to embed a simple timeline in an HTML pagea React application, before adding data and attaching an event handler.

For the ReactJavaScript version of this guide, use the toggle in the top left corner.

To help accelerate the design and build of branded timeline visualizations that fit seamlessly into your application UI, download our free Figma Design Kit.

Creating and Serving an App

First we need to create an app to add a Timeline component to. If you don't have an existing project, you can easily create one with Vite using your preferred package.

npm create vite@latest my-kronograph-app -- --template vanilla
copy code
npm create vite@latest my-kronograph-app -- --template react
copy code

Once this process has completed, you can start your app:

cd my-kronograph-app
npm install 
npm run dev
copy code

By default, Vite runs a development server at http://localhost:5173 .

Installing

Now that you have an app, download the latest version of KronoGraph:

For Safari browsers, hold the ⌥ Option key when pressing the Download button to download the correct .tgz format.

Open a new terminal window and access your project's root folder, which contains the app you just created:

cd my-kronograph-app
copy code

Next move the download into this folder. Do not change the file name. If you are on a UNIX-like operating system you can move the download from the terminal:

mv ~/Downloads/ .
copy code

From your project's root folder, install the bundle as a local package:

npm install file:
copy code

Creating a Timeline

It's time to add a timeline to the app.

First we need to add an element to the HTML page so that KronoGraph can attach a Timeline to it. We then call createTimeline(), passing the id of the parent element to it.

Let's load the files into the page and create a div for the Timeline. Add the following to index.html:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Hello, world!</title> <link rel="stylesheet" href="/src/style.css" /> <script type="module" src="/src/main.js"></script> <!--if you are using TypeScript, delete the line above and then uncomment the line below--> <!--<script type="module" src="main.ts">--> </head> <body> <div id="my-timeline" class="timeline"></div> </body> </html>
copy code

Next we import createTimeline into the page, create a Timeline element in main.js, and add some data:

// main.js
import { createTimeline } from 'kronograph'; const timeline = createTimeline('my-timeline'); const data = { entities: { 'Person 1': {}, 'Person 2': {} }, events: { 'Phone call 1': { entityIds: ['Person 1', 'Person 2'], time: new Date(2020, 6, 1, 12, 0), }, 'Phone call 2': { entityIds: ['Person 1', 'Person 2'], time: new Date(2020, 6, 1, 13, 0), }, 'Phone call 3': { entityIds: ['Person 2', 'Person 1'], time: new Date(2020, 6, 1, 14, 0), }, }, }; timeline.set(data); timeline.fit();
copy code

And define some styling in style.css:

/* style.css */
.timeline {
  height: 100vh;
  width: 100vw;
}

body {
  margin: 0;
}
copy code

This guide assumes that your component is declared in App.jsx. You can also use a separate file but you will need to make sure your architecture includes it in the app.

To create a timeline, we need to import the Timeline component into the app and then pass some data into the Timeline component's props. Replace the contents of App.jsx with:

// App.jsx
import React from 'react'; import Timeline from 'kronograph/react/Timeline'; const data = { entities: { 'Person 1': {} , 'Person 2': {} }, events: { 'Phone call 1': { entityIds: ['Person 1', 'Person 2'], time: new Date(2020, 6, 1, 12, 0), }, 'Phone call 2': { entityIds: ['Person 1', 'Person 2'], time: new Date(2020, 6, 1, 13, 0), }, 'Phone call 3': { entityIds: ['Person 2', 'Person 1'], time: new Date(2020, 6, 1, 14, 0), }, }, }; function App() { return ( <div style={{ height: '100vh', width: '100vw' }}> <Timeline entities={data.entities} events={data.events} /> </div> ); } export default App;
copy code

The timeline sets its height and width to equal the size of its parent element, so any styles should be set on the parent.

Your timeline should now look like this:

Handling Interactions

KronoGraph has a few useful default functions to let users navigate the timeline. You can add your own custom behavior to user interactions by attaching a custom handler:

To define a custom handler, add a new handler function after your data in main.js:

// main.js
function clickHandler(event) { window.alert(`You clicked a timeline item of type '${event.targetType}'`); }
copy code

And then attach it to the Timeline by adding this after createTimeline():

// main.js
timeline.on('click', clickHandler);
copy code

To define a custom handler, add a new handler to the App() function in App.jsx:

// App.jsx
const clickHandler = event => { window.alert(`You clicked a timeline item of type '${event.targetType}'`); }
copy code

And now pass it to the onClick prop:

// App.jsx
<Timeline entities={data.entities} events={data.events} onTimelineClick={clickHandler} />
copy code

Your app should now look like the one below. Try clicking on entities and events on the timeline to see their type in a browser alert.

Congratulations! You have created your first KronoGraph app. Here are some ideas what to do next:

Integrating with Other Products

KronoGraph can be integrated with other Cambridge Intelligence products, such as KeyLines ReGraph and MapWeave , to show timeline visualizations of your data alongside a chart or map visualization. You can configure your application so that when the data updates, or a user interaction changes the timeline, the changes are reflected in the other product and vice versa.

See the Integration Playground or the Integrations section of our Storybook for examples.

In this section we refer to KronoGraph timelines and entities, KeyLines ReGraph charts and nodes, and MapWeave maps and nodes.

General Considerations

There's no one standard way to integrate KronoGraph with another of our products as it depends on your data model and the interactions you want to offer your end users. It's worth considering:

  • How will you model and convert your data between the two? Bear in mind that this also determines the relationship between your nodes and entities. Will you map timeline events directly to links between nodes?
  • How will you display the app components. Do you want to display the timeline and the chart, or map, side by side, or in separate tabs? Do you also want to include a time bar?
  • What interaction patterns between the two products will you use?

The following sections illustrate the most common interaction patterns to get you started:

  • Using the current timeline range to just display information related to that time period in the chart or map.
  • Selecting a node to highlight its associated entity in the timeline.
  • Setting the focus on an entity in the timeline to highlight its associated node in the chart or map.

We provide a set of stories to illustrate how to do this with each of our products, and an Integration Playground

Integrating KeyLines ReGraph

Integrate KronoGraph with KeyLines ReGraph to show both timeline and chart visualizations of your data together.

Filtering the Chart by Time

Keep the chart synchronized with the time range shown in the timeline. As the user pans or zooms the timeline, the chart should then only show connections that are present during the displayed time range.

When the timeline range changes, handle it:

timeline.on('range', timelineRangeHandler);
copy code
onTimelineChange={timelineChangeHandler}
copy code

Create a function to identify the entities currently visible in the timeline, and then display just their corresponding nodes in the chart. The isEmpty function, pickBy function, used here, is provided by the lodash library.

async function timelineRangeHandler() {
  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');
    }
};
copy code
function timelineChangeHandler() {
  const { entities, events } = timeline.current.getInRangeItems({ focus: false });
  setChartItems(pickBy(chartData, (_item, id) => events[id] || entities[id]));
};
copy code

This use of range and getInRangeItems is illustrated in the KeyLines and ReGraph Integration Basics story.

This use of range and getInRangeItems is illustrated in the KeyLines and ReGraph Integration Basics story.

Focusing on Entities to Select Chart Items

The KronoGraph focus API focus API filters the timeline so that only focused items and their connections are shown. This example shows how to use this to select the corresponding items in the chart.

In this example chart nodes have the same ids as their corresponding timeline entities.

When an entity is focused in the timeline, handle it:

timeline.on('focus', ({ focus }) => applySelection(focus));
copy code
onTimelineChange={timelineChangeHandler}
copy code

Create a function that passes the focused items to chart selection:

async function applySelection(selection) {
  chart.selection(selection);
};
copy code
function timelineChangeHandler({ focus }) {
  if (focus){
    setFocusedItems(Object.fromEntries(focus.map(id => [id, true])));
  }
}
copy code

This use of focus is illustrated in the KeyLines and ReGraph Integration Basics story.

This use of focus is illustrated in the KeyLines and ReGraph Integration Basics story.

Selecting Chart Items to Set Their Focus

The example below shows how to display the timelines associated with items selected in the chart.

In this example chart nodes have the same ids as their corresponding timeline entities.

When an item is selected in the chart, handle it:

chart.on('selection-change', () => applySelection(chart.selection()));
copy code
onChange={chartChangeHandler}
copy code

Create a function that passes the selected items to the timeline, and sets their focus:

async function applySelection(selection) {
  const selectedNodeIds = selection.filter(itemId => chart.getItem(itemId).type === 'node');
  timeline.focus(selectedNodeIds);
};
copy code
function chartChangeHandler({ selection }) {
  if (selection) {
    setFocusedItems(selection);
  }    
}
copy code

This use of focus is illustrated in the KeyLines and ReGraph Integration Basics story.

This use of focus is illustrated in the KeyLines and ReGraph Integration Basics story.

Integrating MapWeave

Integrate KronoGraph with MapWeave to conduct in-depth temporal analysis on your geospatial data. You can use the visible timeline range to filter data, or set the focus on entities to filter and zoom on specific nodes and their journeys on the map. You can also use KronoGraph features such as the Ping API Ping API to highlight the item on the timeline, which can be useful for busy datasets.

Filtering Information on a Map by Time Range

Keep the information displayed in MapWeave synchronized with the timeline's time range. As the user pans or zooms the timeline, the map should then only show nodes involved with events displayed in the time range.

When the timeline range changes, handle it:

timeline.on('range', timeRangeChangeHandler);
copy code
onTimelineChange={timelineChangeHandler}
copy code

Create a function to identify the entities currently visible in the timeline, and then display just their corresponding nodes in the map:

function timeRangeChangeHandler( range ) {
  networkLayer.options({ timeFilterRange: range });
}
copy code
function timelineChangeHandler({ range }) {

  setNetworkLayerOptions(options => {
    return { ...options, timeFilterRange: range };
  });
}
copy code

This use of range range is illustrated in the MapWeave Integration Basics story.

Focusing Items to Select Nodes on the Map

The KronoGraph focus API focus API filters the timeline so that only focused items and their connections are shown. The example below shows how to use this to select the corresponding nodes in the map.

In this example the MapWeave nodes have the same ids as their corresponding timeline entities.

Get the items which have focus in the timeline:

timeline.on('focus', focusHandler);
copy code
onTimelineChange={timelineChangeHandler}
copy code

Create a function that passes them to MapWeave and displays just these focused items:

function focusHandler(event) {
  const focusedNode  = event.focus[0];
  const neighbours = graphEng.neighbors(focusedNode );
  const neighbourNodes = neighbours.nodeIds;

  networkLayer.options({
    foreground: { ids: [
      ...neighbourNodes.concat(neighbours.linkIds),
      focusedNode ]},
  });
}
copy code
function timelineChangeHandler({ focus }) {
  if (focus) {
      const focusedNode = focus[0];
      const neighbours = graphEngine.neighbors(focusedNode);
      const neighbourNodes = neighbours.nodeIds;

      setNetworkLayerOptions(options => {
        return {
          ...options,
          foreground: 
          { ids: [
            ...neighbourNodes.concat(neighbours.linkIds),
            focusedNode, ]},
        };
      });
  }}
copy code

This use of focus focus is illustrated in the MapWeave Integration Basics story.

Selecting a Node to Set its Focus

The example below shows how to display the timelines associated with a selected node.

In this example nodes on the map have the same ids as their corresponding timeline entities.

Catch each node that's clicked in the map:

mapweave.on('click', clickHandler);
copy code
onClick={clickHandler}
copy code

Create a function that shows just that node, and passes the selected item to the timeline, and displays it with focus set:

function clickHandler(click) {
  const clickedItem = click.item;
  if (clickedItem.type === 'node') {

    networkLayer.options({
      foreground: { ids: [
        click.id ],
        backgroundOpacity: 0 },
    });

    timeline.focus(click.id);
  }
}
copy code
function clickHandler(click) {
  const clickedItem = click.item;
  if (clickedItem.type === 'node') {

    setNetworkLayerOptions(options => {
      return {
        ...options,
        foreground: { 
          ids: [ click.id ],
          backgroundOpacity: 0 },
      };
    });

    setFocus(click.id);
  }
}
copy code

This use of focus focus is illustrated in the MapWeave Integration Basics story.

Updating KronoGraph

To update the version of KronoGraph, or to update your license, you need to replace your existing kronograph.tgz bundle with the latest version and add it to your project.

First, download the latest KronoGraph bundle:

Safari users can hold the ⌥ Option key when pressing the Download button to download the correct .tgz format.

Open a new terminal window and access your project's root folder.

From there, move the download into your project's root folder. Do not change the file name. If you are on a UNIX-like operating system you can move the download from the terminal:

mv ~/Downloads/ .
copy code

If you already have a bundle in your project, you should remove it now.

Finally, remove the old version and install the bundle again from inside your project's root folder using your package manager:

npm uninstall kronograph npm install file:
copy code

To verify the upgrade was successful, you can either ask your package manager which version was installed:

npm list kronograph
copy code

Or consult the header comments in node_modules/kronograph/kronograph.js.

If you can't see the correct version installed or if the chart isn't displaying correctly, try clearing the package manager's cache and installing the package again:

npm cache clean --force npm install file:
copy code

If you need any help with updating please contact support.

Timeline Basics

Items

KronoGraph is a JavaScript library that lets you add Timeline components to your web applications. These components show the timelines of entities and the events that involve them, creating time-based data visualizations.

Select an option from the drop-down list to see interactive examples of items such as entities, events, annotations, lens view, markers, time series charts and more:

Show:

Entities are persistent, uniquely identifiable actors or objects, shown using horizontally-drawn timelines. Entities can be, for example, people, phone numbers or bank accounts, and they can have a type that defines their basic styling, and can be used to group entities together.

Interactions

KronoGraph allows you to interact with your entities and their timelines in different ways to explore your data.

Select an option from the drop-down list to see examples of interactions such as navigating the timeline, changing the time scale, and using lens view, event folds, pins, focus and more:

Show:
  • Drag or scroll horizontally to pan along the entity timelines.
  • Scroll vertically to zoom in and out of the timelines.
  • Shift + drag to draw a selection marquee and zoom into that time range.

Note that entities are automatically arranged more compactly as you zoom out. If you zoom out far enough, labels are no longer shown. The lens control on the left allows you to toggle lens view and see the labels.

Timeline Customization

KronoGraph allows you to set up the timeline in many different ways so you can change its look, feel and behavior to suit your requirements and data.

Options

Most timeline settings can be found in TimelineOptions. Pass them to the options method options prop to make the changes.

The timeline below uses the default settings. Select an option from the drop-down box to see the effect of changing the background color, entity label colors, focus control display, fonts, highlight colors, scale positions, annotations settings and more:

Show:

This is the default timeline. If no timeline options are specified, then these default values are used.

Show Lens Highlight

When the lens control is shown, because there's not enough vertical space to show entity labels, the entities to be magnified in lens view are, by default, highlighted.

To disable the highlight, as illustrated here, use the showLensHighlight option:

 timeline.options({ showLensHighlight: false });
copy code
 <Timeline
    options={{ 
      showLensHighlight: false, 
    }}
  />
copy code

See the Lens story for more information.

Expanded RowsBeta

Usually an entity's timeline is displayed using a single row. If entities have duration events overlapping with instantaneous ones, or events that are close together, it can be helpful to add extra sub-rows to display them more clearly (if there's room).

You can set entity rows to expand using the expandedRows API so that users can zoom in on busy areas and see individual events more clearly, as illustrated:

You can set entity rows to expand using the expandedRows method so that users can zoom in on busy areas and see individual events more clearly, as illustrated:

Show:

Using the default settings, duration events are shown on separate sub-rows when overlapping with other events, but event summaries aren't affected.

 timeline.expandedRows('auto');
 timeline.options({
   expandedRows: {
     durationEventsOnly: true,
   },
 });
copy code
 <Timeline
   expandedRows={'auto'}
 />
 <Timeline
    options={{ 
      expandedRows: { 
        durationEventsOnly: true,
      } 
    }}
  />
copy code

Refine how entity rows are expanded using:

  • The expandedRows timeline options allow you to specify display options.
  • The expandedRows object specifies the entities whose rows are to be expanded.
  • The expandedRows event is fired when the set of entities to be expanded changes.
  • The expandedRows prop returns the ids of the entities to be expanded.

To explore further, take a look at the Expanded Rows story.

Entity Label Area Width

By default, KronoGraph adjusts the width of the entity label area automatically to best display the labels in your data. Alternatively, you can set the label area width to a custom value and allow the user to adjust the area width by dragging. See the below example:

 timeline.labelAreaWidth('fixed', 200);
copy code
<Timeline
   labelAreaWidth={{mode: 'fixed', width: 200}}
/>
copy code

The width is set in pixels, and can be anywhere between 125px and 80% of the width of the timeline. Any value outside of these limits will be automatically adjusted to be within the limits.

For further details see the labelAreaWidth() API and the Styling the Timeline story.

For further details see the labelAreaWidth() API and the Styling the Timeline story.

Entities

Entity Styling

Entities run horizontally on the timeline and are shown when they have an associated event in the visible time range.

Customize and style your entities with the properties in the Entity object. Here the first entity has all the default properties and the second entity has some custom properties set:

 const entities = {
  'Default Entity Style': {},
  'Custom Entity Style': {
    color: '#c9477b',
    lineWidth: 5,
    labelColor: '#ffd88f',
    glyph: true,
    fadeOutsideRange: true,
  },
},
copy code

Try out some of the properties in the Styling Entities story.

Entity Types

If you have a number of entities that you want to customize in the same way, use the EntityType object and then assign a type to the Entity. Each entity can only have one type.

In these examples, we are using a subset of the "Bigfoot Sightings" data from the Bigfoot Field Researchers Organization (BFRO) . Here, the numbered sightings are organized into types by State. Each State has its own EntityType where the entity color is set.

There is an assigned order for the California and Colorado entity types. The order property allows you to control the sequence of entity types on the timeline.

This is an example of how the entityTypes are shown in the data:

 const entityTypes = {
  "California": {
    "color": "#e9d8a6",
    "order": 1
  },
  "Connecticut": {
    "color": "#94d2bd"
  },
  "Colorado": {
    "color": "#EE9B00",
    "order": 0
  }
}
copy code

This is how the entityTypes are assigned to the entities using the type property:

 const entities = {
  "7211": {
    "type": "California"
  },
}      
copy code

See types in use in the Styling Items by Type story.

Entity Base Types

Entity types can inherit properties from a baseType. In this example, each entity has a type based on its classification. "Class A" and "Class B" set the labelColor and the entity color is inherited from the "California" entity type.

const entities = {
  "7555": {
    "type": "Class A"
  }
}      
copy code
const entityTypes = {
  "California": {
    "color": "#62b6cb",
  },
  "Class B": {
    "labelColor": "#f9c74f",
    "baseType": "California"
  },
  "Class A": {
    "labelColor": "#90be6d",
    "baseType": "California"
  },
  "Class C": {
    "baseType": "California"
  }
}      
copy code

See the Inherited Styles story for more details.

Default Entity Type

It's possible to specify entity properties using the default entity type:

 const entityTypes = {
  default: {
    color: '#EE9B00',
    lineWidth: 5,
    labelColor: '#EE9B00',
  },
},
copy code

Any entity that doesn't have these properties set will inherit them from the default entity type.

Note the default type cannot have a baseType.

Entity Groups

Further organize entities in the timeline using groupBy to create as many nested levels as needed. Here all the entities have the type as "State" and are grouped by "season" and "classification".

The groupBy property is set within the entityType:

const entityTypes = {
  "California": {
    "color": "#e9d8a6",
    "groupBy": [
      "season",
      "classification"
    ]
  },
}  
copy code

The "season" and "classification" are set within the data object for each entity:

 const entities = {
  "14338": {
    "data": {
      "season": "Fall",
      "classification": "Class A"
    },
    "type": "California"
  },   
}      
copy code

For more details on groups, see the Groups story.

Glyphs

Glyphs are circles displayed alongside entity and entity type labels. They're especially useful for adding extra meaning to your labels or for highlighting certain entities or entity types. You can also style them with custom colors or font icons.

Use the following APIs to add glyphs to your timeline:

 const entityTypes: {
  Customer: {
    typeGlyph: {
      color: '#943651',
      fontIcon: {
        fontFamily: 'Font Awesome 5 Free',
        fontWeight: 100,
        text: '\u{f256}',
      },
    },
  },
  'Sales Person': {
    glyph: {
      color: '#FFCD00',
      fontIcon: {
        fontFamily: 'Font Awesome 5 Free',
        fontWeight: 900,
        text: '\u{f53a}',
      },
    },
  },
},
const entities: {
  'smith-johnathan-151': {
    label: 'John Smith',
    type: 'Customer',
    color: '#E32F42',
  },
  'west-josephine-126': {
    label: 'Josephine West',
    color: '#FF7A4F',
    glyph: true,
    type: 'Customer',
  },
  'roberts-nathaniel-023': {
    label: 'Nathan Roberts',
    color: '#00AFB9',
    type: 'Sales Person',
  },
  'baxter-eleanor-004': {
    label: 'Ella Baxter',
    color: '#0077B6',
    type: 'Sales Person',
  },
},
copy code

Glyphs inherit the color of the entity or entity type that they are applied to unless you assign a color via the Glyph object.

The position of glyphs in relation to the entity label or entity type label is controlled globally via the glyphPosition option.

For more ideas take a look at the Styling Glyphs story.

Events

Event Styling

Events can be styled and customized using the properties in the Event object.

You can emphasize different aspects of events using color, and control the appearance of event lines using lineWidth and showArrows. The Styling Events story allows you to explore and experiment with these settings.

You can also (in beta), add labels to event lines, styling their color and orientation, as illustrated in the Event Labels story.

In the example above the color and lineWidth properties have been changed for "event2".

const events = {
  event1: {
    entityIds: ['entity1'],
    time: new Date(2025, 7, 14, 8, 56),
  },
  event2: {
    entityIds: ['entity2', 'entity1'],
    time: new Date(2025, 7, 14, 8, 56, 10),
    color: '#e07a5f',
    lineWidth: 5,
  },
}
copy code

Event Types

If you have a number of events that you want to customize in the same way, use the EventType object and then asign a type to the Event.

In these examples we are using a subset of the "Street Network Changes" from the City of New York . Here, the street change events are styled by the type of change. Each type of change has its own EventType where a color and fontIcon are added.

This is an example of how the EventTypes are shown in the data:

 const eventTypes = {
  "Two-way Conversion": {
    "color": "#227c9d",
    "fontIcon": {
      "fontFamily": "Font Awesome 5 Free",
      "fontWeight": 900,
      "text": "\uf101"
    }
  },
  "One-way Conversion": {
    "color": "#ffcb77",
    "fontIcon": {
      "fontFamily": "Font Awesome 5 Free",
      "fontWeight": 900,
      "text": "\uf138"
    }
  },
},
copy code

This how the EventTypes are assigned to the events using the type property:

 const events = {
  "172 Stchange": {
    "entityIds": [
      "93 Ave",
      "Jamaica Ave"
    ],
    "time": 1436223600000,
    "type": "One-way Conversion"
  }
}

Event Base Types

Properties can be inherited using a baseType.

In this example each event's color is derived from its type. The fontIcon is set on the baseType of each type.

 const eventTypes = {
  "Street Change": {
    "fontIcon": {
      "fontFamily": "Font Awesome 5 Free",
      "fontWeight": 900,
      "text": "\uf101"
    }
  },
  "Brooklyn": {
    "color": "#17c3b2",
    "baseType": "Street Change"
  },
  "The Bronx": {
    "color": "#227c9d",
    "baseType": "Street Change"
  },
  "Queens": {
    "color": "#ffcb77";
    "baseType": "Street Change"
  }
}  
copy code

See the Inherited Styles story for more details.

Default Event Types

Events that have unspecified properties will inherit them from the default type. Assign properties to the default event type as follows:

 const eventTypes = {
  default: {
    color: '#EE9B00',
  },
},
copy code

Note the default type cannot have a baseType.

Event Summaries

When multiple events are drawn too closely to one another, KronoGraph will combine them into event summaries to prevent overlapping.

For event joins, i.e. the points where events meet entity lines, the summary forms a single combined join taking the most used color of the events it contains.

For event lines, i.e. the vertical lines between connected entities, the summary forms a single translucent shape taking the most used color of the events it contains.

When interacting with any of the parts of an event summary, the targetType in the event handler is 'eventSummary' and the eventIds property contains the ids of all summarized events:

function clickHandler ({ targetType, eventIds }) {
// the user clicked an event or event summary
  if (targetType === 'event' || targetType === 'eventSummary') {
    console.log(`Events hovered: `, eventIds);
  }
}
timeline.on('click', clickHandler);
copy code
const handleClick = ({ targetType, eventIds }) => {
// the user clicked an event or event summary
  if (targetType === 'event' || targetType === 'eventSummary') {
    console.log(`Events hovered: `, eventIds);
  }
}
<Timeline 
entities={entities} 
events={events} 
onTimelineClick={handleClick} 
/>
copy code

Event Folds

When more than one event happens at the same time, KronoGraph allows you to inspect those events more closely in an event fold.

Because the events happen simultaneously, we imagine they are folded behind the timeline. To "unfold" them, event folds can be clicked and opened which brings the individual events into view.

The event folds are only visible once the timeline is sufficiently zoomed in so that any event summary they're part of has been resolved to single event times. Only one event fold can be open at a time.

All events within the fold have the same time stamp, shown by where the fold is attached to the scale. The time is accessible via the Event object, so a tooltip can be added if needed.

Event folds are shown by default. You can hide them using the showEventFolds API.

When interacting with the fold, the targetType in the event handler is 'fold' and the eventIds property contains the ids of the events within the fold.

function clickHandler ({ targetType, eventIds }) {
// the user clicked a fold
  if (targetType === 'fold') {
    console.log(`Events List: `, eventIds);
  }
}
timeline.on('click', clickHandler);
copy code
​​const handleClick = ({ targetType, eventIds }) => {
// the user clicked a fold
  if (targetType === 'fold') {
    console.log(`Events List: `, eventIds);
  }
}
<Timeline 
entities={entities} 
events={events} 
onTimelineClick={handleClick} 
/>
copy code

Take a peek at our Event Folds story for a working example.

Time Series Charts

Time series charts are used to visualize continuous time series data over a recorded period.

You can add one or more time series charts to the timeline. Time series charts share the timeline's scale, aligning the data points with the events displayed in the timeline. This lets you identify interesting points or patterns in the data and investigate the particular events that may be related to them.

This example uses a sample of the Gridwatch dataset, which tracks the sources of energy production in the UK.

To add time series charts to your timeline, pass them in the timeSeriesCharts data object:

To add time series charts to your timeline, pass them in the timeSeriesCharts prop:

const data = {
  events : {},
  entities: {},
  timeSeriesCharts: {
    nuclear: {
      color: "#f48c06",
      data: [
        // ... //
      ]
    },
    gas: {
      color: "#b8d0eb",
      data: [
        // ... //
      ]
    },
    wind: {
      color: "#c77dff",
      data: [
        // ... //
      ]
    },
    hydro: {
      color: "#00b4d8",
      data: [
        // ... //
      ]
    },
    solar: {
      color: "#ffcc00",
      data: [
        // ... //
      ]
    },
  }},
copy code
<Timeline 
events={events} 
entities={entities}
timeSeriesCharts={
  nuclear: {
    color: "#f48c06",
    data: [
      // ... //
    ]
  },
  gas: {
    color: "#b8d0eb",
    data: [
      // ... //
    ]
  },
  wind: {
    color: "#c77dff",
    data: [...
    ]
  },
  hydro: {
    color: "#00b4d8",
    data: [
      // ... //
    ]
  },
  solar: {
    color: "#ffcc00",
    data: [
      // ... //
    ]
  },
}
/>;
copy code

Set the size and position of the timeline in the timeSeriesCharts options:

timeline.options({
  timeSeriesCharts: {
    sizePercent: 100,
    position: 'bottom',
  },
});
copy code
options={{
  timeSeriesCharts: {
    sizePercent: 100, 
    position: 'bottom' 
  },
}}
copy code

Note that if scale wrapping is applied or the scale mode is set to nonlinear, time series charts will be hidden.

Styling

Time series charts can be styled and customized using the properties in the TimeSeriesChart Object.

 const timeSeriesCharts = {
  'Default Chart Style': { 
    data: [
      // ... //
    ],
  },
  'Custom Chart Style': {
    data: [
      // ... //
    ],
    color: '#c9477b',
    labelColor: '#ffd88f',
    fillAreaAlpha: 0.5,
    lineStyle: 'dashed',
  },
},
copy code

Take a look at how the styles can be applied in the Time Series Charts story.

Stacks

If you have more than one time series dataset, the charts can be added to a time series chart stack. This allows you to compare two related time series datasets or save space on the timeline. In this example, the energy types are grouped into 'Renewable' and 'Non-Renewable'.

To stack your time series charts add the timeSeriesChartStack object to your timeline data. Include the chartIds you want to see in each stack.

 const data = {
  events: {},
  entities: {},
  timeSeriesChartStacks: {
    Renewable: {
      chartIds: [
        "wind",
        "hydro",
        "solar"
      ]
    },
    Non-Renewable: {
      chartIds: [
        "nuclear",
        "gas"
      ]
    }
  },
  timeSeriesCharts: {
    nuclear: {
      color: "#f48c06",
      data: [
        // ... //
      ]
    },
    gas: {
      color: "#b8d0eb",
      data: [
        // ... //
      ]
    },
    wind: {
      color: "#c77dff",
      data: [
        // ... //
      ]
    },
    hydro: {
      color: "#00b4d8",
      data: [
        // ... //
      ]
    },
    solar: {
      color: "#ffcc00",
      data: [
        // ... //
      ]
    },
  },  
}  
copy code
<Timeline 
events={events} 
entities={entities}
timeSeriesChartStacks={
  Renewable: {
      chartIds: [
        "wind",
        "hydro",
        "solar"
      ]
    },
    Non-Renewable: {
      chartIds: [
        "nuclear",
        "gas"
      ]
    }
}
timeSeriesCharts={
  nuclear: {
    color: "#f48c06",
    data: [
      // ... //
    ]
  },
  gas: {
    color: "#b8d0eb",
    data: [
      // ... //
    ]
  },
  wind: {
    color: "#c77dff",
    data: [
      // ... //
    ]
  },
  hydro: {
    color: "#00b4d8",
    data: [
      // ... //
    ]
  },
  solar: {
    color: "#ffcc00",
    data: [
      // ... //
    ]
  },
},
}
/>;
copy code

When charts are stacked only the label of the stack is shown. The labels for the individual charts are not shown.

Data Points

The properties of time series chart data points are available via the subItem object. When the targetType is 'timeSeriesChart', the subItem object will be returned with the values for that data point.

timeline.on('click', (pointerEvent) => {
  if (pointerEvent.targetType === 'timeSeriesChart') {
    const {index, subId, time, value, x, y} = pointerEvent.subItem;
    alert([
      `Data point at x = ${x}, y = ${y}`,
      `   Time: ${time}`,
      `   Index: ${index}`,
      `   subID: ${subId}`,            
      `   Value: ${value}`,
    ].join('\n'));
  }
});
copy code
const handleClick = (pointerEvent) => {
  if (pointerEvent.targetType === 'timeSeriesChart') {
    const {index, subId, time, value, x, y} = pointerEvent.subItem;
    alert([
      `Data point at x = ${x}, y = ${y}`,
      `   Time: ${time}`,
      `   Index: ${index}`,
      `   subID: ${subId}`,            
      `   Value: ${value}`,
    ].join('\n'));
  }
}
<Timeline 
entities={entities} 
events={events} 
onTimelineClick={handleClick} 
/>
copy code

The subId will return a string in the form chartId-index.

Data points don't have a width or height, so these parameters will return undefined.

See the Time Series Chart Tooltips story for an example.

AnnotationsBeta

KronoGraph lets you add annotations on events and entity ranges. This provides more context for presentations, investigative workflows or exported timelines. Annotations can also be used to create a collaborative tool between multiple people who share and analyze the same timeline.

Add annotations to your timeline using the annotations annotations API or allow users to make them as they explore the timeline (see Creating annotations).

This example uses a NYC Pizza Slices dataset and shows two annotations loaded into the timeline with the default settings.

Annotations refer to a subject, which can be events or entity ranges, and contain a text label. In the chart above the Great value at Luigi's annotation has an entity range as the subject and the Best tasting pepperoni annotation has two events as the subject. These are defined as follows:

 timeline.annotations({
  note1: { 
    subject: ['event36', 'event105'], // subjects are event IDs
    label: 'Best tasting pepperoni' 
  },
  note2: {
    subject: [ // subjects are entity IDs and a time range
      {
        id: `Luigi's Pizza`,
        range: { start: Date.UTC(2016, 0, 5), end: Date.UTC(2017, 2, 22) },
      },
    ],
    label: `Great value at Luigi's`,
  },
});
copy code
<Timeline
  annotations={
    note1: { 
      subject: ['event36', 'event105'], // subjects are event IDs
      label: 'Best tasting pepperoni' 
    },
    note2: {
      subject: [ // subjects are entity IDs and a time range
        {
          id: `Luigi's Pizza`,
          range: { start: Date.UTC(2016, 0, 5), end: Date.UTC(2017, 2, 22) },
        },
      ],
      label: `Great value at Luigi's`,
    },
  }
/>  
copy code

By default, the deleteAnnotation control is displayed. When clicked, it fires an annotations event.

By default, the deleteAnnotation control is displayed. When clicked, it fires an onTimelineChange event.

Annotations are located in their own layer on top of the timeline are only drawn when their subjects are visible. This means that they can optionally be included in an export to share offline with others.

Take a look at the Crime Investigation demo to see how annotations can be used.

Creating

Annotations can be created live in the timeline. Use the selection interaction to capture the subjects, and use them to add a new annotation to the annotations object.

 // Turn on selection mode
timeline.selectionMode(true);

timeline.on('drag-end', () => {
  const eventIds = timeline.selection();

  if (eventIds.length > 0){
    // Make one annotation pointing at all the selected events
    timeline.annotations({
      myAnnotation: {
        label: 'These events are interesting',
        subject: eventIds
      }
    });
    // Clear the selection
    timeline.selection([]);
    // Exit selection mode
    timeline.selectionMode(false);
  }
});
copy code
export const myComponent = () => {
  const [annotations, setAnnotations] = useState({});
  const [selection, setSelection] = useState([]);
  // Turn on selection mode
  const [selectionMode, setSelectionMode] = useState(true);
  function makeAnnotation(){
    // Make one annotation pointing at all the selected events
    const eventIds = selection;
      if (eventIds.length > 0){
        const myAnnotation = {
            label: 'These events are interesting',
            subject: eventIds
          }
        setAnnotations({myAnnotation});
      }
      // Clear the selection
      setSelection([]);
      // Exit selection mode
      setSelectionMode(false);
    }
  
  return (
    <Timeline
    entities={entities}
    events={events}
    onTimelineChange={event => {
      if (event.selection !== undefined) {
        setSelection(event.selection);
      }
    }}
    onTimelineDragEnd={makeAnnotation}
    annotations={annotations}
    selection={selection}
    selectionMode={selectionMode}
  />
  )
}
copy code

Try it out with the Creating Annotations story.

Positioning

By default, annotations appear between the timeline data and the scale in a rail at the top of the timeline. However, annotations can also be in the 'bottom' rail or 'free' relative to their event or entity range subjects.

Annotations can be repositioned in the timeline by clicking and dragging, or can be dragged into or out of the rails.

Here, the Best tasting pepperoni annotation position is set to 'bottom', and the Great value at Luigi's annotation has an angle of 'sw' and a distance of '100px'.

 timeline.annotations({
  note1: {
    subject: ['event36', 'event105'],
    label: 'Best tasting pepperoni',
    position: { angle: 'bottom' }, // the annotation is in the bottom rail
  },
  note2: {
    subject: [
      {
        id: `Luigi's Pizza`,
        range: { start: Date.UTC(2016, 0, 5), end: Date.UTC(2017, 2, 22) },
      },
    ],
    label: `Great value at Luigi's`,
    position: { angle: 'sw', distance: 100 }, // the annotation is set relative to the entity range
  },
});
copy code
<Timeline
  annotations={
    note1: { 
      subject: ['event36', 'event105'], 
      label: 'Best tasting pepperoni',
      position: { angle: 'bottom' }, // the annotation is in the bottom rail
    },
    note2: {
      subject: [
        {
          id: `Luigi's Pizza`,
          range: { start: Date.UTC(2016, 0, 5), end: Date.UTC(2017, 2, 22) },
        },
      ],
      label: `Great value at Luigi's`,
      position: { angle: 'sw', distance: 100 }, // the annotation is set relative to the entity range
    },
  }
/>  
copy code

Settings that affect the size of the rails and annotations can be found in Timeline Options.

Styling

The annotation is comprised of two different sections, both of which can be styled:

  • A body that shows the information about the subjects.
  • A connector that relates the annotation to the subjects.
An image showing the different sections of an annotation.

The connector is split into three parts which you can style using the connectorStyle property.

  • A line pointing to the subject, or multiple lines for multiple subjects.
  • An optional subject end set by the subjectEnd property.
  • An optional container around event or entity range subjects set by the container property.
 timeline.annotations({
  note1: {
    // annotation body style settings
    subject: ['event36', 'event105'],
    label: 'Best tasting pepperoni',
    position: { angle: 'w', distance: 100 },
    borderColor: 'orange',
    borderWidth: 2,
    connectorStyle: {
      // annotation connector style settings
      subjectEnd: 'dot',
      container: 'rectangle',
      color: 'orange',
      width: 2,
      lineStyle: 'dashed',
    },
  },
});
copy code
<Timeline
  annotations={
    // annotation body style settings
    subject: ['event36', 'event105'],
    label: 'Best tasting pepperoni',
    position: { angle: 'w', distance: 100 },
    borderColor: 'orange',
    borderWidth: 2,
    connectorStyle: {
      // annotation connector style settings
      subjectEnd: 'dot',
      container: 'rectangle',
      color: 'orange',
      width: 2,
      lineStyle: 'dashed',
    },
  }  
/>
copy code

You can choose to show the delete or edit controls using the deleteAnnotation and editAnnotation control options.

Try out some of the options with the Styling Annotations story.

Editing

If you choose to enable the edit control for annotations, you will also need to write application code to edit the label text for the appropriate annotation in the annotations object.

You will need to capture the click event for the edit control. The targetType is 'annotationEditButton' and annotationId property contains the id of the annotation to be edited:

function clickHandler ({ targetType, annotationId }) {
  if (targetType === 'annotationEditButton') {
    console.log(`Annotation to edit: `, annotationId);
  }
}
timeline.on('click', clickHandler);
copy code
const handleClick = ({ targetType, annotationId }) => {
  if (targetType === 'annotationEditButton') {
    console.log(`Annotation to edit: `, annotationId);
  }
}
<Timeline 
entities={entities} 
events={events} 
onTimelineClick={handleClick} 
/>
copy code

Additionally, you will need to create an HTML element to handle the editing, show it when the edit button is clicked, and then save the edited text.

This can be seen in the Creating Annotations story.

Markers

Markers allow you to flag a specific time or time range with a line that vertically crosses all the visible entities on the timeline.

Add markers to your timeline by using the timeline.markers API. You can add as many markers to your timeline as you like.

Add markers to your timeline by using the markers API. You can add as many markers to your timeline as you like.

By default, markers inherit their colors and text size from the scales options. However, markers can be individually styled by specifying color, fontIcon or labelColor and other options in the Marker object.

timeline.markers([
  {
    label: 'Incident',
    time: { start: new Date(2025, 7, 14, 9, 30), end: new Date(2025, 7, 14, 12, 30) },
  },
  {
    label: 'Resolution',
    time: new Date(2025, 7, 14, 13, 0),
    color: '#a3c096',
    labelColor: '#219ebc',
    fontIcon: { fontFamily: 'Font Awesome 5 Free', fontWeight: 900, text: '\u{f058}' },
    showAtBottom: false,
  },
]);
copy code
<Timeline 
  markers={[
    {          
      label: 'Incident', 
      time: {
          start: new Date(2025, 7, 14, 9, 30), 
          end: new Date(2025, 7, 14, 12, 30) 
        } 
    },
    {
      label: 'Resolution',
      time: new Date(2025, 7, 14, 13, 0),
      color: '#a3c096',
      labelColor: '#219ebc',
      fontIcon: { fontFamily: 'Font Awesome 5 Free', fontWeight: 900, text: '\u{f058}' },
      showAtBottom: false,
    }
  }]
  />
copy code

See more in the Markers story.

Font Icons

KronoGraph allows you to display font icons on entities, events, glyphs and markers.

Font icons on events are only displayed at zoom levels where the individual events in the timeline are not drawn too closely to one another.

You can set a font family on each individual icon with the fontFamily property. Use escaped character code values in the text property to set font icons:

const entities = {
  'Person 1': {
    fontIcon: {
      fontFamily: 'Font Awesome 5 Free',
      fontWeight: 900, // use the 'solid' font icon
      text: '\u{f1b9}', // car icon
    },
  },
};

const events = {
  'email 1': {
    entityIds: ['Person 1'],
    time: new Date(2025, 7, 14, 9, 27),
    fontIcon: {
      fontFamily: 'Font Awesome 5 Free',
      fontWeight: 900,
      text: '\u{f2b6}', // open envelope icon
    },
  },
};
copy code

See the Font Icons story for more details.

Loading Font Icons

Fonts must be loaded into the page before they can be used in KronoGraph.

To ensure that KronoGraph is able to render your font icons, wait for them to load before calling set(). passing your data to the Timeline component.

There are multiple ways to control font loading, including using dedicated libraries such as Web Font Loader .

Modern browsers also support the native fonts.load() function:

const fontAwesomeSolid = '900 16px "Font Awesome 5 Free"';
document.fonts.load(fontAwesomeSolid);

document.fonts.ready.then(() => {
  const timeline = createTimeline('my-timeline');
  timeline.set({ entities, events });
});
copy code
const loadFonts = () => {
  const fontAwesomeSolid = '900 16px "Font Awesome 5 Free"';
  document.fonts.load(fontAwesomeSolid);
};

export const MyComponent = () => {
  const [fontsReady, setFontsReady] = React.useState(false);

  loadFonts();
  document.fonts.ready.then(() => {
    setFontsReady(true);
  });

  if (!fontsReady) {
    return null;
  }

  return (
    <Timeline
      entities={entities}
      events={events}
    />
  );
};
copy code

Heatmap

To help with visual analysis of large datasets, KronoGraph offers the option to aggregate individual events into a heatmap. In the heatmap view, individual cells span periods of time in the timeline.

By default, the heatmap cell colors are based on the colors of underlying events and the cell color alpha values (i.e. transparency) represent the event count. Customize your heatmaps using Heatmap Values and Heatmap Colors.

The heatmap view shows when there are more than 100 events in the active range. You can change the default number of events with the heatmapThreshold option.

The heatmapDirection option allows you to control how the heatmap shows entity and event connections. Three options are provided: any (the default), from and to.

For example, here we see a number of entities and events.Events on the timeline.
With any selected the heatmap will show a cell for all underlying event directions.Heatmap direction any.
With from selected, the heatmap will show cells for entity rows that have events going from them.Heatmap direction from.
With to selected, the heatmap will show cells for entity rows that have events going to them.Heatmap direction to.

Additional options to style the heatmap include heatmapPadding and showLines. Both can be seen in the Styling the Timeline story.

For more examples, see the Heatmap section in our storybook.

Heatmap Values

You can set a custom value to be used by the heatmap instead of the event count to determine the color of the cells.

Specify this property with the heatmapValue option:

timeline.options({ 
  events: { 
    heatmapValue: 'amount', 
  } 
});
copy code
options{{ 
  events: { 
    heatmapValue: 'amount',
  } 
}}
copy code

The custom value is defined in the Event object:

const events = {
  event1: {
    entityIds: ['Person 1', 'Person 2'],
    time:  new Date(2025, 7, 14, 9, 27),
    data: {
      amount: 50,
    },
  },
};
copy code
const events = {
  event1: {
    entityIds: ['Person 1', 'Person 2'],
    time:  new Date(2025, 7, 14, 9, 27),
    data: {
      amount: 50,
    },
  },
};
copy code

Heatmap Colors

KronoGraph also provides heatmapColor options that allow you choose a color scheme for your heatmap. Select options to see how the heatmap is displayed.

Show:
Cell calculation:

The default setting uses the event color for the heatmap cells (or the entity color if the events don't have a color assigned). The alpha value is based on the number of events or Heatmap Values.

See the Heatmap Colors story for more examples for this option.

Reveal Events

You can use reveal() to keep events visible when switching to heatmap view or to show events in the context of the heatmap.

You can use reveal() to keep events visible when switching to heatmap view or to show events in the context of the heatmap.

timeline.reveal(['event2', 'event8']);
copy code
<Timeline
  reveal={['event2', 'event8']}
/>
copy code

In addition, you can use the revealHeatmapAlpha option to fade out entity rows that don't have any revealed events.

timeline.options({
  events: {
    revealHeatmapAlpha: 0.3
  }
});
copy code
<Timeline
  options={{
    events: {
      revealHeatmapAlpha: 0.3
    }
  }}
/>
copy code

Try it out in the Reveal story. Note that event folds are unavailable when using reveal.

Heatmap Cells

Information for heatmap cells is available via the subItem object. When the targetType is 'cell', the subItem object will be returned with the values for that cell.

timeline.on('click', (pointerEvent) => {
  if (pointerEvent.targetType === 'cell') {
    const {value, x, y, width, height} = pointerEvent.subItem;
    alert([
      `Heatmap cell at x = ${x}, y = ${y}`,
      `   Width: ${width}`,
      `   Height: ${height}`,            
      `   Value: ${value}`,
    ].join('\n'));
  }
});
copy code
const handleClick = (pointerEvent) => {
  if (pointerEvent.targetType === 'cell') {
    const {value, x, y, width, height} = pointerEvent.subItem;
    alert([
      `Heatmap cell at x = ${x}, y = ${y}`,
      `   Width: ${width}`,
      `   Height: ${height}`,            
      `   Value: ${value}`,
    ].join('\n'))
  }
}
<Timeline 
entities={entities} 
events={events} 
onTimelineClick={handleClick} 
/>      
copy code

The subItem parameters index and time are unused by the heatmap and return undefined.

See the Heatmap Tooltips story for ideas about how to use the subItem object.

Handling Events

KronoGraph lets you respond to different kinds of events fired by the timeline:

  • user interactions - such as when the user double-clicks a group
  • changing items in the timeline - such as when an entity is highlighted

To attach an event handler to an event, use the timeline.on() function. To detach it, use timeline.off(). pass your handler functions through to the event props.

When an event fires, KronoGraph passes a single object containing all the details of the event, such as which buttons or EventModifierKeys were active during the event, to the attached event handlers. Use destructuring to extract only the properties you need:

function clickHandler ({ id, button }) {
// the user clicked the left mouse button or tapped on screen
  if (button === 0) {
    console.log(id);
  }
}
timeline.on('click', clickHandler);
copy code
const handleClick = ({ id, button }) => {
// the user clicked the left mouse button or tapped on screen
  if (button === 0) {
    console.log(id);
  }
}
<Timeline 
entities={entities} 
events={events} 
onTimelineClick={handleClick} 
/>
copy code

To prevent default behavior of an event, call the preventDefault() function in the event handler:

function eventHandler({preventDefault}) {
  // handler code
  preventDefault();
}
copy code
const preventDoubleClick = event => {
  event.preventDefault();
};

<Timeline 
entities={entities} 
events={events} 
onTimelineDoubleClick = {preventDoubleClick}
/>
copy code

Some events, such as pointer-move or drag-move, fire continuously as the pointer moves. You can add a debouncing or throttling function to control this:

let timer;
function throttle(func, delay) {
  if (timer) return;
  timer = setTimeout(() => {
    func();
    timer = undefined;
  }, delay);
}
// throttle eventHandlerFunction to only run once every 100ms
timeline.on('pointer-move',() => {
  throttle(eventHandlerFunction, 100);
});
copy code

See a complete list of the available events events in the API Reference.

Check out the Events story to see events in action.

Sub-items

When a user interacts with some types of items on the timeline, an event will return a subItem object in its handler that contains more information about the sub-item.

if (targetType === 'timeSeriesChart' && subItem) {
  const { x, y, value, time } = subItem;
  console.log(x, y, value, time);
}
copy code

KronoGraph returns a sub-item object for Heatmap Cells and Data Points.

Handling Time

There are a number of formats to represent time, which means that each dataset you load into KronoGraph may need to be handled differently. You will need to convert your time into a Date object or a millisecond number. Standard JavaScript handles time down to millisecond resolution, which is one thousandth of a second.

 const date1 = new Date(2022, 11, 14); // year, month, day
 const date2 = Date.UTC(2022, 11, 14); // 1670976000000 milliseconds

KronoGraph will always return dates as Date objects. You can then convert them into the format that suits you best.

 const isoDate1 = date1.toISOString(); // 2022-12-14T00:00:00.000Z

Higher Resolution Time

For the majority of applications, milliseconds show sufficient time resolution to display your data. However, in some cases, milliseconds just aren't granular enough.

KronoGraph allows for the use of nanoseconds (1⁄1000000000 of a second, or 10−9 seconds) by accepting and returning an extra value in addition to standard date values to account for these smaller fractions of seconds.

Depending on how time is recorded in your data, it is possible to break date strings into a Date object and a number of nanoseconds, shown in the following example:

const str = "Sep 24, 2019 17:56:40.563420880";

// split the date string at the decimal point into a date and a fraction of a second
const [dateText, fractionText] = str.split(".");

// convert "Sep 24, 2019 17:56:40" to a Date object
const time = new Date(dateText);

// make sure that the nanosecond portion of the date string has nine decimal places
// by adding zeros if needed
const nanosecondsText = fractionText.padEnd(9, "0");

// convert the nanoseconds text string to a number
const nanoseconds = Number(nanosecondsText);
copy code

See the MDN docs for more information about manipulating strings.

You can then assign the values to the appropriate time properties, in this case to time and timeNanoseconds. Depending on the Scale Localization options you are using, the timeline could appear as below:

 const timelineData = {
  entities: {
    ent1: { label: 'Entity 1', },
    ent2: { label: 'Entity 2', },
  },
  events: {
    evt1: {
      entityIds: ['ent1', 'ent2'],
      time: new Date('Sep 24, 2019 17:56:40'),
      timeNanoseconds: 563420880,
      color: '#FF8838',
    },
  },
};
copy code

Take a look at the Higher Resolution Time story for an example.

Range Limits

If you use a timeNanoseconds value, KronoGraph will select a resolution to display the data depending on the values supplied. There are three levels of resolution available:

  • Milliseconds (10-3 seconds)
  • Microseconds (10-6 seconds)
  • Nanoseconds (10-9 seconds)

KronoGraph will use the resolution required, so long as the time range of your data does not exceed the range limits for that resolution. The range limits are as follows:

Resolution Range limits ~ Range
Milliseconds00:00 1 January 1970 UTC ± 100,000,000 days547,945 years
MicrosecondsA chosen midway point in the data ± 253 microseconds570 years
NanosecondsA chosen midway point in the data ± 253 nanoseconds208 days

If the time range exceeds these limits, KronoGraph will use the next best resolution. A warning message will appear in the developer console unless the showWarnings option is set to false.

Time Zones

Every geographical region has its own standard time zone. Each zone has a UTC offset, which is the difference in hours and minutes from Universal Coordinated Time (UTC). Many regions observe Daylight Saving Time (DST), where the offset changes during the seasons. Offset rules may also change for unpredictable political reasons.

JavaScript has no built-in functionality to manage multiple time zones, but it does have functionality to map between UTC and the user's local time zone, defined by the operating system.

The JavaScript Date constructor treats most input formats as local time. The exceptions are single dateStrings with a declared time zone and millisecond numbers, which use UTC. See the MDN Docs for more details.

If you are using timeNanoseconds, the value will remain the same regardless of the time zone.

Time Zones in KronoGraph

KronoGraph also has no built-in functionality to manage time zones, and will display dates exactly as they're passed in.

You will need to decide which time zone to use for KronoGraph events. The most common choices are either the user’s time zone or UTC.

User Time Conversions

Sometimes users want to see dates in their local time zone. If your server stores dates with time zones, simply pass the dates directly to the client:

// serverside
const storedDate = '2016-05-22T15:58:33.592Z';
// send storedDate directly to client

Then create a JavaScript Date object:

// clientside
const dateToUse = new Date(storedDate);
timeline.markers([{ label: 'dateInLocalTime', time: dateToUse }]);

KronoGraph will then display the date in the user’s time zone.

If your dates don’t have time zones, when JavaScript creates the date object it will assume the date is in local time already:

// clientside
// date string without a time zone
const zonelessDateString = 'Mon, 11 May 2015 15:00:00';
// date will be shown in the user’s time zone.
const dateInLocalTime = new Date(zonelessDateString);

KronoGraph will then display the date as 3pm in the user’s time zone.

Server-side Conversions

If users are used to seeing dates in UTC or another time zone, you may want to convert the date to that time zone.

If users want dates in a specific time zone, it's best to convert on the server before sending the data.

We recommend using a library such as Moment.js to assist with managing time zone conversions.

For a date on the server:

// serverside
const storedDate = '2016-05-22T07:58:33.592Z';

Convert this to local time on the server:

// serverside
const convertedDate = myCustomDateConverter(storedDate);
// send convertedDate to client

The data arrives in local time:

// clientside
// this assumes convertedDate is a string
const dateToUse = new Date(convertedDate.time);
timeline.markers([{ label: 'dateInConvertedTime', time: dateToUse }]);

Passing this value to KronoGraph will display dates in the converted time.

Millisecond Conversions

KronoGraph interprets numbers as time in milliseconds since January 1, 1970, 00:00:00 UTC, and displays them in UTC. This means they are shown as the same time for users in all time zones.

If you construct a date object with a millisecond number JavaScript will convert this to local time and you will get a different date than if you use the millisecond value itself:

// ms number is UTC
const milliseconds = 1431356400000;
const event1time =  milliseconds;
// Date object is local time
const event2time = new Date(milliseconds);
copy code

Dates returned by the timeline API are in local time. You can convert these back to milliseconds:

const returnedDate = new Date(2023, 0, 1, 14, 30); // local time

// convert to UTC milliseconds
const ms = Date.UTC(
returnedDate.getFullYear(), 
returnedDate.getMonth(),
returnedDate.getDate(),
returnedDate.getHours(),
returnedDate.getMinutes(),
returnedDate.getSeconds(),
returnedDate.getMilliseconds(),
);
// 1672583400000
copy code

Scales

Scale Localization

Dates and times are displayed in the scale in US English format by default. You can use the dateOrder and twelveHourClock options for basic adjustment of the US English format.

For further customization of the displayed date and time you can use:

  • dateTimeFormats for different scale zoom levels and wrapping options. Each one is a string containing formatting codes and/or literal characters.
  • dateTimeNames for months and days of the week, as well as the AM/PM symbols.
timeline.options({
  scales: {
    dateTimeFormats: {
      date: 'yyyy-MM-dd',
    },
  },
});
copy code
dateTimeFormats codes dateTimeFormats default settings

You can see some examples of date and time formatting for different locales in the Scales Options story.

Scale Wrapping

By default, time flows along the timeline component in a linear way, showing events in the sequence that they occurred.

KronoGraph offers an alternative view of time using scale wrapping, which positions events based on part of their timestamp, such as the hour of the day or the day of the week. This lets you visualize repeating patterns within the data, making it clearer how activity is distributed. This is also sometimes called "pattern of life" analysis.

You can change the timeline's scale wrapping by setting the wrapping option, for example:

options={{ 
  scales: {
    wrapping: 'week'
  } 
}}
copy code
timeline.options({
  scales: {
    wrapping: 'week'
  }
});
copy code

There are four scale wrapping options available in KronoGraph:

  • day - the scale is one day long, and events are positioned by the hour of day that they occurred.
  • week - the scale is one week long, and events are positioned by the time since the start of the week.
  • month - the scale is 31 days long, and events are positioned based on their time within the month.
  • year - the scale is one year long, and events are positioned by their time since the start of the year.

When the scale is wrapped, its labels show times within the wrapped interval, instead of specific dates and times. You can still zoom in to see details of events in a particular part of the scale. The timeline.fit() function will fit the view to the whole wrapped interval, instead of the range of event times. The fit() instance method will fit the view to the whole wrapped interval, instead of the range of event times.

Internally, scale wrapping uses a "base range" of times for each wrapping option, which it maps event times to. If you call timeline.range() when the scale is wrapped, the times in the returned range are relative to this base range. If an onTimelineChange event containing a range object is invoked, the times in the event's range object are relative to this base range.

The base ranges for each wrapping option are:

WrappingStartEnd
day12 am on Jan 1, 200012 am on Jan 2, 2000
week12 am on Monday, Jan 3, 200012 am on Monday, Jan 10, 2000
month12 am on Jan 1, 200012 am on Feb 1, 2000
year12 am on Jan 1, 200012 am on Jan 1, 2001

So for example if the scale is using week wrapping and you zoom in to Tuesday, timeline.range() will return a result with a start date of 12 am, January 4, 2000, and an end date of 12 am, January 5, 2000. Similarly, if you call timeline.range() as a setter, then the dates you provide should be relative to the base range.

Timestamps passed to event handlers for events such as click or hover are also relative to the base range.

Check out the Scale Wrapping story to see an example of scale wrapping in action.

Note that if scale wrapping is applied, time series charts will be hidden.

So for example if the scale is using week wrapping and you zoom in to Tuesday, the TimeRange object will return a result with a start date of 12 am, January 4, 2000, and an end date of 12 am, January 5, 2000. Similarly, if you are passing value to the range prop, the dates you provide should be relative to the base range.

Timestamps passed to event handlers for events such as onTimelineClick or onTimelineHover are also relative to the base range.

Check out the Scale Wrapping story to see an example of scale wrapping in action.

Note that if scale wrapping is applied, time series charts will be hidden.

Scale Mode

By default, KronoGraph spaces events along the timeline in relation to a linear scale. However, some datasets might have lots of events clustered closely together, followed by periods of time where no events occur. In these instances, it can be useful to change the scaleMode to nonlinear to space the events proportionally, reduce the amount of empty space and allow for a larger time range to be seen at once.

The following timeline shows the dates of notable inventions and uses the nonlinear scale to stretch and compress time where needed, allowing more inventions to be shown in the same view. For example, there are more than 1500 years between the invention of paper and the printing press, but the nonlinear scale mode compresses the gap to make better use of the space in the timeline. Hover over the timeline and use the scale guide to see the year of each invention.

timeline.options({
  scales: { 
      scaleMode: 'nonlinear' 
  }
});
copy code
<Timeline
  options={{
    scales: { 
      scaleMode: 'nonlinear' 
    }
  }}
/>
copy code

By default, when an entity is focused the timeline is rescaled for visible events. Disable this behavior by setting the rescale option to 'false'.

See the Nonlinear Scale Mode story to try it out.

The scale is made up of an inner and outer scale. The outer scale gives context while the inner scale provides a more accurate indication of the date and time. Each vertical tick mark is drawn at a regular time interval, which shows how much time is compressed or expanded on the scale.

Screenshot of inner and outer scale.

Customize the display of the scale with dateOrder and twelveHourClock. Currently, dateTimeFormats and dateTimeNames aren't supported using the nonlinear scale.

Note that when the scale mode is set to 'nonlinear', time series charts will be hidden.

Handling Data

In order to display your data in KronoGraph, you need to transform it into JSON format.

This example shows how to convert a CSV file to the correct JSON format, but a similar method can be applied whatever the format of your original data.

In our example, we're using mini-dish.csv, delivered in the downloaded KG package. It's a subset of data from the now-retired "What's on the menu?" project, originally hosted by the New York Public Library.

The code below uses the NPM packages csvtojson and fs-extra to help transform and save the appropriate file.

const csvtojson = require('csvtojson');
const fs = require('fs-extra');

async function transformData() {
  // read the csv file
  const csvData = await csvtojson().fromFile('mini-dish.csv'); 
  // create an empty object for events
  const events = {}; 
  // create an empty object for entities
  const entities = {}; 

  // iterate through the csv data and assign the 
  // values in the file to the appropriate API
  csvData.forEach(({ name, first_appeared, last_appeared, highest_price }, index) => {
    // add data to the event object
    events[`event${index}`] = {
      entityIds: [name],
      time: {
        start: Date.parse(first_appeared),
        end: Date.parse(last_appeared),
      },
    };

    // calculate the color of the entity based on the highest_price value
    let color;
    if (highest_price === '0') {
      color = '#ff9147';
    } else if (highest_price > '0' && highest_price <= '0.5') {
      color = '#f7d06e';
    } else {
      color = '#e12d39';
    }
    // add data to the entity object
    entities[name] = {
      color,
    };
  });
  // create a data object containing the events and entities objects
  const data = { events, entities }; 
  // write the JSON file
  await fs.writeJSON('dishes-data.json', data); 
}

transformData();
copy code

This is the result of the transformation - a JSON file.

Once the JSON file is correctly formatted it can be used to create a timeline:

 import { createTimeline } from 'kronograph';
 const data = require('dishes-data.json');

 const timeline = createTimeline('handlingDataExample');

 timeline.set(data);
 timeline.fit();
copy code

The timeline would look like this:

State Management

On every render, KronoGraph must decide whether any of its props have changed and what to update accordingly. The timeline assumes that whenever a new object is passed into a prop, something has changed and KronoGraph will redraw. If the same object is passed into a prop, it assumes nothing has changed.

KronoGraph must decide if any data has changed every time the API is called and make the respective updates. The Timeline assumes that whenever a new object is passed into a property, something has changed and KronoGraph will redraw. If the same object is passed into set(), it assumes nothing has changed, even if the object itself has been altered.

By relying on referential equality to detect changes, KronoGraph can quickly and efficiently respond to re-renders, even in very large data sets.

This means that applications must be disciplined in how state is updated to prevent unnecessary redraws being called. For example, passing a new object with identical property values into the entities prop property will trigger the data to be reloaded and redrawn.

This holds true across the state tree: if a sub property has changed, a whole new object hierarchy must be created and passed into the relevant prop. property. Consider the following object in the entities prop: property:

const entities = {
  'smith-j' : {
    label: 'John Smith',
    fontIcon: {
      text: '☺',
    },
  },
};

To change John Smith's fontIcon.text property, we can't simply mutate the first object and pass the same into the entities prop, property, as the change will not be recognized and our timeline will stay the same.

To correctly render this change, we have to create new objects for entities, smith-j (i.e. entities['smith-j']), and fontIcon. We also need to create a new parent object to pass to set(). In the object below, we've starred the properties that need to be re-created:

const entities* = {
  'smith-j'* : {
    label: 'John Smith',
    fontIcon*: {
      text: '☺',
    },
  },
};

In a real app, we would have something like:

// Clone the entities object before making any changes
const newEntities = Object.assign({}, this.state.entities);

// Clone and update the entity we want to change
const newEntity = Object.assign({}, newEntities['smith-j'], {
  fontIcon: {
    ...newEntities['smith-j'].fontIcon,
    text: '$',
  }
});

// Write the change back to the newEntities object
newEntities['smith-j'] = newEntity;

// Update app state and trigger a re-render
this.setState({ entities: newEntities });
copy code
// In this example, we assume that the app has a 'let currentData' 
// variable containing an object with properties 
// 'entities', 'events', 'entityTypes' and 'eventTypes'.

// Clone the entities object before making any changes
const newEntities = Object.assign({}, currentData.entities);

// Clone and update the entity we want to change
const newEntity = Object.assign({}, newEntities['smith-j'], {
  fontIcon: {
    ...newEntities['smith-j'].fontIcon,
    text: '$',
  }
});

// Write the change back to newEntities
newEntities['smith-j'] = newEntity;

// Update the app's currentData object with the changed
// entities object and pass the new object to the timeline.
currentData = { ...currentData, entities: newEntities }
timeline.set(currentData);
copy code

For an example of passing state changes in and out of the Timeline component, see the Change Event story.

Using immutability in your app's code provides better performance, fewer unexpected side-effects, and allows you to rewind or undo user actions.

There are many code patterns and functions designed for immutability, including:

  • Object.assign()
  • The spread operator const events = {...this.state.events}; const events = { ...state.events };
  • Array operators like map and filter
  • Utility libraries like lodash

We use these patterns throughout our stories.

Updating Data

On every render, KronoGraph must decide whether any of its props have changed and what to update accordingly. The timeline assumes that whenever a new object is passed into a prop, something has changed and KronoGraph will redraw. If the same object is passed into a prop, it assumes nothing has changed.

KronoGraph must decide if any data has changed every time the API is called and make the respective updates. The Timeline assumes that whenever a new object is passed into a property, something has changed and KronoGraph will redraw. If the same object is passed into set(), it assumes nothing has changed, even if the object itself has been altered.

By relying on referential equality to detect changes, KronoGraph can quickly and efficiently respond to re-renders, even in very large data sets.

This means that applications must be disciplined in how state is updated to prevent unnecessary redraws being called. For example, passing a new object with identical property values into the entities prop property will trigger the data to be reloaded and redrawn.

This holds true across the state tree: if a sub property has changed, a whole new object hierarchy must be created and passed into the relevant prop. property. Consider the following object in the entities prop: property:

const entities = {
  'smith-j' : {
    label: 'John Smith',
    fontIcon: {
      text: '☺',
    },
  },
};

To change John Smith's fontIcon.text property, we can't simply mutate the first object and pass the same into the entities prop, property, as the change will not be recognized and our timeline will stay the same.

To correctly render this change, we have to create new objects for entities, smith-j (i.e. entities['smith-j']), and fontIcon. We also need to create a new parent object to pass to set(). In the object below, we've starred the properties that need to be re-created:

const entities* = {
  'smith-j'* : {
    label: 'John Smith',
    fontIcon*: {
      text: '☺',
    },
  },
};

In a real app, we would have something like:

// Clone the entities object before making any changes
const newEntities = Object.assign({}, this.state.entities);

// Clone and update the entity we want to change
const newEntity = Object.assign({}, newEntities['smith-j'], {
  fontIcon: {
    ...newEntities['smith-j'].fontIcon,
    text: '$',
  }
});

// Write the change back to the newEntities object
newEntities['smith-j'] = newEntity;

// Update app state and trigger a re-render
this.setState({ entities: newEntities });
copy code
// In this example, we assume that the app has a 'let currentData' 
// variable containing an object with properties 
// 'entities', 'events', 'entityTypes' and 'eventTypes'.

// Clone the entities object before making any changes
const newEntities = Object.assign({}, currentData.entities);

// Clone and update the entity we want to change
const newEntity = Object.assign({}, newEntities['smith-j'], {
  fontIcon: {
    ...newEntities['smith-j'].fontIcon,
    text: '$',
  }
});

// Write the change back to newEntities
newEntities['smith-j'] = newEntity;

// Update the app's currentData object with the changed
// entities object and pass the new object to the timeline.
currentData = { ...currentData, entities: newEntities }
timeline.set(currentData);
copy code

For an example of passing state changes in and out of the Timeline component, see the Change Event story.

Using immutability in your app's code provides better performance, fewer unexpected side-effects, and allows you to rewind or undo user actions.

There are many code patterns and functions designed for immutability, including:

  • Object.assign()
  • The spread operator const events = {...this.state.events}; const events = { ...state.events };
  • Array operators like map and filter
  • Utility libraries like lodash

We use these patterns throughout our stories.

Instance Methods

Additional methods are available on timeline objects when you have a reference to an instance of a timeline. Refs provide a way to access React elements created in the render method. For example, when you have a ref to a timeline, you can call the fit method which will adjust the view to make all timeline items visible. See the API Reference for more details.

Creating a ref in a stateful component

This example uses the React.createRef() API, introduced in React 16.3. If you are using an earlier release of React we recommend using callback refs instead.

class MyStatefulComponent extends React.PureComponent {
  constructor(props) {
    super(props);
    this.timeline = React.createRef();
  }

  fitToView = () => {
    this.timeline.current.fit();
  };

  render = () => {
    const { items } = this.props;
    return (
      <>
        <Timeline ref={this.timeline} items={items} />
        <button onClick={this.fitToView}>
          Show all items
        </button>
      </>
    );
  };
}

Creating a ref in a functional component

If you are using React 16.8 or above then you have access to the hooks API and can use the useRef() hook in your functional components.

const MyFunctionalComponent = (props) => {
  const { items } = props;
  const timelineRef = React.useRef(null);

  const fitToView = () => {
    timelineRef.current.fit();
  };

  return (
    <>
      <Timeline ref={timelineRef} items={items} />
      <button onClick={fitToView}>
        Show all items
      </button>
    </>
  );
};

Export

You can export a static png image of your current timeline view using the export function.

import { createTimeline } from 'kronograph';
import { data } from './data';

const timeline = createTimeline('my-timeline');
timeline.set(data);
timeline.fit();
timeline.export({ type: 'png' }).then(({url}) => {
  window.open(url);
});
copy code

You can export a static png image of your current timeline view using the export instance method.

import React, {useEffect, useRef} from 'react';
import Timeline from 'kronograph/react/Timeline';
import { entities, events, markers } from './data';

export const Demo = () => {
  const timelineRef = useRef(null);
  
  useEffect(() => {
    timelineRef.current.export({type: 'png'}).then(({url}) => {
      window.open(url);
    })
  });
  
  return <Timeline 
            ref={timelineRef} 
            events={events} 
            entities={entities} 
            markers={markers} 
          />;
};
copy code

Try exporting a timeline in our Export story.

Architecture

Security

KronoGraph is a low-risk, highly secure JavaScript library that is unlikely to be affected by common security vulnerabilities:

  • No native data transfers with remote servers or server-side dependencies.
  • No user data tracking or persistent data on local storage.
  • Runs entirely within the browser using standard JavaScript. It has no plug-in or extension requirements.
  • Source code is obfuscated and minified before distribution.
  • Nothing is added to the global scope, protecting its functions.
  • KronoGraph does not pollute prototypes.
  • New APIs and features are tested to prevent introduction of vulnerabilities.

KronoGraph Development

KronoGraph is a closed source project, with all code controlled by Cambridge Intelligence staff, reviewed by multiple expert developers and tested thoroughly by our experienced QA team.

KronoGraph source code is developed in TypeScript and built using automated tools including a linter (ESlint ) and a suite of security scanners, including:

  • Secret scanner - scans the source code for accidental exposure of sensitive security information
  • Container scanner - scans the webserver container for vulnerabilities
  • Dependency scanner - scans our internal and build-time dependencies for known issues and vulnerabilities
  • Static application security testing (SAST) - scans the source code for vulnerabilities, encryption issues and other potentially exploitable holes

If we identify a vulnerability, we review it internally and deal with it before release.

There is no accepted standard scanner for malicious JavaScript code. Our JavaScript files are built using secure processes and hosted on secure web servers. We will never add malicious behaviors to our source code, and we are confident that third parties cannot hijack or compromise our downloads.

Dependencies

The KronoGraph Timeline component requires React . You can install it using yarn:

npm install --save react@^19.0.0
copy code

KronoGraph supports React versions ^16.8.0, ^17.0.0, ^18.0.0 and ^19.0.0.

TypeScript

KronoGraph includes full type definitions for TypeScript compatibility. See kronograph.d.tsTimeline.d.ts for the definitions. Types should be automatically available when you import KronoGraph.

Accessibility

Designing accessible products means they can be used effectively by as many people as possible, providing an inclusive experience for people with impairments and different needs.

Accessible design improves the product for everyone, as well as creating a better user experience for users with disabilities. In addition, most countries support accessibility with laws and regulations on both public and private sector organizations.

There is no single universal standard, so we recommend that you always check what policies are relevant for you. However, the W3C's Web Content Accessibility Guidelines 2.1 are most often referenced as an acceptable standard.

Compliance with Regulations

KronoGraph is not a complete application by itself, it is a toolkit that you integrate into your own product. This means that accessibility should be considered at the level of the end product to give your application users the best user experience possible.

KronoGraph as a visual medium is often exempt from compliance requirements because the functionality cannot always be fully reproduced in an accessible way. To help you with your accessibility audits, we have prepared a table that summarizes the guidelines of WCAG 2.1 and how they relate to KronoGraph:

WCAG 2.1 Guidelines Comparison

We are always working on implementing features that make KronoGraph more accessible and easier to use. Some tips using various KronoGraph features are listed below.

Keyboard Controls

KronoGraph doesn't have any native keyboard navigation behavior, which means that you can customize keyboard controls without having to override any default actions. To help you do this, KronoGraph offers a number of native events that you can respond to. It also supports standard element events such as key-down, focus and others.

KronoGraph doesn't have any native keyboard navigation behavior, which means that you can customize keyboard controls without having to override any default actions. To help you do this, KronoGraph offers a number of native events that you can respond to. It also supports standard element events such as onTimelineKeyDown and others.

This example shows how to customize keyboard navigation behavior by calling pan() and zoom() in a handler triggered by a key-down event listener:

This example shows how to customize keyboard navigation behavior by calling the pan() and zoom() instance methods in a handler triggered by a onTimelineKeyDown event listener:

 timeline.on('key-down', keydownHandler);

 function keydownHandler(e) {
   if (e.keyCode === 37) {
     timeline.pan('left');
   } else if (e.keyCode === 39) {
     timeline.pan('right');
   } else if (e.keyCode === 73) {
     timeline.zoom('in');
   } else if (e.keyCode === 79) {
     timeline.zoom('out');
   }
 }
copy code
 const timelineRef = useRef(null);
 const handleKeyDown = (e) => {
   if (e.keyCode === 37) {
     timelineRef.current.pan('left');
   } else if (e.keyCode === 39) {
     timelineRef.current.pan('right');
   } else if (e.keyCode === 73) {
     timelineRef.current.zoom('in');
   } else if (e.keyCode === 79) {
     timelineRef.current.zoom('out');
   }
 }
 <Timeline 
 entities={entities} 
 events={events} 
 onTimelineKeyDown={handleKeyDown} 
 />

Some browsers support some keyboard navigation by default, e.g. using the Tab key to iterate through form controls. When planning keyboard shortcuts, you should avoid overriding native browser and operating system shortcuts that affect the browser behavior.

Accessible Styling

Text

We recommend using simple sans-serif fonts as they are generally easier to read for most users. Alongside text, you should also convey the meaning in another way that doesn't require reading or understanding the language, such as font icons or glyphs.

Color

The colors you use should always have a sufficient contrast ratio. Online tools such as this WebAIM Contrast checker can help you check your color combinations for accessibility. Special attention should be paid to contrast if users can switch between dark and light modes.

Show meaning in multiple ways

You should not rely on a single method, such as color, to convey a meaning. Add as many markers for context or tooltips for extra information as needed. On entities you can use glyphs and font icons, while on events you can use font icons.

Arrange Entities

Entities can be arranged into types and groups to help users understand how they are related to each other.

ARIA Support

ARIA (Accessible Rich Internet Applications), is a set of roles and attributes that can supplement HTML elements or attributes that don't have built-in accessible semantics or behavior.

KronoGraph lets you set ARIA properties, such as aria-label, on the DOM element.

 const labelAria = document.getElementById('KronoGraph-timeline');
 const entityCount = Object.keys(timelineData.entities).length;
 if (labelAria) {
   labelAria.setAttribute(
     'aria-label',
     `KronoGraph timeline showing ${entityCount} entities.`,
   );
 }  
copy code

Audio

Seeing the timeline is essential for spotting and understanding the patterns and connections revealed in the data, which makes it challenging to make timelines accessible e.g. for screen readers. As an alternative, you can hide the timeline's div from screen readers using the aria-hidden property, and instead supply a text in the aria-description that summarizes the timeline to the extent needed in your context, for example:

The element here is a timeline made of entities and events, where entities represent employees and events represent their communications.

You can also use audio as an additional channel to written information or as an extra method of alerting or highlighting, but remember to never rely on audio alone. Click on an entity or an event to hear the name or time described.

 function clickHandler(event) {
  const msg = new SpeechSynthesisUtterance();
  if (event.targetType === 'entity') {
    msg.text = event.label;
  } else if (event.targetType === 'event') {
    msg.text = event.time;
  }
  if (msg) {
    window.speechSynthesis.speak(msg);
  }
}

const timeline = createTimeline('accessibilityAudio');
timeline.set(timelineData);
timeline.fit();
timeline.markers([{ label: 'Company announcement.', time: new Date(2025, 7, 14, 8, 56, 2) }]);
timeline.options({ scales: { showAtBottom: false } });
timeline.on('click', clickHandler);      
copy code
 const clickHandler = ({targetType, label, time}) => {
  const msg = new SpeechSynthesisUtterance();
  if (targetType === 'entity') {
    msg.text = label;
  } else if (targetType === 'event') {
    msg.text = time;
  }
  if (msg) {
    window.speechSynthesis.speak(msg);
  }
}

<Timeline 
  entities={entities} 
  events={events} 
  markers={ [{ label: 'Company announcement.', time: new Date(2025, 7, 14, 8, 56, 2) }]}
  options={{scales: {showAtBottom: false}}}
  onTimelineClick={clickHandler} 
/>    
copy code

Migration

2.0 Migration Guide

KronoGraph 2.0 changes the way data is displayed and how users interact with it. This guide gives an overview of these changes to help you upgrade and make the most of the new features.

The key changes are:

  • KronoGraph uses a new lens to visualize and examine large datasets.
  • The concept of closed groups and any related API are removed.

There are no breaking changes to the KronoGraph data format, so you should be able to load your data into KronoGraph 2.0 without issues.

However, your data may look very different, especially if you have more entities than can fit on the screen at one time. You may want to think about how to communicate this to your end users.

Introducing lens

In earlier versions, when the timeline contained more entities than could fit on the screen, KronoGraph collapsed entities into 'runs', or, if you've defined types or groups in your data, 'closed groups'. Both were opened using the expand control.

Our users suggested that closed groups could sometimes open and close unpredictably when navigating the timeline, and that the design of narrow bands of data with lots of white space above and below could be improved.

KronoGraph 2.0 draws all entities and their events, even if there isn't enough space to display each entity label. This results in an immediate visual impact from a dataset, with heatmaps that convey all your data from the first glance.

If there are too many rows to label, instead of groups users can now open and close the lens. The lens enlarges a small set of entities in the timeline center and lets you scroll up and down through the whole dataset.

Migration tips

For any help implementing the changes, please contact support.

Below is a list of suggestions to help you migrate:

If you don't have types or groups defined in your data, consider adding them to visually split up large datasets and to label the split data when there isn't enough space to label individual entities.

New lens event returns the id of the entity at the center of the lens. Use the event to track the data that is currently in view.

New lens property returned with the onTimelineChange object provides the id of the entity at the center of the lens. Use the property to track the data that is currently in view.

New 'lensScrollbar' and 'lensControl' targetType values are returned from pointer events when the user interacts with the lens scroll bar or the lens control. Check your event handler code to ensure the behavior is as you expect during the user interaction.

New 'lensScrollBar' dragType is returned from pointer events when the user interacts with the lens scroll bar. Check your event handler code to ensure the behavior is as you expect during the user interaction.

A new label property is added to the object passed to pointer event handlers which helps you determine if an entity is drawn large enough for labels and interactions, or if it's too small to be shown fully. For example, if you're adding tooltips or annotations, you may want to only display them against entities that have a label.

Although we strongly recommend using the new lens feature, you can still recreate 'closed groups' of entities by manipulating your data.

To force a group of entities into a single row, you can replace their ids in the entityIds property of events with a new entity id representing a summary row and add this new entity to your entity list.

Removed APIs

All APIs referencing open and closed groups have been removed. If you have any code which references them, you'll need to remove or modify it:

  • expand event
  • expandedRowId
  • expand option of controls
  • targetType values: 'collapse', 'expand', 'entityGroupMore', 'entityGroupRun'

In addition, the priority option of entityType remains in the API for file format compatibility but it doesn't have any effect.

Other API changes

We've removed support for the focus action on groups, based on feedback that users were unsure what focusing a group meant or how to undo it. We recommend using filtering instead - if you want the user to see only the events associated with a certain group, filter other events out of your data. You can also programmatically focus all the entities in a group using the focus API. You can also programmatically focus all the entities in a group using the focus API.

The pointer-move event now fires when the timeline moves beneath the pointer, as well as when the pointer moves over the timeline. This synchronizes your tooltips and annotations with moving timeline. No migration is required, but you can check that your tooltips and annotations behave as expected.

The onTimeLinePointerMove event is now invoked when the timeline moves beneath the pointer, as well as when the pointer moves over the timeline. This synchronizes your tooltips and annotations with moving timeline. No migration is required, but you can check that your tooltips and annotations behave as expected.

We've changed some default settings to reduce visual clutter and make our event folds feature for simultaneous events more discoverable:

APIDefault before 2.0KronoGraph 2.0 default
color'inverse''entity'
heatmapPaddingfalsetrue
heatmapThreshold250100
showCirclestruefalse
showEventFoldsfalsetrue
showLines'always''individualEvents'
standardRowHeight1824

Obfuscation Requirements

KronoGraph source code must be obscured within your application. To prevent it being accessed or reused by third parties you need to:

  • Exclude the words KronoGraph or Cambridge Intelligence from any folder or path names.
  • Combine the KronoGraph files with the source code from your application, so that KronoGraph is not available as a separate file. We recommend using Terser with either Webpack, Parcel or Rollup (any tool which achieves the results listed is acceptable).
  • Remove the copyright notice (this is usually a minification step).
  • Ensure you are not deploying source maps of your source code to your end users.
  • Fully integrate KronoGraph into your product. We don't allow direct exposure of the KronoGraph API to end users via your own API ‘wrapper’.

Before deploying your code, check that:

  • The words KronoGraph or Cambridge Intelligence aren’t in any folder or path names.
  • The final file containing KronoGraph is at least 20% larger than the original kronograph.js file.
  • There are no Cambridge Intelligence copyright notices in your deployed code bundles.
  • You ensure compliance with the terms of any applicable Open Source Software ("OSS") licenses, which may include obligations such as replicating copyright notices or attribution requirements. For more details on OSS usage and licensing, please refer to the documentation provided with the relevant OSS.

About KronoGraph

KronoGraph is always in development, so we add, improve and remove features based on the needs of our products and customers.

Some features might first be introduced as alpha or beta before they become a stable part of KronoGraph. However, any new feature will be fully tested.

Sometimes we need to deprecate APIs and make breaking changes.

Alpha Features

Alpha features will be marked in the API using an Alpha tag.

  • This is a developmental stage, so we might introduce breaking changes to the API. These will always be announced in the Release Notes.
  • You shouldn't use alpha features in production.
  • We don't provide customer support for alpha features used in a production environment, but we do support alpha features in a development environment.

If you need any help with an alpha feature please contact support.

Beta Features

Beta features will be marked in the API using a beta tag.

  • Some features are introduced as beta from the start, while other alpha features move to beta after further development based on user feedback.
  • This is still a developmental stage, so we might introduce breaking changes to the API. These will always be announced in the Release Notes.
  • You can deploy beta features to production, but note that breaking changes will affect your production upgrades.

Once we've made all intended improvements to beta features, they move out of beta and become a stable part of KronoGraph.

If you need any help with a beta feature please contact support.

Deprecated Features

Deprecated features will be marked in the API using a deprecated tag.

  • You should stop using deprecated features and update your code as soon as possible.
  • All deprecated features will be announced in the Release Notes along with code change suggestions if necessary.
  • We will keep deprecated features available for your use until the next major release.

If you need any help with a deprecated feature please contact support.

Breaking Changes

Breaking changes could be an API update, a replacement feature or a visual change. They can cause your application to stop working.

  • Breaking changes will always be announced in the Release Notes.
  • Most breaking changes happen in a major release, however some breaking changes might be introduced in minor releases.
  • Check your code to see if it is affected by the change and make any changes necessary.

If you need any help with a breaking change please contact support.

About this Website

This website is built using the following libraries and assets:

Babel MIT
date-fns MIT
Esri Leaflet Apache 2.0
Font Awesome Font SIL OFL 1.1
Font Awesome Icons CC BY 4.0
Fuse.js Apache 2.0
Icofont Font SIL OFL 1.1
Leaflet BSD 2-Clause
Litepicker MIT
Lodash MIT
lz-string MIT
Material Design Icons Font SIL OFL 1.1
Material UI MIT
Monaco Editor MIT
Muli Font SIL OFL 1.1
Raleway Font SIL OFL 1.1
React MIT
React DOM MIT
react-split-pane MIT
Rollup MIT
Storybook MIT
Web Font Loader Apache 2.0
WHATWG fetch polyfill MIT

Please note that KronoGraph itself only has one dependency: React. This is only required when using the React version of KronoGraph.

We also make use of the following data sets in our examples:

  • Phone calls demo: A. McDiarmid et al. CRAWDAD. 10.15783/C7HP4Q
  • Pattern of life demo: Nurek, Mateusz, and Radosław Michalski. "Combining Machine Learning and Social Network Analysis to Reveal the Organizational Structures." Applied Sciences 10, no. 5 (2020): 1699, DOI 10.3390/app10051699
  • Crime investigation demo: https://www.independent.co.uk/news/uk/crime/salisbury-timeline-novichok-attack-suspects-b1923902.html

The social media demo makes use of the following images: