Search

Save and Load

Export

Save chart changes and load them later.

Save and Load
View live example →

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

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.