Search

Time Bar

Integrations > KeyLines and ReGraph

Sync a KronoGraph timeline with a time bar.

Time Bar
View live example →

Pan or zoom the timeline or drag the time bar sliders to adjust the visible time range and filter the chart.

This story shows a simple integration between the KronoGraph timeline and a chart and time bar provided by KeyLines.

See also:

  • range API
  • <a href='#' onclick="parent.toShowcasePlayground('integration', true);">Integration Playground</a>

Pan or zoom the timeline or drag the time bar sliders to adjust the visible time range and filter the chart.

This story shows a simple integration between the KronoGraph timeline and a chart and time bar provided by ReGraph.

See also:

  • range API
  • <a href='#' onclick="parent.toShowcasePlayground('integration', true);">Integration Playground</a>
import { createTimeline } from "kronograph";
import KeyLines from "keylines";
import { chartData, chartOptions, timelineData, timelineOptions, timebarOptions } from "./data";
import { isEmpty } from "lodash";

let ignoreTimebarEvents = false;

function setupTimeline() {
  const timeline = createTimeline({ container: "timeline-container", options: timelineOptions });
  timeline.set(timelineData);
  timeline.fit();
  return timeline;
}

async function setupTimebar() {
  const timebar = await KeyLines.create({
    container: "timebar-container",
    type: "timebar",
    options: timebarOptions,
  });
  await timebar.load(chartData);
  await timebar.zoom("fit");
  return timebar;
}

async function setupChart() {
  const chart = await KeyLines.create({
    container: "chart-container",
    type: "chart",
    options: chartOptions,
  });
  await chart.load(chartData);
  await chart.layout();
  await chart.zoom("fit");
  return chart;
}

async function main() {
  const timeline = setupTimeline();
  const chart = await setupChart();
  const timebar = await setupTimebar();

  async function filterChart() {
    const { entities: visibleEntityIds } = timeline.getInRangeItems();
    const filterOptions = { type: "node", animate: false };
    const { shown, hidden } = await chart.filter(({ id }) => visibleEntityIds[id], filterOptions);
    if (!isEmpty(shown.nodes) || !isEmpty(hidden.nodes)) {
      await chart.layout("organic");
    }
  }

  chart.on("selection-change", () => {
    timeline.focus(chart.selection());
    filterChart();
  });

  // prevent selection of links
  chart.on("click", (ev) => {
    if (ev.id) {
      const item = chart.getItem(ev.id);
      if (item.type === "link") {
        ev.preventDefault();
      }
    }
  });

  timeline.on("range", async ({ start, end }) => {
    // The range of the KronoGraph timeline has just changed.
    // We respond to this by updating the range of the KeyLines timebar.
    // This will cause the KeyLines timebar to emit a "change" event,
    // but we don't want that to cause the timeline range to be set again,
    // so we make sure this ignoreTimebarEvents flag is set while the timebar "change" event fires.
    ignoreTimebarEvents = true;
    await timebar.range(start, end, { animate: false });
    ignoreTimebarEvents = false;
    filterChart();
  });

  timeline.on("focus", () => {
    chart.selection(timeline.focus());
    filterChart();
  });

  timebar.on("change", ({ type }) => {
    if (type === "range" && !ignoreTimebarEvents) {
      const { dt1, dt2 } = timebar.range();
      timeline.range(dt1, dt2);
      filterChart();
    }
  });
}

main();
const colorPalette = {
  lightBlueGray: "#e5e8eb",
  darkBlueGray: "#242c32",
  darkBlueGrayAlpha: "rgba(36, 44, 50, 0.8)",
  blueGray: "#606d7b",
  blueGrayAlpha: "rgba(96, 109, 123, 0.5)",
  orange: "#f58a3d",
  purple: "#7069FA",
  red: "#ef4e4e",
  blue: "#4098D7",
  yellow: "#f7d06e",
  teal: "#27ab83",
};

export const timelineOptions = {
  focus: {
    backgroundColor: colorPalette.blueGray,
  },
  scales: { showAtBottom: false },
};

export const chartOptions = {
  backgroundAlpha: 0.3,
  backColour: colorPalette.darkBlueGray,
  selectionColour: colorPalette.blueGray,
  navigation: { shown: false },
  controlTheme: "dark",
  handMode: true,
  minZoom: 0.2,
  overview: false,
};

export const timebarOptions = {
  backColour: colorPalette.darkBlueGray,
  sliders: "free",
  area: { colour: "white" },
  type: "area",
  controlBarTheme: "dark",
  sliderColour: colorPalette.blueGrayAlpha,
  showControlBar: false,
  fontColour: colorPalette.lightBlueGray,
  scale: { highlightColour: colorPalette.blueGray },
};

const nodes = [
  { t: "Alexander", c: colorPalette.orange },
  { t: "Bernadette", c: colorPalette.purple },
  { t: "Christelle", c: colorPalette.red },
  { t: "Deirdre", c: colorPalette.teal },
  { t: "Erika", c: colorPalette.blue },
  { t: "Felipa", c: colorPalette.yellow },
  { t: "Grant", c: colorPalette.orange },
  { t: "Heath", c: colorPalette.purple },
  { t: "Irene", c: colorPalette.red },
  { t: "John", c: colorPalette.teal },
].map((node, idx) => {
  return {
    ...node,
    type: "node",
    id: `${idx + 1}`,
    fc: colorPalette.lightBlueGray, // label text color
    fbc: colorPalette.darkBlueGrayAlpha, // label text background color
  };
});

const linkInfo = [
  { id: "a1", time: 1755157740000, id1: "6", id2: "2" },
  { id: "a2", time: 1755158160000, id1: "6", id2: "8" },
  { id: "a3", time: 1755158520000, id1: "4", id2: "1" },
  { id: "a4", time: 1755160020000, id1: "9", id2: "1" },
  { id: "a5", time: 1755163200000, id1: "9", id2: "6" },
  { id: "a6", time: 1755163800000, id1: "4", id2: "3" },
  { id: "a7", time: 1755164400000, id1: "9", id2: "5" },
  { id: "a8", time: 1755165060000, id1: "5", id2: "3" },
  { id: "a9", time: 1755169500000, id1: "7", id2: "9" },
  { id: "a10", time: 1755178620000, id1: "1", id2: "8" },
  { id: "a11", time: 1755184020000, id1: "10", id2: "3" },
  { id: "a12", time: 1755189720000, id1: "8", id2: "5" },
];

const links = linkInfo.map((link) => {
  const { c: c2 } = nodes.find((node) => node.id === link.id1);
  const { c } = nodes.find((node) => node.id === link.id2);
  return {
    ...link,
    type: "link",
    c, // color at id1 end
    c2, // color at id2 end
    a2: true, // show arrow at id2 end
    w: 1.5, // width
    dt: [new Date(link.time)], // time
  };
});

export const chartData = {
  type: "LinkChart",
  items: [...nodes, ...links],
};

const entities = {};
const events = {};

linkInfo.forEach((link) => {
  events[link.id] = { time: new Date(link.time), entityIds: [link.id1, link.id2] };
});

nodes.forEach((node) => {
  entities[node.id] = { color: node.c, label: node.t };
});

export const timelineData = { events, entities };
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="@ci/theme/kg/css/examples.css" />
  </head>
  <body>
    <div style="display: flex; flex-direction: column; height: 100vh">
      <div style="display: flex; flex: 3; min-height: 0">
        <div id="timeline-container" style="flex: 3 1 200px"></div>
        <div id="chart-container" style="flex: 2 1 200px; min-width: 0"></div>
      </div>
      <div id="timebar-container" style="flex: 0 1 150px; min-height: 0"></div>
    </div>
    <script type="module" src="./code.js"></script>
  </body>
</html>
import React, { useEffect, useState, useRef } from "react";
import { createRoot } from "react-dom/client";
import Timeline from "kronograph/react/Timeline";
import { Chart, TimeBar } from "regraph";
import pickBy from "lodash/pickBy";
import zipObject from "lodash/zipObject";

import {
  chartLinks,
  chartNodes,
  chartOptions,
  timelineData,
  timelineOptions,
  timebarOptions,
} from "./data";

const allChartItems = { ...chartNodes, ...chartLinks };

function preventLinkSelection(event) {
  if (chartLinks[event.id]) {
    event.preventDefault();
  }
}

export const Demo = () => {
  const [range, setRange] = useState();
  const [focus, setFocus] = useState([]);
  const [chartItems, setChartItems] = useState(allChartItems);
  const timeline = useRef(null);

  useEffect(() => {
    if (timeline.current) {
      // When timeline range or focus changes, filter the items in the ReGraph chart.
      const { entities, events } = timeline.current.getInRangeItems();
      setChartItems(pickBy(allChartItems, (_item, id) => events[id] || entities[id]));
    }
  }, [range, focus]);

  function changeHandler({
    range: newRange,
    focus: newTimelineFocus,
    selection: newChartSelection,
  }) {
    if (newTimelineFocus) {
      setFocus(newTimelineFocus);
    }
    if (newRange) {
      setRange(newRange);
    }
    if (newChartSelection) {
      setFocus(Object.keys(newChartSelection));
    }
  }

  const chartSelection = zipObject(
    focus,
    focus.map(() => true)
  );

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
      <div style={{ display: "flex", flex: 3, minHeight: 0 }}>
        <div id="timeline-container" style={{ flex: "3 1 200px", minWidth: 0 }}>
          <Timeline
            {...timelineData}
            focus={focus}
            options={timelineOptions}
            onTimelineChange={changeHandler}
            range={range}
            ref={timeline}
          />
        </div>
        <div id="chart-container" style={{ flex: "2 1 200px", minWidth: 0 }}>
          <Chart
            onChange={changeHandler}
            onClick={preventLinkSelection}
            options={chartOptions}
            items={chartItems}
            selection={chartSelection}
          />
        </div>
      </div>
      <div id="timebar-container" style={{ flex: "0 1 150px", minHeight: 0 }}>
        <TimeBar
          items={chartLinks}
          mode="smooth"
          options={timebarOptions}
          onChange={changeHandler}
          range={range}
          animation={{ animate: false }}
        />
      </div>
    </div>
  );
};

const root = createRoot(document.getElementById("my-timeline"));
root.render(<Demo />);
import mapValues from "lodash/mapValues";

const colorPalette = {
  lightBlueGray: "#e5e8eb",
  darkBlueGray: "#242c32",
  darkBlueGrayAlpha: "rgba(36, 44, 50, 0.8)",
  blueGray: "#606d7b",
  blueGrayAlpha: "rgba(96, 109, 123, 0.5)",
  orange: "#f58a3d",
  purple: "#7069FA",
  red: "#ef4e4e",
  blue: "#4098D7",
  yellow: "#f7d06e",
  teal: "#27ab83",
};

export const timelineOptions = {
  focus: { backgroundColor: colorPalette.blueGray },
  scales: { showAtBottom: false },
};

export const chartOptions = {
  backgroundColor: colorPalette.darkBlueGray,
  selection: { color: colorPalette.blueGray },
  navigation: { visible: false },
  controlTheme: "dark",
  minZoom: 0.2,
  overview: false,
};

export const chartNodes = mapValues(
  {
    1: { name: "Alexander", color: colorPalette.orange },
    2: { name: "Bernadette", color: colorPalette.purple },
    3: { name: "Christelle", color: colorPalette.red },
    4: { name: "Deirdre", color: colorPalette.teal },
    5: { name: "Erika", color: colorPalette.blue },
    6: { name: "Felipa", color: colorPalette.yellow },
    7: { name: "Grant", color: colorPalette.orange },
    8: { name: "Heath", color: colorPalette.purple },
    9: { name: "Irene", color: colorPalette.red },
    10: { name: "John", color: colorPalette.teal },
  },
  ({ name, color }) => {
    return {
      label: {
        text: name,
        backgroundColor: colorPalette.darkBlueGrayAlpha,
        color: colorPalette.lightBlueGray,
      },
      color,
    };
  }
);

const linkInfo = {
  a1: { time: Date.UTC(2025, 7, 14, 7, 49), id1: "6", id2: "2" },
  a2: { time: Date.UTC(2025, 7, 14, 7, 56), id1: "6", id2: "8" },
  a3: { time: Date.UTC(2025, 7, 14, 8, 2), id1: "4", id2: "1" },
  a4: { time: Date.UTC(2025, 7, 14, 8, 27), id1: "9", id2: "1" },
  a5: { time: Date.UTC(2025, 7, 14, 9, 20), id1: "9", id2: "6" },
  a6: { time: Date.UTC(2025, 7, 14, 9, 30), id1: "4", id2: "3" },
  a7: { time: Date.UTC(2025, 7, 14, 9, 40), id1: "9", id2: "5" },
  a8: { time: Date.UTC(2025, 7, 14, 9, 51), id1: "5", id2: "3" },
  a9: { time: Date.UTC(2025, 7, 14, 11, 5), id1: "7", id2: "9" },
  a10: { time: Date.UTC(2025, 7, 14, 13, 37), id1: "1", id2: "8" },
  a11: { time: Date.UTC(2025, 7, 14, 15, 7), id1: "10", id2: "3" },
  a12: { time: Date.UTC(2025, 7, 14, 16, 42), id1: "8", id2: "5" },
};

export const chartLinks = mapValues(linkInfo, (link) => {
  return {
    id1: link.id1,
    id2: link.id2,
    color: chartNodes[link.id2].color,
    end2: { color: chartNodes[link.id1].color, arrow: true },
    times: [{ time: link.time }],
    width: 1.5,
  };
});

export const timebarOptions = {
  backgroundColor: colorPalette.darkBlueGray,
  sliders: { type: "free", color: colorPalette.blueGrayAlpha },
  controlBarTheme: "dark",
  labels: {
    fontColor: colorPalette.lightBlueGray,
  },
  scale: { hoverColor: colorPalette.blueGray },
};

const entities = mapValues(chartNodes, (node) => {
  return { label: node.label.text, color: node.color };
});
const events = mapValues(linkInfo, (link) => {
  return {
    time: new Date(link.time),
    entityIds: [link.id1, link.id2],
  };
});

export const timelineData = { events, entities };
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="@ci/theme/kg/css/examples.css" />
  </head>
  <body>
    <div id="my-timeline" style="height: 100vh"></div>
    <script type="module" src="./code.jsx"></script>
  </body>
</html>

Terms of use

These terms do not alter or supersede any existing agreements between you (or your employer) and us.

By accessing or using any Content you agree to be bound by these Terms of Use. Please review these terms carefully before using the website.

The contents of this website, including but not limited to any text, code samples, API references, schemas, interactive tools, and other materials (collectively, the 'Content'), are made available for informational and internal evaluation purposes only. All intellectual property rights in the Content are reserved. No licence is granted to use the Content for any commercial purpose, or to copy, distribute, modify, reverse-engineer, or incorporate any part of the Content into any product or service, without our prior written consent.

This Content is provided “as is” and “as available,” without any representations, warranties, or guarantees of any kind, whether express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, non-infringement, or accuracy. To the fullest extent permitted by applicable law, we expressly exclude and disclaim all implied warranties, conditions, and other terms that might otherwise be implied.

We disclaim all liability for any loss or damage, whether direct, indirect, incidental, consequential, or otherwise, arising from any reliance placed on the Content or from your use of it, to the fullest extent permitted by applicable law. By continuing to access or use the Content, you acknowledge and agree to these terms.