Click on nodes to see their individual content links, or change the behavior using the radio buttons in the bottom right.
Summary links represent connections to or from a combined node.
Define when to show content and summary links in your onCombineLinks event handler.
See also
import React, { useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { Chart } from "regraph";
import data from "./data";
import "@ci/theme/rg/css/layout.css";
export const Demo = () => <SummaryLinks items={data()} />;
function SummaryLinks(props) {
const [state, setState] = useState({
combine: { level: 1, properties: ["group"] },
autoSelectionStyle: true,
mode: "default",
clickedId: null,
layout: { name: "lens", tightness: 8 },
});
const contentLinks = useRef({});
const onCombineLinks = ({ setStyle, id, links }) => {
const { clickedId } = state;
const linkKeys = Object.keys(links);
const { mode } = state;
const { end1, end2 } = links[linkKeys[0]];
const style = {
glyphs: [
{
label: { text: `${linkKeys.length}`, color: "rgb(60,60,60)", bold: true },
color: "rgb(240,240,240)",
size: 2,
},
],
width: Math.sqrt(Object.keys(links).length) * 3,
end1,
end2,
};
if (mode === "contents") {
style.contents = true;
style.summary = false;
} else if (mode === "custom") {
const wasClicked = id === clickedId;
style.contents = wasClicked;
style.fade = wasClicked;
if (wasClicked) {
contentLinks.current = links;
}
} else if (mode === "specific") {
style.contents = {
"b-n": true,
"a-f-1": true,
"e-k-2": true,
};
style.fade = true;
}
setStyle(style);
};
const onCombineNodes = ({ setStyle, nodes }) => {
const { color } = Object.values(nodes)[0];
setStyle({
label: { text: "" },
border: { color },
});
};
const onClick = ({ id: clickedId }) => {
const { mode, combine } = state;
if (mode === "custom") {
if (contentLinks.current[clickedId]) {
// If the clicked item is a child link in a summary link
// that is currently showing its contents, then we leave
// the state as it is
} else {
contentLinks.current = {};
// If the clicked item is anything else then we update the clickedId
// The combine property is recreated as a new object in the state
// so that onCombineLinks is re-run, in order to update the summary and
// contents properties for the combo links.
setState((current) => {
return { ...current, combine: { ...combine }, clickedId };
});
}
} else {
// We keep track of the clickedId in all other modes, so that as we switch
// modes it is up to date
setState((current) => {
return { ...current, clickedId };
});
}
};
const onModeChange = (event) => {
const mode = event.target.id;
setState((current) => {
return {
...current,
mode,
combine: { ...current.combine },
autoSelectionStyle: mode === "default",
};
});
};
const { items } = props;
const { combine, autoSelectionStyle, mode: currentMode, layout } = state;
return (
<div className="story">
<div className="chart-wrapper">
<Chart
items={items}
combine={combine}
layout={layout}
options={{
combo: { autoSelectionStyle },
overview: false,
navigation: false,
}}
onCombineLinks={onCombineLinks}
onCombineNodes={onCombineNodes}
onClick={onClick}
/>
<div className="legend">
<div style={{ marginBottom: "10px" }}>Selection Style Behavior</div>
{[
{ mode: "default", label: "Default" },
{ mode: "contents", label: "Always show content links" },
{ mode: "summary", label: "Always show summary links" },
{ mode: "custom", label: "Show contents when clicking summary" },
{ mode: "specific", label: "Show specific content links" },
].map(({ mode, label }) => (
<label key={mode} htmlFor={mode} className="legend-radio">
{label}
<input
type="radio"
id={mode}
name="mode"
defaultChecked={currentMode === mode}
onChange={onModeChange}
/>
<span />
</label>
))}
</div>
</div>
</div>
);
}
const root = createRoot(document.getElementById("regraph"));
root.render(<Demo />); import { style } from "@ci/theme/rg/js/storyStyles";
function data() {
return {
a: {
data: {
group: "1",
},
...style.primary1,
},
b: {
data: {
group: "1",
},
...style.primary1,
},
c: {
data: {
group: "1",
},
...style.primary1,
},
d: {
data: {
group: "1",
},
...style.primary1,
},
e: {
data: {
group: "1",
},
...style.primary1,
},
f: {
data: {
group: "2",
},
...style.primary1,
},
g: {
data: {
group: "2",
},
...style.primary1,
},
h: {
data: {
group: "2",
},
...style.primary1,
},
i: {
data: {
group: "2",
},
...style.primary1,
},
j: {
data: {
group: "3",
},
...style.primary1,
},
k: {
data: {
group: "3",
},
...style.primary1,
},
l: {
data: {
group: "3",
},
...style.primary1,
},
m: { ...style.primary1 },
n: { ...style.primary1 },
"a-c": {
id1: "a",
id2: "c",
...style.link,
},
"a-f-1": {
id1: "a",
id2: "f",
...style.link,
},
"a-f-2": {
id1: "a",
id2: "f",
...style.link,
},
"b-c": {
id1: "b",
id2: "c",
...style.link,
},
"b-h": {
id1: "b",
id2: "h",
...style.link,
},
"b-g": {
id1: "b",
id2: "g",
...style.link,
},
"b-n": {
id1: "b",
id2: "n",
...style.link,
},
"c-d": {
id1: "c",
id2: "d",
...style.link,
},
"c-g": {
id1: "c",
id2: "g",
...style.link,
},
"c-j": {
id1: "c",
id2: "j",
...style.link,
},
"d-f": {
id1: "d",
id2: "f",
...style.link,
},
"f-d": {
id1: "f",
id2: "d",
...style.link,
},
"d-e": {
id1: "d",
id2: "e",
...style.link,
},
"e-k-1": {
id1: "e",
id2: "k",
...style.link,
},
"e-k-2": {
id1: "e",
id2: "k",
...style.link,
},
"f-g": {
id1: "f",
id2: "g",
...style.link,
},
"f-h": {
id1: "f",
id2: "h",
...style.link,
},
"f-j": {
id1: "f",
id2: "j",
...style.link,
},
"f-m": {
id1: "f",
id2: "m",
...style.link,
},
"g-h": {
id1: "g",
id2: "h",
...style.link,
},
"h-m-1": {
id1: "h",
id2: "m",
...style.link,
},
"h-m-2": {
id1: "h",
id2: "m",
...style.link,
},
"i-j": {
id1: "i",
id2: "j",
...style.link,
},
"i-l": {
id1: "i",
id2: "l",
...style.link,
},
"j-l": {
id1: "j",
id2: "l",
...style.link,
},
"k-n": {
id1: "k",
id2: "n",
...style.link,
},
"l-j": {
id1: "l",
id2: "j",
...style.link,
},
"l-n": {
id1: "l",
id2: "n",
...style.link,
},
};
}
export default data; <!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";
.legend {
font-family: sans-serif;
position: absolute;
bottom: 16px;
right: 16px;
background-color: rgba(255, 255, 255, 0.7);
border: solid 1px lightgray;
padding: 12px 12px 6px;
}
.legend-checkbox,
.legend-radio {
display: block;
position: relative;
padding-left: 35px;
padding-right: 12px;
margin-bottom: 4px;
line-height: 25px;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.legend-checkbox:hover,
.legend-radio:hover {
background-color: #eee;
}
.legend-checkbox > input,
.legend-radio > input {
position: absolute;
opacity: 0;
height: 0;
width: 0;
}
.legend-checkbox > span,
.legend-radio > span {
position: absolute;
top: 0;
left: 0;
}
.legend-checkbox > span {
height: 25px;
width: 25px;
background-color: var(--primary1);
}
.legend-radio > span {
border-color: var(--primary2);
border-width: 2px;
border-style: solid;
border-radius: 50%;
background-color: white;
height: 20px;
width: 20px;
}
.legend-checkbox > span:after,
.legend-radio > span:after {
content: "";
position: absolute;
display: none;
}
.legend-checkbox > span:after {
left: 9px;
top: 5px;
width: 5px;
height: 10px;
border-color: white;
border-style: solid;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.legend-radio > span:after {
left: 4px;
top: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
}
.legend-radio:hover > span:after {
display: block;
background-color: #eee;
}
.legend-checkbox > input:checked ~ span:after,
.legend-radio > input:checked ~ span::after {
display: block;
}
.legend-radio > input:checked ~ span::after {
background-color: var(--primary1);
}