Search

Events

Interaction

Explore mouse, keyboard and touch events by interacting with chart.

Events
View live example →

Interact with the chart to see events logged in the controls below it.

You can control which events are logged with the checkboxes below the chart.

High-precision events (onDrag, onPointerMove, onWheel) fire continuously, and will usually require throttling or debouncing if you cause state changes in their handlers.

ReGraph returns detailed information on each event.

See also

import React, { useMemo, useState } from "react";
import { createRoot } from "react-dom/client";

import * as L from "leaflet/dist/leaflet";
import mapValues from "lodash/mapValues";
import throttle from "lodash/throttle";
import ReactJson from "react-json-view";
import { Chart } from "regraph";

import data, { color, textColor } from "./data";

import "leaflet/dist/leaflet.css";
import "@ci/theme/rg/css/layout.css";
import "@ci/theme/rg/css/button.css";
import "@fortawesome/fontawesome-free/css/fontawesome.css";
import "@fortawesome/fontawesome-free/css/solid.css";

const MAP_BOUNDS = {
  nw: [50.564969, -128.133788],
  se: [24.783256, -62.45472],
};

const layout = { name: "organic", tightness: 2 };

const options = {
  navigation: { position: "ne" },
  iconFontFamily: "Font Awesome 5 Free",
  map: {
    leaflet: {
      maxZoom: 13,
      maxBounds: L.latLngBounds(MAP_BOUNDS.nw, MAP_BOUNDS.se),
      maxBoundsViscosity: 1,
    },
  },
};

const combine = { properties: ["city"] };

const CheckBox = ({ id, text, onChange, checked }) => (
  <label htmlFor={id} className="container">
    {text}
    <input type="checkbox" id={id} defaultChecked={checked} onChange={onChange} />
    <span className="checkmark" />
  </label>
);

const Options = (props) => {
  const {
    onClearLogs,
    onLogHoverClick,
    logHoverEvents,
    onLogInteractionClick,
    logInteractionEvents,
    onLogHighPrecisionClick,
    logOnHighPrecision,
    onExpandArgumentsClick,
    expandArguments,
    onMapModeClick,
    mapMode,
  } = props;
  return (
    <div className="event-options">
      <span className="mode-label">Options:</span>
      <CheckBox
        id="logOnHover"
        text="Log hover events"
        onChange={onLogHoverClick}
        checked={logHoverEvents}
      />
      <CheckBox
        id="logOnItemInteraction"
        text="Log item interaction events"
        onChange={onLogInteractionClick}
        checked={logInteractionEvents}
      />
      <CheckBox
        id="logHighPrecisionEvents"
        text="Log high precision events"
        onChange={onLogHighPrecisionClick}
        checked={logOnHighPrecision}
      />
      <CheckBox
        id="expandArguments"
        text="Expand event arguments"
        onChange={onExpandArgumentsClick}
        checked={expandArguments}
      />
      <CheckBox
        id="toggleMapMode"
        text="Toggle Leaflet integration"
        onChange={onMapModeClick}
        checked={mapMode}
      />
      <button type="button" onClick={onClearLogs}>
        Clear logs
      </button>
    </div>
  );
};

const EventNotificationCircle = (timestamp) => {
  const now = new Date().valueOf();
  const fadeDelay = 500;
  return (
    <div
      className={
        now - timestamp > fadeDelay
          ? "event-circle old-event-circle"
          : "event-circle new-event-circle"
      }
    />
  );
};

function EventLog(props) {
  const { history, expandArguments, ...rest } = props;

  return (
    <div className="logs" style={{ flexBasis: 0 }} {...rest}>
      <table className="events">
        <thead>
          <tr>
            <th className="right">Recent</th>
            <th>Event</th>
            <th>Arguments</th>
          </tr>
        </thead>
               
          {history.map(({ event, value, timestamp }) => {
            return (
              <tr key={timestamp}>
                <td>{                                  }</td>
                <td>{event}</td>
                <td>
                  <ReactJson
                    src={value}
                    collapsed={expandArguments ? 2 : 0}
                    name={null}
                    enableClipboard={false}
                    displayDataTypes={false}
                  />
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

function formatEvent(event) {
  if (event) {
    return mapValues(event, (propValue) => {
      if (typeof propValue === "function") {
        return "function";
      }
      return propValue;
    });
  }
  return event;
}

function ChartEvents() {
  const [history, setHistory] = useState([]);
  const [demoOptions, setDemoOptions] = useState({
    logOnHover: false,
    logOnHighPrecision: false,
    expandArguments: false,
    map: false,
  });

  const updateHistory = (eventName, event) => {
    // update the history
    setHistory((current) => {
      return [
        {
          event: eventName,
          value: formatEvent(event),
          timestamp: new Date().valueOf(),
        },
        ...current,
      ];
    });
  };

  const clearHistory = () => {
    setHistory([]);
  };

  const handleCombineNodes = (event) => {
    event.setStyle({ closedStyle: { color }, label: { color: textColor, backgroundColor: color } });
  };

  const handleChange = (event) => {
    updateHistory("onChange", event);
  };

  const handleClick = (event) => {
    updateHistory("onClick", event);
  };

  const handleContextMenu = (event) => {
    updateHistory("onContextMenu", event);
  };

  const handleDoubleClick = (event) => {
    updateHistory("onDoubleClick", event);
  };

  const handleDragEnd = (event) => {
    updateHistory("onDragEnd", event);
  };

  const handleDrag = useMemo(
    () =>
      throttle((event) => {
        if (demoOptions.logOnHighPrecision) {
          updateHistory("onDrag", event);
        }
      }, 200),
    [demoOptions]
  );

  const handleDragOver = (event) => {
    updateHistory("onDragOver", event);
  };

  const handleDragStart = (event) => {
    updateHistory("onDragStart", event);
  };

  const handleHover = (event) => {
    if (demoOptions.logOnHover) {
      updateHistory("onHover", event);
    }
  };

  const handleItemInteraction = (event) => {
    if (demoOptions.logInteractionEvents) {
      updateHistory("onItemInteraction", event);
    }
  };

  const handlePointerDown = (event) => {
    updateHistory("onPointerDown", event);
  };

  const handlePointerMove = useMemo(
    () =>
      throttle((event) => {
        if (demoOptions.logOnHighPrecision) {
          updateHistory("onPointerMove", event);
        }
      }, 200),
    [demoOptions]
  );

  const handlePointerUp = (event) => {
    updateHistory("onPointerUp", event);
  };

  const handleViewChange = useMemo(
    () =>
      throttle((event) => {
        if (demoOptions.logOnHighPrecision) {
          updateHistory("onViewChange", event);
        }
      }, 200),
    [demoOptions]
  );

  const handleWheel = useMemo(
    () =>
      throttle((event) => {
        if (demoOptions.logOnHighPrecision) {
          updateHistory("onWheel", event);
        }
      }, 200),
    [demoOptions]
  );

  const renderOptions = () => {
    const { expandArguments, logOnHover, logInteractionEvents, logOnHighPrecision, map } =
      demoOptions;
    return (
      <Options
        onClearLogs={clearHistory}
        onLogHoverClick={(event) =>
          setDemoOptions((current) => ({ ...current, logOnHover: event.target.checked }))
        }
        onLogInteractionClick={(event) =>
          setDemoOptions((current) => ({ ...current, logInteractionEvents: event.target.checked }))
        }
        onLogHighPrecisionClick={(event) =>
          setDemoOptions((current) => ({ ...current, logOnHighPrecision: event.target.checked }))
        }
        onExpandArgumentsClick={(event) =>
          setDemoOptions((current) => ({ ...current, expandArguments: event.target.checked }))
        }
        onMapModeClick={(event) =>
          setDemoOptions((current) => ({ ...current, map: event.target.checked }))
        }
        expandArguments={expandArguments}
        logHoverEvents={logOnHover}
        logInteractionEvents={logInteractionEvents}
        logOnHighPrecision={logOnHighPrecision}
        mapMode={map}
      />
    );
  };

  return (
    <div className="story">
      <div className="chart-wrapper">
        <Chart
          items={data.items}
          annotationPositions={data.annotationPositions}
          layout={layout}
          combine={combine}
          options={options}
          onChange={handleChange}
          onClick={handleClick}
          onCombineNodes={handleCombineNodes}
          onContextMenu={handleContextMenu}
          onDoubleClick={handleDoubleClick}
          onDragEnd={handleDragEnd}
          onDrag={handleDrag}
          onDragOver={handleDragOver}
          onDragStart={handleDragStart}
          onHover={handleHover}
          onItemInteraction={handleItemInteraction}
          onPointerDown={handlePointerDown}
          onPointerMove={handlePointerMove}
          onPointerUp={handlePointerUp}
          onViewChange={handleViewChange}
          onWheel={handleWheel}
          map={demoOptions.map}
        />
      </div>
      <div className="event-panel">
        {               }
        <EventLog history={history} expandArguments={demoOptions.expandArguments} />
      </div>
    </div>
  );
}

const FontReadyChart = React.lazy(() =>
  document.fonts.load("900 24px 'Font Awesome 5 Free'").then(() => ({
    default: ChartEvents,
  }))
);

export function Demo() {
  return (
    <React.Suspense fallback="">
      <FontReadyChart />
    </React.Suspense>
  );
}

const root = createRoot(document.getElementById("regraph"));
root.render(<Demo />);
import { style } from "@ci/theme/rg/js/storyStyles";

const data = {
  items: {
    1: {
      label: { ...style.primaryLabel1, text: "John Doe" },
      ...style.primary1,
      glyphs: [{ ...style.primary3, position: "ne", label: { text: "43" } }],
      coordinates: {
        lat: 47.449,
        lng: -122.309306,
      },
    },
    2: {
      label: { ...style.primaryLabel1, text: "Registered Office" },
      ...style.primary1,
      coordinates: {
        lat: 32.847111,
        lng: -96.851778,
      },
    },
    4: {
      label: { ...style.primaryLabel1, text: "Investment Round" },
      ...style.primary1,
      glyphs: [
        { ...style.primary3, angle: 45, label: { text: "$120k" } },
        { ...style.secondary3, angle: 315, label: { text: "x" } },
      ],
      coordinates: {
        lat: 37.721278,
        lng: -122.220722,
      },
    },
    5: {
      label: { ...style.primaryLabel1, text: "Angel Investor" },
      ...style.primary1,
      donut: {
        width: 15,
        segments: [
          { ...style.secondary1.color, size: 30 },
          { ...style.tertiary1.color, size: 40 },
          { ...style.primary1.color, size: 30 },
        ],
      },
      coordinates: {
        lat: 33.636719,
        lng: -84.428067,
      },
    },
    6: {
      label: { ...style.primaryLabel1, text: "Software Business" },
      ...style.primary1,
      halos: [
        { ...style.primary2, radius: 40, width: 20 },
        { ...style.primary3, radius: 60, width: 5 },
      ],
      coordinates: {
        lat: 42.364347,
        lng: -71.005181,
      },
    },
    7: {
      color: null,
      glyphs: [
        { ...style.cat11, position: 0, label: { text: "1" } },
        { ...style.cat07, position: 1, label: { text: "2" } },
        { ...style.cat03, position: 2, label: { text: "3" } },
      ],
      label: {
        text: "Employees",
      },
      coordinates: {
        lat: 45.5051,
        lng: -122.675,
      },
    },
    8: {
      label: { text: "John Doe" },
      ...style.primary1,
      shape: {
        width: "auto",
        height: "auto",
      },
      border: { radius: 6 },
      label: [
        {
          ...style.darkLabel,
          fontIcon: {
            text: "fas fa-user",
          },
          fontSize: 20,
          padding: { top: 0, right: 0, bottom: 0, left: 8 },
          position: { vertical: 18, horizontal: "left" },
        },
        {
          ...style.darkLabel,
          text: "Kevin Presto",
          bold: true,
          fontSize: 15,
          padding: { top: 8, right: 10, bottom: 0 },
          margin: { left: 10 },
          position: { vertical: "top", horizontal: "right" },
        },
        {
          ...style.darkLabel,
          fontSize: 8,
          padding: { top: 0, right: 10, bottom: 0, left: 0 },
          text: "Label 2",
        },
        {
          ...style.darkLabel,
          fontSize: 8,
          padding: { top: 0, right: 10, bottom: 0, left: 0 },
          text: "Label 3",
        },
      ],
      coordinates: {
        lat: 39.0997,
        lng: -94.5783,
      },
    },
    9: {
      label: { ...style.primaryLabel1, text: "Department 1" },
      ...style.primary1,
      coordinates: {
        lat: 34.0549,
        lng: -118.2426,
      },
      data: { city: "Los Angeles Group" },
    },
    10: {
      label: { ...style.primaryLabel1, text: "Department 2" },
      ...style.primary1,
      coordinates: {
        lat: 34.0549,
        lng: -118.2426,
      },
      data: { city: "Los Angeles Group" },
    },
    l1: {
      ...style.link,
      id1: "6",
      id2: "2",
    },
    l2: {
      ...style.link,
      id1: "6",
      id2: "8",
    },
    l3: {
      ...style.link,
      id1: "6",
      id2: "1",
      label: { text: "Director" },
    },
    l4: {
      ...style.link,
      id1: "4",
      id2: "2",
    },
    l5: {
      ...style.link,
      id1: "4",
      id2: "5",
      end1: {
        arrow: true,
      },
      end2: {
        label: { text: "Invests" },
      },
    },
    l6: {
      ...style.link,
      id1: "6",
      id2: "4",
    },
    l7: {
      ...style.link,
      id1: "6",
      id2: "7",
      label: {
        text: "Salary",
      },
      end1: {
        label: { text: "Pays" },
      },
      end2: {
        arrow: true,
        glyphs: [
          { ...style.cat11, position: 0, label: { text: "$" } },
          { ...style.cat07, position: 1, label: { text: "$" } },
          { ...style.cat03, position: 2, label: { text: "$" } },
        ],
      },
    },
    an1: {
      subject: "2",
      label: { text: "A simple annotation" },
    },
    l8: {
      ...style.link,
      id1: "6",
      id2: "9",
    },
    l9: {
      ...style.link,
      id1: "6",
      id2: "10",
    },
  },
  annotationPositions: { an1: { distance: 40, angle: "sw" } },
};

export default data;
export const color = style.primary1.color;
export const textColor = style.primaryLabel1.color;
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div id="regraph" style="height: 100vh"></div>
    <script type="module" src="./code.jsx"></script>
  </body>
</html>
@import "@ci/theme/rg/css/variables.css";

table.events {
  min-width: 430px;
}

table.events td {
  vertical-align: text-top;
  padding: var(--light-mode-button-padding);
  font-family: monospace;
}

table.events th {
  text-align: left;
  padding: var(--light-mode-button-padding);
}

table.events th.right {
  text-align: right;
}

.event-circle {
  transition: background-color 1200ms ease-out;
  -webkit-transition: background-color 1200ms ease-out;
  height: 6px;
  width: 6px;
  border-radius: 50%;
  float: right;
  background-color: white;
}

.new-event-circle {
  background-color: #2dcda8;
}

.old-event-circle {
  background-color: white;
}

.checkbox {
  padding-left: 12px;
}

.checkbox-container-fixed {
  position: absolute;
  top: -16px;
  background-color: rgba(255, 255, 255, 0.4);
}

.log-container {
  display: flex;
  padding: 36px 12px 12px 12px;
  height: 70%;
}

.logs {
  font-family: monospace;
  border: solid 1px lightgray;
  overflow: auto;
  margin: 0;
  flex: 1 1 auto;
}

.logs .react-json-view {
  white-space: nowrap;
  /* Prevents jump when opening/closing events */
  min-width: 316px;
}

.logs .react-json-view .string-value {
  white-space: initial;
}

.event-panel .logs {
  border: none;
}

.event-panel .logs .count > * {
  background-color: var(--secondary1);
  color: white;
  border-radius: 20px;
  padding: 4px 8px;
}

.log-container-fixed {
  display: flex;
  padding: 36px 12px 12px 12px;
  position: absolute;

  bottom: 0;
  height: 50vh;
  z-index: 400;
}

/* Customize the label (the container) */
.container {
  display: block;
  position: relative;
  padding-left: 35px;
  margin: 6px 12px 6px 24px;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

/* Hide the browser's default checkbox */
.container input {
  position: absolute;
  opacity: 0;
  cursor: pointer;
  height: 0;
  width: 0;
}

.mode-label {
  margin-left: 12px;
}

/* Create a custom checkbox */
.checkmark {
  position: absolute;
  top: -3px;
  left: 0;
  height: 25px;
  width: 25px;
  background-color: #eee;
}

/* On mouse-over, add a grey background color */
.container:hover input ~ .checkmark {
  background-color: #ccc;
}

/* When the checkbox is checked, add a teal background */
.container input:checked ~ .checkmark {
  background-color: var(--primary2);
}

/* Create the checkmark/indicator (hidden when not checked) */
.checkmark:after {
  content: "";
  position: absolute;
  display: none;
}

.checkmark.round {
  border-radius: var(--light-mode-small-border-radius);
}

/* Show the checkmark when checked */
.container input:checked ~ .checkmark:after {
  display: block;
}

input[disabled] {
  opacity: 0.4;
}

/* Style the checkmark/indicator */
.container .checkmark:after {
  left: 9px;
  top: 5px;
  width: 5px;
  height: 10px;
  border: solid white;
  border-width: 0 3px 3px 0;
  -webkit-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  transform: rotate(45deg);
}

.event-options {
  flex: 1;
  flex-direction: column;
  max-width: 300px;
  align-items: flex-start;
}

.event-options .legend-radio {
  margin-left: 22px;
}

.event-options .legend-radio:hover {
  background-color: inherit;
}

.event-options .checkmark {
  border: solid 2px var(--primary2);
  background-color: white;
  width: 21px;
  height: 21px;
}

.event-options .container:hover input ~ .checkmark {
  background-color: #eee;
}

.event-options .container:hover input:checked ~ .checkmark {
  background-color: var(--primary1);
}

.event-options .checkmark:after {
  left: 7px;
  top: 3px;
}

#clear-logs {
  float: right;
}

.chart-wrapper {
  flex-grow: 5;
}
.event-panel {
  display: flex;
  flex: 1;
  flex-grow: 3;
  border-top: solid 1px #c0c0c0;
  background-color: white;
  padding-top: 8px;
  max-height: 300px;
  overflow: auto;
}

/*  Webkit hides scroll bars. Force them on the log container so that
    You get a sense of more logs being added
 */
.logs::-webkit-scrollbar {
  -webkit-appearance: none;
}

.logs::-webkit-scrollbar:vertical {
  width: 11px;
}

.logs::-webkit-scrollbar:horizontal {
  height: 11px;
}

.logs::-webkit-scrollbar-thumb {
  border-radius: 8px;
  border: 2px solid white; /* should match background, can't be transparent */
  background-color: rgba(0, 0, 0, 0.5);
}

.logs::-webkit-scrollbar-track {
  background-color: #fff;
  border-radius: 8px;
}

.options input {
  box-shadow: none;
}

.options input:hover {
  box-shadow: none;
}

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.