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;
}