Skip to content

Commit ff78651

Browse files
committed
[GraphCaption] node Size caption
copied from retina relates #21
1 parent 131dd7a commit ff78651

File tree

5 files changed

+193
-0
lines changed

5 files changed

+193
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { FC, useCallback, useEffect, useState } from "react";
2+
import { GraphCaptionProps } from ".";
3+
import { Size } from "../../core/appearance/types";
4+
import { shortenNumber } from "../GraphFilters/utils";
5+
import { useSigmaAtom, useVisualGetters } from "../../core/context/dataContexts";
6+
7+
const NodeSizeCaption: FC<
8+
Pick<GraphCaptionProps, "minimal"> & {
9+
nodesSize: Size;
10+
range?: { min: number; max: number };
11+
}
12+
> = ({ minimal, nodesSize, range }) => {
13+
const sigma = useSigmaAtom();
14+
// update nodeSize Size to camera updates from Sigma
15+
const { getNodeSize } = useVisualGetters();
16+
17+
const [nodeSizeState, setNodeSizeState] = useState<
18+
| {
19+
minValue: number;
20+
minRadius: number;
21+
maxValue: number;
22+
maxRadius: number;
23+
}
24+
| undefined
25+
>(undefined);
26+
27+
const refreshState = useCallback(() => {
28+
if (!sigma || !nodesSize.field || !range || !getNodeSize || !range) return null;
29+
30+
setNodeSizeState({
31+
minValue: range.min,
32+
minRadius: sigma.scaleSize(getNodeSize({ [nodesSize.field]: range.min })),
33+
maxValue: range.max,
34+
maxRadius: sigma.scaleSize(getNodeSize({ [nodesSize.field]: range.max })),
35+
});
36+
}, [getNodeSize, sigma, nodesSize.field, range]);
37+
38+
// Refresh caption when metric changes:
39+
useEffect(() => {
40+
refreshState();
41+
}, [refreshState]);
42+
43+
// Refresh caption on camera update:
44+
useEffect(() => {
45+
sigma.getCamera().addListener("updated", refreshState);
46+
return () => {
47+
sigma.getCamera().removeListener("updated", refreshState);
48+
};
49+
}, [sigma, refreshState]);
50+
51+
if (nodesSize.field) {
52+
return (
53+
<div className="graph-caption-item">
54+
<h4 className="fs-6">{nodesSize.field}:</h4>
55+
{nodeSizeState && (
56+
<div className="node-sizes">
57+
<div>
58+
<div className="circle-wrapper">
59+
<div
60+
className="dotted-circle"
61+
style={{ width: nodeSizeState?.minRadius * 2, height: nodeSizeState?.minRadius * 2 }}
62+
/>
63+
</div>
64+
<div className="caption text-center">{shortenNumber(nodeSizeState?.minValue)}</div>
65+
</div>
66+
<div className="ms-2">
67+
<div className="circle-wrapper">
68+
<div
69+
className="dotted-circle"
70+
style={{ width: nodeSizeState?.maxRadius * 2, height: nodeSizeState?.maxRadius * 2 }}
71+
/>
72+
</div>
73+
<div className="caption text-center">{shortenNumber(nodeSizeState?.maxValue)}</div>
74+
</div>
75+
</div>
76+
)}
77+
</div>
78+
);
79+
} else return null;
80+
};
81+
82+
export default NodeSizeCaption;

src/components/GraphCaption/index.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { FC, useMemo } from "react";
2+
3+
import { useAppearance, useFilteredGraph, useGraphDataset, useSigmaAtom } from "../../core/context/dataContexts";
4+
import NodeSizeCaption from "./NodeSizeCaption";
5+
import { fromPairs, mapValues } from "lodash";
6+
import { DatalessGraph, ItemData } from "../../core/graph/types";
7+
8+
export interface GraphCaptionProps {
9+
minimal?: boolean;
10+
}
11+
12+
const getAttributeRanges = (graph: DatalessGraph, itemData: Record<string, ItemData>, fields: string[]) => {
13+
return graph.nodes().reduce((acc, n) => {
14+
return mapValues(acc, (value, field) => {
15+
const fieldValue = itemData[n][field];
16+
if (fieldValue && (typeof fieldValue === "number" || !isNaN(+fieldValue)))
17+
return { min: Math.min(value.min, +fieldValue), max: Math.max(value.max, +fieldValue) };
18+
return value;
19+
});
20+
}, fromPairs(fields.map((f) => [f, { min: Infinity, max: -Infinity }])));
21+
};
22+
23+
const GraphCaption: FC<GraphCaptionProps> = ({ minimal }) => {
24+
const appearance = useAppearance();
25+
const filteredGraph = useFilteredGraph();
26+
const { nodeData, edgeData } = useGraphDataset();
27+
28+
// min-max values for ranking caption items
29+
const rankingDataRanges = useMemo(() => {
30+
const ranges: Record<"node" | "edge", Record<string, { min: number; max: number }>> = { node: {}, edge: {} };
31+
// should we iterate on nodes
32+
const nodeRankingFields = [appearance.nodesColor, appearance.nodesSize]
33+
.map((appearanceSpec) => {
34+
if (appearanceSpec.type === "ranking") {
35+
return appearanceSpec.field;
36+
}
37+
return null;
38+
})
39+
.filter((f): f is string => f !== null);
40+
41+
if (nodeRankingFields.length > 0) {
42+
ranges.node = getAttributeRanges(filteredGraph, nodeData, nodeRankingFields);
43+
}
44+
// should we iterate on edges
45+
const edgeRankingFields = [appearance.edgesColor, appearance.edgesSize]
46+
.map((appearanceSpec) => {
47+
if (appearanceSpec.type === "ranking") {
48+
return appearanceSpec.field;
49+
}
50+
return null;
51+
})
52+
.filter((f): f is string => f !== null);
53+
54+
if (edgeRankingFields.length > 0) {
55+
ranges.edge = getAttributeRanges(filteredGraph, edgeData, edgeRankingFields);
56+
}
57+
58+
return ranges;
59+
}, [appearance, filteredGraph, nodeData, edgeData]);
60+
61+
//sigma.getNodeDisplayData(id).size
62+
63+
return (
64+
<div title="caption" className="graph-caption">
65+
<NodeSizeCaption
66+
minimal={minimal}
67+
nodesSize={appearance.nodesSize}
68+
range={
69+
appearance.nodesSize.field !== undefined ? rankingDataRanges.node[appearance.nodesSize.field] : undefined
70+
}
71+
/>
72+
</div>
73+
);
74+
};
75+
76+
export default GraphCaption;

src/styles/_graph-caption.scss

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.graph-caption {
2+
// z-index: $zindex-caption;
3+
4+
.node-sizes {
5+
display: flex;
6+
flex-direction: row;
7+
align-items: flex-end;
8+
}
9+
10+
.circle-wrapper {
11+
height: 50px;
12+
overflow: hidden;
13+
display: flex;
14+
align-items: center;
15+
min-width: 30px;
16+
justify-content: center;
17+
}
18+
19+
.dotted-circle {
20+
border-radius: 100%;
21+
background: #cccccc66;
22+
border: 2px dotted black;
23+
}
24+
}

src/styles/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,4 @@
6060
@import "filters";
6161
@import "slider";
6262
@import "highlighjs";
63+
@import "graph-caption";

src/views/graphPage/GraphRendering.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MarqueeController } from "./controllers/MarqueeController";
1515
import { EventsController } from "./controllers/EventsController";
1616
import NodeProgramBorder from "../../utils/bordered-node-program";
1717
import { resetCamera } from "../../core/sigma";
18+
import GraphCaption from "../../components/GraphCaption";
1819

1920
function useFullScreen(): { toggle: () => void; isFullScreen: boolean } {
2021
const [isFullScreen, setFullScreen] = useState<boolean>(false);
@@ -86,6 +87,14 @@ const InteractionsController: FC = () => {
8687
);
8788
};
8889

90+
const GraphCaptionLayer: FC = () => {
91+
return (
92+
<div className="position-absolute" style={{ left: 10, bottom: 10 }}>
93+
<GraphCaption minimal />
94+
</div>
95+
);
96+
};
97+
8998
export const GraphRendering: FC = () => {
9099
const sigmaGraph = useSigmaGraph();
91100
const { hoveredNode, hoveredEdge } = useSigmaState();
@@ -127,6 +136,7 @@ export const GraphRendering: FC = () => {
127136
</div>
128137
</SigmaContainer>
129138
<InteractionsController />
139+
<GraphCaptionLayer />
130140
</>
131141
);
132142
};

0 commit comments

Comments
 (0)