Interact with the chart and use the controls in the bottom left corner to Save and Load the changes.
You can capture the exact state of the chart by saving the current ReGraph props and any changes tracked by the onChange event.
The saved chart is loaded back to ReGraph when a new React key prop is passed and a re-render is triggered.
You can use a React reducer function to manage the state updates. Once the saved chart is loaded, any following chart updates will automatically delete the changes collected by onChange, and give control back to the ReGraph component to take advantage of its adaptive behavior.
See also
import React, { useReducer, useRef } from "react";
import { createRoot } from "react-dom/client";
import { Chart } from "regraph";
import data from "./data";
export const Demo = () => {
// useReducer acts much like useState but is passed through our custom saveAndLoadReducer function defined below
const [state, dispatch] = useReducer(saveAndLoadReducer, {
key: undefined,
saves: [],
currentSave: {
combine: { properties: ["group"] },
options: { navigation: false, overview: false },
items: data(),
openCombos: {},
},
});
const chartRef = useRef();
const comboIds = useRef({});
const chartChanges = useRef({});
// separate out our metadata from our chart properties
const { image, openCombos, ...chartProps } = state.currentSave;
function onChange(change) {
if (!state.hasLoaded) {
// perform our initial save on the first load of the chart
dispatch({ type: "update", state: { hasLoaded: true } });
doSave();
}
// track transient changes to the chart that occur via user interaction in a ref
// these are merged on top of chart properties when saving a snapshot
["positions", "selection", "view"].forEach((prop) => {
if (prop in change) {
chartChanges.current[prop] = change[prop];
}
});
}
async function doSave() {
const { url } = await chartRef.current.export({
fitTo: {
width: 600,
height: 400,
},
});
const newSave = {
...state.currentSave,
...chartChanges.current,
image: url,
};
dispatch({
type: "update",
state: { saves: [...state.saves, newSave] },
});
}
function doLoad(save) {
comboIds.current = {};
chartChanges.current = {};
// perform a complete remount of the chart. the reducer will take care of animation and
// key switching for us. we create a shallow copy of the currentSave to ensure we don't overwrite it
// when relinquishing the props
dispatch({ type: "load", state: { currentSave: { ...save } } });
}
function onCombineNodes({ id, setStyle, nodes }) {
comboIds.current[id] = true;
setStyle({
open: !!(openCombos[id] ?? true),
label: null,
closedStyle: {
color: "rgba(240,240,240,1)",
glyphs: [{ label: { text: Object.keys(nodes).length }, color: "grey" }],
size: 1.5,
border: { color: "grey" },
},
});
}
function onDoubleClick({ id }) {
if (comboIds.current[id]) {
const isOpen = openCombos[id] ?? true;
dispatch({
type: "update",
state: {
currentSave: {
...state.currentSave,
openCombos: { ...openCombos, [id]: !isOpen },
combine: { ...state.currentSave.combine },
},
},
});
}
}
return (
<div className="story">
<div className="chart-wrapper">
<Chart
onCombineNodes={onCombineNodes}
onDoubleClick={onDoubleClick}
onChange={onChange}
ref={chartRef}
key={state.key}
{...chartProps}
/>
<div className="save-panel">
<div className="save-list">
{state.saves.map((save) => (
<SaveView
key={save.image}
save={save}
onDeleteClick={() =>
dispatch({
type: "update",
state: { saves: state.saves.filter((a) => a !== save) },
})
}
onLoadClick={() => doLoad(save)}
/>
))}
</div>
<hr />
<div className="controls">
<button type="button" onClick={doSave}>
Save
</button>
<button
type="button"
onClick={() => dispatch({ type: "update", state: { saves: [] } })}
>
Clear
</button>
</div>
</div>
</div>
</div>
);
};
// define our reducer function that takes the old state and an action to return a new state.
// - we accept 'load' and 'update' actions which will remount the chart or update the chart with new properties respectively.
// - we relinquish control of these properties back to ReGraph to take advantage of its adaptive layouts and view autofitting.
const saveAndLoadReducer = (prevState, { type, state }) => {
// shallow merge the old and new state together to form the next state
const nextState = { ...prevState, ...state, lastUpdateType: type };
// only relinquish control on the first 'update' after a 'load'
if (type === "update" && prevState.lastUpdateType === "load") {
const relinquishedProps = ["positions", "view", "selection", "animation"];
relinquishedProps.forEach((prop) => {
if (nextState.currentSave[prop] === prevState.currentSave[prop]) {
delete nextState.currentSave[prop];
}
});
} else if (type === "load") {
// if we're loading from a snapshot then switch key and disable animation
nextState.key = crypto.randomUUID();
nextState.currentSave.animation = { animate: false };
}
return nextState;
};
const SaveView = ({ save, onDeleteClick, onLoadClick }) => {
return (
<div className="save-box">
<button onClick={onDeleteClick} className="delete-save">
<i className="fas fa-times" />
</button>
<button onClick={onLoadClick} className="load-save">
Load
</button>
<img alt="A chart export" src={save.image} />
</div>
);
};
const root = createRoot(document.getElementById("regraph"));
root.render(<Demo />); import { style } from "@ci/theme/rg/js/storyStyles";
const nodeColor = style.primary1.color;
const linkColor = style.link.color;
export default function data() {
return {
a: {
data: {
group: "1",
},
color: nodeColor,
},
b: {
data: {
group: "1",
},
color: nodeColor,
},
c: {
data: {
group: "1",
},
color: nodeColor,
},
d: {
data: {
group: "1",
},
color: nodeColor,
},
e: {
data: {
group: "1",
},
color: nodeColor,
},
f: {
data: {
group: "2",
},
color: nodeColor,
},
g: {
data: {
group: "2",
},
color: nodeColor,
},
h: {
data: {
group: "2",
},
color: nodeColor,
},
i: {
data: {
group: "2",
},
color: nodeColor,
},
j: {
data: {
group: "3",
},
color: nodeColor,
},
k: {
data: {
group: "3",
},
color: nodeColor,
},
l: {
data: {
group: "3",
},
color: nodeColor,
},
m: { color: nodeColor },
n: { color: nodeColor },
"a-c": {
id1: "a",
id2: "c",
color: linkColor,
width: 4,
},
"a-f-1": {
id1: "a",
id2: "f",
color: linkColor,
width: 4,
},
"a-f-2": {
id1: "a",
id2: "f",
color: linkColor,
width: 4,
},
"b-c": {
id1: "b",
id2: "c",
color: linkColor,
width: 4,
},
"b-h": {
id1: "b",
id2: "h",
color: linkColor,
width: 4,
},
"b-g": {
id1: "b",
id2: "g",
color: linkColor,
width: 4,
},
"b-n": {
id1: "b",
id2: "n",
color: linkColor,
width: 4,
},
"c-d": {
id1: "c",
id2: "d",
color: linkColor,
width: 4,
},
"c-g": {
id1: "c",
id2: "g",
color: linkColor,
width: 4,
},
"c-j": {
id1: "c",
id2: "j",
color: linkColor,
width: 4,
},
"d-f": {
id1: "d",
id2: "f",
color: linkColor,
width: 4,
},
"f-d": {
id1: "f",
id2: "d",
color: linkColor,
width: 4,
},
"d-e": {
id1: "d",
id2: "e",
color: linkColor,
width: 4,
},
"e-k-1": {
id1: "e",
id2: "k",
color: linkColor,
width: 4,
},
"e-k-2": {
id1: "e",
id2: "k",
color: linkColor,
width: 4,
},
"f-g": {
id1: "f",
id2: "g",
color: linkColor,
width: 4,
},
"f-h": {
id1: "f",
id2: "h",
color: linkColor,
width: 4,
},
"f-j": {
id1: "f",
id2: "j",
color: linkColor,
width: 4,
},
"f-m": {
id1: "f",
id2: "m",
color: linkColor,
width: 4,
},
"g-h": {
id1: "g",
id2: "h",
color: linkColor,
width: 4,
},
"h-m-1": {
id1: "h",
id2: "m",
color: linkColor,
width: 4,
},
"h-m-2": {
id1: "h",
id2: "m",
color: linkColor,
width: 4,
},
"i-j": {
id1: "i",
id2: "j",
color: linkColor,
width: 4,
},
"i-l": {
id1: "i",
id2: "l",
color: linkColor,
width: 4,
},
"j-l": {
id1: "j",
id2: "l",
color: linkColor,
width: 4,
},
"k-n": {
id1: "k",
id2: "n",
color: linkColor,
width: 4,
},
"l-j": {
id1: "l",
id2: "j",
color: linkColor,
width: 4,
},
"l-n": {
id1: "l",
id2: "n",
color: linkColor,
width: 4,
},
};
} <!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> .save-panel {
border: 1px solid rgb(154, 154, 154);
color: black;
position: absolute;
left: 10px;
bottom: 10px;
border-radius: 5px;
width: 250px;
height: 50%;
background-color: white;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 10px;
}
.save-panel > button {
margin: 0;
}
.save-panel > hr {
width: 100%;
}
.delete-save {
width: 25px;
padding: 0;
height: 25px;
left: 5px;
top: 5px;
position: absolute;
}
.load-save {
left: 5px;
bottom: 5px;
position: absolute;
}
.save-box > img {
width: 100%;
border-radius: 5px;
}
.save-box {
position: relative;
display: flex;
border-radius: 5px;
border: 1px solid rgb(238, 238, 238);
}
.save-list {
flex: 1;
overflow-y: scroll;
overflow-x: hidden;
gap: 5px;
display: flex;
flex-direction: column;
}
.controls {
display: flex;
justify-content: space-between;
}