Click on nodes to bring up a donut menu.
Click a donut segment to trigger an action.
You can update any item style property in the onItemInteraction event handler.
See also
import React, { useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import cloneDeep from "lodash/cloneDeep";
import has from "lodash/has";
import { Chart } from "regraph";
import { data, donutGlyphs, donutMenuItems, segmentColors } from "./data";
import { style } from "@ci/theme/rg/js/storyStyles";
import "@fortawesome/fontawesome-free/css/fontawesome.css";
import "@fortawesome/fontawesome-free/css/solid.css";
function isNode(item) {
return !has(item, "id1") || !has(item, "id2");
}
function DonutMenu() {
const [items, setItems] = useState(data);
const [nextIdToAdd, setNextIdToAdd] = useState(11);
const [animation, setAnimation] = useState({ time: 250 });
const chartRef = useRef(null);
// gets the style of hovered donut segments.
// hover feedback for interactive elements helps users expect an action
const getHoverStyle = (item, subItem) => {
if (isNode(item) && subItem != null && (subItem.type === "donut" || subItem.type === "glyph")) {
// the subitem can also be a glyph, but these have the same index
const { index } = subItem;
const highlightedDonut = cloneDeep(donutMenuItems);
// update hovered segment's color
highlightedDonut.segments[index].color = segmentColors[index].hover;
return { donut: highlightedDonut, glyphs: donutGlyphs };
}
return {};
};
// gat the donut styles for selected nodes
const getSelectionStyle = (item) => {
if (isNode(item)) {
return {
donut: donutMenuItems,
glyphs: donutGlyphs,
};
}
return {};
};
const onItemInteractionHandler = ({ hovered, selected, id, item, setStyle, subItem }) => {
let newStyle = {};
if (selected) {
newStyle = getSelectionStyle(item);
}
if (hovered) {
newStyle = { ...newStyle, ...getHoverStyle(item, subItem) };
}
// apply the new styles to the selected/hovered node
setStyle({ [id]: newStyle });
};
const addNode = (id) => {
setAnimation({ time: 750 });
setItems((current) => {
// add a new node and link to the selected node
const newId = `node${nextIdToAdd}`;
setNextIdToAdd(nextIdToAdd + 1);
return {
...current,
[newId]: { ...style.primary0 },
[`link${newId}`]: { id1: id, id2: newId, ...style.link },
};
});
};
const removeNode = (id) => {
setAnimation({ time: 750 });
setItems((current) => {
// destructure the current node and keep the rest
const { [id]: removed, ...rest } = current;
return rest;
});
};
const toggleAlert = (id) => {
setAnimation({ time: 250 });
setItems((current) => {
// toggle the warning font icon
let newStyle = {};
if (current[id].fontIcon) {
newStyle = {
...current[id],
fontIcon: null,
};
} else {
newStyle = {
...current[id],
fontIcon: { text: "fas fa-exclamation-triangle", color: style.colors.offWhite },
};
}
return {
...current,
[id]: newStyle,
};
});
};
// actions for the menu options
const actions = [addNode, removeNode, toggleAlert];
const onClickHandler = ({ subItem, id }) => {
// call the function associated with the donut segment
// we made sure the index of glyphs match the donut segment they are over in the data
if (subItem != null && (subItem.type === "donut" || subItem.type === "glyph")) {
actions[subItem.index](id);
}
};
return (
<Chart
animation={animation}
items={items}
ref={chartRef}
onClick={onClickHandler}
onItemInteraction={onItemInteractionHandler}
options={{
hoverDelay: 0,
selection: { color: style.colors.transparent },
iconFontFamily: "Font Awesome 5 Free",
imageAlignment: { "fas fa-exclamation-triangle": { size: 0.8, dy: -10 } },
navigation: false,
overview: false,
}}
/>
);
}
const FontReadyChart = React.lazy(() =>
document.fonts.load("900 24px 'Font Awesome 5 Free'").then(() => ({
default: DonutMenu,
}))
);
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";
export const data = {
node1: { ...style.cat04 },
node2: { ...style.cat02 },
node3: { ...style.cat02 },
node4: { ...style.cat02 },
node5: { ...style.cat00 },
node6: { ...style.cat00 },
node7: { ...style.cat00 },
node8: { ...style.cat00 },
node9: { ...style.cat00 },
node10: { ...style.cat00 },
link1: {
id1: "node1",
id2: "node2",
...style.link,
},
link2: {
id1: "node1",
id2: "node3",
...style.link,
},
link3: {
id1: "node1",
id2: "node4",
...style.link,
},
link4: {
id1: "node2",
id2: "node5",
...style.link,
},
link5: {
id1: "node2",
id2: "node6",
...style.link,
},
link6: {
id1: "node3",
id2: "node7",
...style.link,
},
link7: {
id1: "node3",
id2: "node8",
...style.link,
},
link8: {
id1: "node4",
id2: "node9",
...style.link,
},
link9: {
id1: "node4",
id2: "node10",
...style.link,
},
};
export const segmentColors = [
{ base: style.cat11.color, hover: style.colors.transgradient11 },
{ base: style.cat03.color, hover: style.colors.transgradient03 },
{ base: style.cat07.color, hover: style.colors.transgradient07 },
];
export const donutMenuItems = {
width: 20,
border: { color: style.colors.offWhite, width: 1 },
segments: [
{ color: segmentColors[0].base, size: 1 },
{ color: segmentColors[1].base, size: 1 },
{ color: segmentColors[2].base, size: 1 },
],
};
export const donutGlyphs = [
{
angle: 60,
radius: 37,
color: style.colors.transparent,
fontIcon: { text: "fas fa-plus", color: style.colors.offWhite },
},
{
angle: 180,
radius: 37,
color: style.colors.transparent,
fontIcon: { text: "fas fa-trash-alt", color: style.colors.offWhite },
},
{
angle: 300,
radius: 37,
color: style.colors.transparent,
fontIcon: { text: "fas fa-exclamation-triangle", color: style.colors.offWhite },
},
]; <!doctype html>
<html>
<body>
<div id="regraph" style="height: 100vh"></div>
<script type="module" src="./code.jsx"></script>
</body>
</html>