//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//     Use React to create time bar and chart with font icons and tooltips.
//
// demo uses webpack to manage dependencies and build
import React, { useState, useRef, useEffect } from "react";
import { render } from "react-dom";
import { Chart, Timebar } from "react-keylines";
import { LeftPanel, RightPanel } from "react-demo";
import KeyLines from "keylines";

const Tooltip = ({ tooltipState }) => {
  return (
    <span
      id="tooltip"
      className={tooltipState.tooltipClass}
      style={{ left: tooltipState.left, top: tooltipState.top }}
    >
      <div className="arrow"></div>
      <table>
        <tbody>
          <tr>
            <td className="table-cell">
              <b>Appointed on: </b>
            </td>
            <td>{tooltipState.start}</td>
          </tr>
          <tr>
            <td className="table-cell">
              <b>Resigned on: </b>
            </td>
            <td>{tooltipState.end}</td>
          </tr>
        </tbody>
      </table>
    </span>
  );
};

const ReactDemo = () => {
  // data is a global variable stored in reactchartfilter-data.js
  const [state, setState] = useState(data);

  const [handMode, setHandMode] = useState(true);
  const [timebarSelection, setTimebarSelection] = useState([]);
  const [tooltipState, setTooltipState] = useState({
    start: "",
    end: "",
    left: "",
    top: "",
    tooltipClass: "hidden",
  });

  // here we save the reference to the chart and time bar so we can call KeyLines' methods on them
  const chart = useRef(null);
  const timebar = useRef(null);

  const greyColours = ["rgb(105, 105, 105)", "rgb(192, 192, 192)"];

  // only styling the first 3 selected items as the timebar can only show three selection lines
  const selectionColours = [
    ["#9600d5", "#d0a3cb"],
    ["#0044ff", "#6b93ff"],
    ["#ff0000", "#ff9e9e"],
  ];

  const updateHandMode = ({ id, preventDefault }) => {
    if (id === "_toggleMode") {
      preventDefault();
      setHandMode(!handMode);
    }
  };

  const toggleTooltip = ({ id, x, y }) => {
    const getDateFromTimestamp = (timestamp) => {
      return timestamp ? new Date(timestamp).toDateString().slice(4) : "n/a";
    };

    if (chart.current) {
      const item = id ? chart.current.getItem(id) : null;
      const viewWidth = chart.current.viewOptions().width;

      let newTooltipState = {};

      if (item && item.type === "link") {
        const timebarItem = data.items.filter((tbItem) => {
          if (tbItem.id === item.id) {
            return tbItem;
          }
        });
        newTooltipState.start = getDateFromTimestamp(timebarItem[0].dt[0].dt1);
        newTooltipState.end = getDateFromTimestamp(timebarItem[0].dt[0].dt2);

        // style tooltip to left of cursor if it's x position + width exceeds
        // the remaining chart space.
        if (viewWidth && viewWidth > x + 250) {
          newTooltipState.top = `${y - 24}px`;
          newTooltipState.left = `${x + 22}px`;
          newTooltipState.tooltipClass = "";
        } else {
          newTooltipState.top = `${y - 24}px`;
          newTooltipState.left = `${x - 218}px`;
          newTooltipState.tooltipClass = "right";
        }
      } else {
        newTooltipState.tooltipClass = "hidden";
      }

      setTooltipState(newTooltipState);
    }
  };

  // filter the items on the chart according to the range of the timebar
  const onTimebarChange = () => {
    // if all our components are loaded
    if (chart.current && timebar.current) {
      // filter the chart to show only items in the new range
      chart.current
        .filter(timebar.current.inRange, { animate: false, type: "link" })
        .then(() => {
          // and then adjust the chart's layout
          chart.current.layout("organic", {
            time: 500,
            mode: "adaptive",
            easing: "linear",
          });
        });
    }
  };

  const buildItemStyling = (item, width, colour, border = false) => {
    if (item.type === "node") {
      return {
        id: item.id,
        fi: {
          c: colour,
          t: item.d.type === "person" ? "fas fa-user" : "fas fa-building",
        },
        b: border ? colour : null,
      };
    }
    // else it's a link
    return { id: item.id, w: width || 4, c: colour };
  };

  const highlightItemsOnSelection = () => {
    const timebarSelection = [];
    const chartProperties = {};

    // set default colouring
    chart.current.each({ type: "all" }, (item) => {
      chartProperties[item.id] = buildItemStyling(
        item,
        3,
        item.type === "node" ? greyColours[0] : greyColours[1]
      );
    });

    let selectedIds = chart.current.selection();
    if (selectedIds) {
      // prevent the selection of more than 3 items as the timebar can only show three selection lines
      if (selectedIds.length > selectionColours.length) {
        selectedIds = selectedIds.slice(0, selectionColours.length);
        chart.current.selection(selectedIds);
      }

      selectedIds.forEach((id, index) => {
        let neighbouringNodes = [];
        let links = [];
        const item = chart.current?.getItem(id);
        const colour =
          index < selectionColours.length
            ? selectionColours[index][0]
            : greyColours[1];
        const linkColour = selectionColours[index][1];

        if (item && item.type === "node") {
          // all: true - applies styles to hidden nodes + links not in current time range
          const neighbours = chart.current
            ?.graph()
            .neighbours(id, { all: true });
          if (neighbours && neighbours.links) {
            links = neighbours.links;
            neighbouringNodes = neighbours.nodes;
          }

          // colour node
          chartProperties[id] = buildItemStyling(item, 3, colour, true);
          links.forEach((link) => {
            const linkItem = chart.current?.getItem(link);
            chartProperties[link] = buildItemStyling(linkItem, 5, linkColour);
          });
        } else if (item && item.type === "link") {
          links.push(id);
          neighbouringNodes.push(item.id1, item.id2);

          // colour link
          chartProperties[id] = buildItemStyling(item, 5, colour);
        }
        // colour neighbour nodes
        neighbouringNodes.forEach((neighbourId) => {
          if (
            chartProperties[neighbourId].fi.c === greyColours[0] &&
            index < selectionColours.length
          ) {
            chartProperties[neighbourId] = buildItemStyling(
              chart.current?.getItem(neighbourId),
              3,
              selectionColours[index][1]
            );
          }
        });
        // and select in the timebar
        timebarSelection.push({
          id: links,
          index,
          c: colour,
        });
      });

      setTimebarSelection(timebarSelection);
      chart.current.setProperties(
        Object.keys(chartProperties).map((key) => chartProperties[key])
      );
    }
  };

  // get a reference to the loaded timebar
  const loadedTimebar = (newTimebar) => {
    timebar.current = newTimebar;
    timebar.current.mark([
      {
        dt1: new Date("01 Jan 2018 01:00:00"),
        dt2: new Date("01 Jan 2050 01:00:00"),
      },
    ]);
  };
  // get a reference to the loaded chart
  const loadedChart = (newChart) => {
    chart.current = newChart;
  };

  const loadFontIcons = () => {
    let newItems = [];
    state.items.forEach((item) => {
      if (item.type === "node") {
        newItems = [
          ...newItems,
          {
            ...item,
            fi: {
              c: greyColours[0],
              t: item.d.type === "person" ? "fas fa-user" : "fas fa-building",
            },
          },
        ];
      } else {
        newItems.push(item);
      }
    });

    setState({ ...state, items: newItems });
  };

  useEffect(() => {
    document.fonts.load('24px "Font Awesome 5 Free"').then(loadFontIcons);
  }, []);

  return (
    <>
      <LeftPanel name="reactchartfilter">
        <Tooltip tooltipState={tooltipState} />
        <Chart
          ready={loadedChart}
          data={state}
          animateOnLoad={true}
          options={{
            logo: { u: "/images/Logo.png" },
            handMode,
            iconFontFamily: "Font Awesome 5 Free",
            selectionColour: "#35495E",
            hover: 0,
            selectedNode: {
              fbc: "rgba(0,0,0,0)",
            },
            selectedLink: {
              fbc: "rgba(0,0,0,0)",
            },
          }}
          containerClassName="klchart klchart-timebar"
          selection-change={highlightItemsOnSelection}
          hover={toggleTooltip}
          drag-move={toggleTooltip}
          click={updateHandMode}
        />
        <Timebar
          ready={loadedTimebar}
          data={state}
          selection={timebarSelection}
          change={onTimebarChange}
          animateOnLoad={true}
          options={{
            histogram: {
              colour: "#61dbfb",
              highlightColour: "#3e8496",
              markColour: "rgb(192, 192, 192)",
              markHiColour: "rgb(105, 105, 105)",
            },
            playSpeed: 30,
          }}
          containerClassName="kltimebar"
        />
      </LeftPanel>
      <RightPanel
        name="reactchartfilter"
        title="React Integration"
        description="Use React and the time bar to filter chart items."
      >
        <fieldset>
          <legend>Interaction</legend>
          <ul>
            <li>Use the time bar controls to see changes over time</li>
            <li>
              Hover/click a link to to show the duration of an individual's
              directorship
            </li>
            <li>
              Multi-select up to three items to highlight their neighbours
            </li>
          </ul>
        </fieldset>
        <fieldset>
          <legend>Data Format</legend>
          <p>
            The time bar and chart data formats are similar, so you can pass the
            same data object to both components.
          </p>
        </fieldset>
      </RightPanel>
    </>
  );
};

// render our top-level component into the DOM
render(<ReactDemo />, document.querySelector(".chart-wrapper"));
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>React Integration</title>
    <link rel="stylesheet" type="text/css" href="/css/keylines.css">
    <link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
    <link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
    <link rel="stylesheet" type="text/css" href="/css/demo.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/fontawesome.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/solid.css">
    <link rel="stylesheet" type="text/css" href="/reactchartfilter.css">
    <link rel="preload" as="font" href="fonts/fontAwesome5/fa-solid-900.woff2" crossorigin="anonymous">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
    <script src="/reactchartfilter-data.js" defer type="text/javascript"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols">
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
      <!-- This is a bundled JavaScript file.-->
      <!-- React conventions are to include scripts after the target DOM node has loaded.-->
      <script src="/reactchartfilter.js" defer type="text/javascript"></script>
    </div>
  </body>
</html>
//
//     React components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

// This file provides React wrappers for the KeyLines chart and time bar
// You are welcome to make changes for better integration with your own projects

import React from 'react';
import KeyLines from 'keylines';

function invoke(fn, ...args) {
  return (typeof fn === 'function') ? fn(...args) : undefined;
}

// This is the lowest level wrapper of the KeyLines integration - it deals with loading of the
// KeyLines component, options, resizing and raising KeyLines events up

function createKeyLinesComponent(type, onLoadFn) {

  class KlComponent extends React.Component {
    component = undefined;
    componentLoading = false;
    type = type;
    onLoad = onLoadFn;

    constructor(props) {
      super(props);

      // Bind our class specific functions to this
      // See: https://facebook.github.io/react/docs/react-without-es6.html#autobinding
      ['applyProps', 'setSelection', 'onEvent', 'onLoad'].forEach((prop) => {
        this[prop] = this[prop].bind(this);
      });
    }

    componentDidMount() {
      if (this.componentLoading) { return; }
      this.componentLoading = true;

      const componentDefinition = {
        container: this.klElement,
        type: this.type,
        options: this.props.options,
      };

      KeyLines.create(componentDefinition).then((component) => {
        this.componentLoading = false;
        this.component = component;

        component.on('all', this.onEvent);

        this.applyProps().then(() => {
          // Finally, tell the parent about the component so it can call functions on it
          invoke(this.props.ready, component, this.klElement);
        });
      }).catch(() => {
        this.componentLoading = false;
      });
    }

    componentWillUnmount() {
      if (this.component) {
        // Clean up the component
        this.component.destroy();
        this.component = undefined;
      }
    }

    setSelection() {
      // This works because the selectionchange event is not raised when changing selection
      // programmatically
      const selectedItems = this.component.selection(this.props.selection);

      if (this.type === 'chart' && selectedItems.length > 0) {
        this.component.zoom('selection', { animate: true, time: 250 });
      }
    }

    componentDidUpdate(prevProps) {
      if (this.component) {
        // we need to intercept the options being set and pass them on to the component manually
        if (this.props.options && this.props.options !== prevProps.options) {
          this.component.options(this.props.options); // don't worry about callback here
        }

        const reload = this.props.data !== prevProps.data;
        if (reload) {
          // note applyProps also deals with selection
          return this.applyProps();
        } else if (this.props.selection || prevProps.selection) {
          if (this.props.selection !== prevProps.selection) {
            this.setSelection();
          }
        }
      }
    }

    // this looks for a handler with the right name on the props, and if it finds
    // one, it will call it with the event arguments.
    onEvent(props) {
      return invoke(this.props[props.name], props.event);
    }

    // this applies all the component related props except the options which are
    // handled differently
    applyProps() {
      return new Promise((resolve) => {
        this.component.load(this.props.data).then(() => {
          this.onLoad({ animate: !!this.props.animateOnLoad }).then(() => {
            this.component.selection(this.props.selection);
            resolve();
          });
        });
      });
    }

    render() {
      const { containerClassName, style, children } = this.props;
      return (
        <div ref={(klElement) => { this.klElement = klElement; }} className={containerClassName}
          style={style}>
          {children}
        </div>
      );
    }
  }

  // defaultProps has to be a static property of the component class
  KlComponent.defaultProps = {
    data: {},
    animateOnLoad: false,
    options: {},
    selection: [],
  };

  return KlComponent;
}

function createChartComponent() {
  function onLoad(options) {
    // default behaviour when loading data in the chart
    return this.component.layout('organic', options);
  }

  return createKeyLinesComponent('chart', onLoad);
}

// Define the Chart component
export const Chart = createChartComponent();

function createTimebarComponent() {
  function onLoad(options) {
    // default behaviour when loading data in the timebar
    return this.component.zoom('fit', options);
  }

  return createKeyLinesComponent('timebar', onLoad);
}

// Define the Timebar component
export const Timebar = createTimebarComponent();
//
//     React demo components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

import React, { useEffect, useRef } from 'react';

/**
 * @typedef {Object} RightPanelProps
 * @property {string} name - The name of the demo.
 * @property {string} title - The title of the demo.
 * @property {string} description - The short description of the demo.
 */

/**
 * @param {RightPanelProps} props
 */
const RightPanel = (props) => {
  const sourceTab = useRef(null)
  const aboutTab = useRef(null)

  useEffect(() => {
    const sourcePanel = document.getElementById('sourcePanel');
    const aboutPanel = document.getElementById('aboutPanel');
    if (sourcePanel && aboutPanel) {
      // re-assign parent of child elements, rather than copy HTML so as to not remove any event handlers
      sourceTab.current.appendChild(sourcePanel);
      for(const child of Array.from(sourcePanel.children)) {
        sourceTab.current.appendChild(child);
      }

      const aboutTemplate = aboutPanel.innerHTML;
      aboutTab.current.innerHTML = aboutTemplate;
    }
  }, [])

  return (
    <div className="rhs citext flex flex-column">
      <div className="title-bar">
        <h3 className="flex1">{document.title.split('|')[0]}</h3>
        <div className="buttons flex" id="titlebarbuttons">
          <a className="btn btn-label disabled fshide" href="#" data-toggle="tooltip" id="fullscreenDisabled">
            <i className="fa fa-compress"></i>
          </a>
          <a className="btn btn-label" href="#" data-toggle="tooltip" id="fullscreenButton">
            <i className="fa fa-expand"></i>
          </a>
        </div>
      </div>
      <div className="notice">
        <div className="boxout">
          <div className="boxout-image">
            <img src={`/images/${props.name}/ReGraph.png`}></img>
          </div>
          <p className="boxout-body" style={{ paddingLeft: 42, minHeight: 40 }}>Using React? Check out ReGraph, our React graph visualisation toolkit.&nbsp;
            <a href="mailto:[email protected]">Contact us</a>
            <span> or </span>
            <a href="https://cambridge-intelligence.com/regraph/?utm_source=keylines&utm_medium=sdk-site&utm_campaign=keylines-sdk-referral-link">learn more</a>.
          </p>
        </div>
      </div>
      <div className="tabsdiv tab" data-tab-group="rhs">
        <a className="btn btn-spaced fshide rhs-toggle fancy-button" id="closeRHS" href="#" data-toggle="tooltip">
          <i className="fa fa-chevron-right"></i>
        </a>
        <button className="tablinks active" name="controlsTab" type="button">Controls</button>
        <button className="tablinks" name="aboutTab" type="button">About</button>
        <button className="tablinks" name="sourceTab" type="button">Source</button>
      </div>
      <div className="tab-content-panel flex1" data-tab-group="rhs">
        <div className="toggle-content is-visible tabcontent" id="controlsTab">
          <div className="cicontent">
            <img className='kl-center' src={`/images/${props.name}/reactjs.png`} width='75%' />
            {props.children}
          </div>
        </div>
        <div ref={aboutTab} className="toggle-content tabcontent" id="aboutTab"></div>
        <div ref={sourceTab} className="toggle-content tabcontent" id="sourceTab"></div>
      </div>
    </div>
  );
}

/**
 * @typedef {Object} LeftPanelProps
 * @property {string} name
 */

/**
 * @param {LeftPanelProps} props
 */
const LeftPanel = (props) => {
  return (
    <div className="flex1" id="lhs">
      <div id="lhsVisual" className="toggle-content is-visible">
        {props.children}
      </div>
      <div id="lhsSource" className="toggle-content">
        <iframe id="sourceView" style={{ width: '100%', height: '100%', border: 'none' }} src={`source-${props.name}.js.htm`} scrolling="no"></iframe>
      </div>
    </div>
  );
}

export { LeftPanel, RightPanel };
Loading source