Skip to content

Commit 9d2ba91

Browse files
Add icons to unit chart and strictValuePosition prop to bee swarm
1 parent 6576701 commit 9d2ba91

10 files changed

Lines changed: 356 additions & 226 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ stats.html
2424
.vscode/*
2525
!.vscode/extensions.json
2626
.idea
27+
.npmrc
2728
.DS_Store
2829
*.suo
2930
*.ntvs*

package-lock.json

Lines changed: 166 additions & 166 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@undp/data-viz",
3-
"version": "2.6.5",
3+
"version": "2.6.7",
44
"main": "./dist/index.cjs",
55
"module": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -467,7 +467,7 @@
467467
"peerDependencies": {
468468
"@dnd-kit/core": "^6.3.1",
469469
"@dnd-kit/modifiers": "^9.0.0",
470-
"@undp/design-system-react": "^2.2.6",
470+
"@undp/design-system-react": "^2.2.7",
471471
"dom-to-svg": "^0.12.2",
472472
"file-saver": "^2.0.5",
473473
"handlebars": "^4.7.8",

src/Components/Dashboard/GraphEl.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,7 @@ function GraphEl(props: Props) {
15031503
case 'beeSwarmChart':
15041504
return {
15051505
customLayers: settings?.customLayers,
1506+
strictValuePosition: settings?.strictValuePosition,
15061507
theme: settings?.theme,
15071508
dimmedOpacity: settings?.dimmedOpacity,
15081509
resetSelectionOnDoubleClick: settings?.resetSelectionOnDoubleClick,
@@ -1789,6 +1790,7 @@ function GraphEl(props: Props) {
17891790
case 'unitChart':
17901791
return {
17911792
naLabel: settings?.naLabel,
1793+
gridIcon: settings?.gridIcon,
17921794
numberDisplayOptions: settings?.numberDisplayOptions,
17931795
theme: settings?.theme,
17941796
totalNoOfDots: settings?.totalNoOfDots,

src/Components/Graphs/BeeSwarmChart/Graph.tsx

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ interface Props {
7272
hideAxisLine: boolean;
7373
locale: string;
7474
padZeros: boolean;
75+
strictValuePosition: boolean;
7576
}
7677

7778
export function VerticalGraph(props: Props) {
@@ -111,6 +112,7 @@ export function VerticalGraph(props: Props) {
111112
hideAxisLine,
112113
locale,
113114
padZeros,
115+
strictValuePosition,
114116
} = props;
115117
const svgRef = useRef(null);
116118
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
@@ -183,26 +185,42 @@ export function VerticalGraph(props: Props) {
183185
...(d.data && { data: { ...d.data } }),
184186
})) as BeeSwarmChartDataType[]
185187
).filter((d) => d.position);
188+
if (strictValuePosition) {
189+
dataTemp.forEach((d) => {
190+
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
191+
(d as any).fy = y(d.position as number);
192+
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
193+
(d as any).x = graphWidth / 2;
194+
});
195+
}
186196
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
187-
forceSimulation(dataTemp as any)
188-
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
189-
.force('y', forceY((d: any) => y(d.position as number)).strength(5))
197+
const simulation = forceSimulation(dataTemp as any)
190198
.force('x', forceX((_d) => graphWidth / 2).strength(1))
191199
.force(
192200
'collide',
193201
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
194202
forceCollide((d: any) => (radiusScale ? radiusScale(d.radius || 0) + 1 : radius + 1)),
195203
)
196204
.force('charge', forceManyBody().strength(-15))
197-
.alphaDecay(0.05)
205+
.alphaDecay(0.05);
206+
207+
if (!strictValuePosition) {
208+
simulation.force(
209+
'y',
210+
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
211+
forceY((d: any) => y(d.position as number)).strength(5),
212+
);
213+
}
214+
215+
simulation
198216
.tick(10000)
199217
.on('tick', () => {
200218
setFinalData(dataTemp as BeeSwarmChartDataTypeForBubbleChart[]);
201219
})
202220
.on('end', () => {
203221
setFinalData(dataTemp as BeeSwarmChartDataTypeForBubbleChart[]);
204222
});
205-
}, [radius, graphWidth, dataOrdered, y, radiusScale]);
223+
}, [radius, graphWidth, dataOrdered, y, radiusScale, strictValuePosition]);
206224

207225
return (
208226
<>
@@ -471,6 +489,7 @@ export function HorizontalGraph(props: Props) {
471489
hideAxisLine,
472490
locale,
473491
padZeros,
492+
strictValuePosition,
474493
} = props;
475494
const svgRef = useRef(null);
476495
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
@@ -543,26 +562,44 @@ export function HorizontalGraph(props: Props) {
543562
...(d.data && { data: { ...d.data } }),
544563
})) as BeeSwarmChartDataType[]
545564
).filter((d) => d.position);
565+
566+
if (strictValuePosition) {
567+
dataTemp.forEach((d) => {
568+
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
569+
(d as any).fx = x(d.position as number);
570+
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
571+
(d as any).y = graphHeight / 2;
572+
});
573+
}
574+
546575
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
547-
forceSimulation(dataTemp as any)
548-
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
549-
.force('x', forceX((d: any) => x(d.position as number)).strength(5))
576+
const simulation = forceSimulation(dataTemp as any)
550577
.force('y', forceY((_d) => graphHeight / 2).strength(1))
551578
.force(
552579
'collide',
553580
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
554581
forceCollide((d: any) => (radiusScale ? radiusScale(d.radius || 0) + 1 : radius + 1)),
555582
)
556583
.force('charge', forceManyBody().strength(-15))
557-
.alphaDecay(0.05)
584+
.alphaDecay(0.05);
585+
586+
if (!strictValuePosition) {
587+
simulation.force(
588+
'x',
589+
// biome-ignore lint/suspicious/noExplicitAny: undefined data type
590+
forceX((d: any) => x(d.position as number)).strength(5),
591+
);
592+
}
593+
594+
simulation
558595
.tick(10000)
559596
.on('tick', () => {
560597
setFinalData(dataTemp as BeeSwarmChartDataTypeForBubbleChart[]);
561598
})
562599
.on('end', () => {
563600
setFinalData(dataTemp as BeeSwarmChartDataTypeForBubbleChart[]);
564601
});
565-
}, [radius, graphHeight, dataOrdered, x, radiusScale]);
602+
}, [radius, graphHeight, dataOrdered, x, radiusScale, strictValuePosition]);
566603

567604
return (
568605
<>

src/Components/Graphs/BeeSwarmChart/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ interface Props {
9595
showNAColor?: boolean;
9696
/** Toggle visibility of axis line for the main axis */
9797
hideAxisLine?: boolean;
98+
/** Defines if teh data point are locked exactly to their data value on the axis; if false, nodes are pulled toward their value but may drift when crowded */
99+
strictValuePosition?: boolean;
98100
/** Data points to highlight. Use the label value from data to highlight the data point */
99101
highlightedDataPoints?: (string | number)[];
100102
/** Defines the opacity of the non-highlighted data */
@@ -184,6 +186,7 @@ export function BeeSwarmChart(props: Props) {
184186
dimmedOpacity = 0.3,
185187
customLayers = [],
186188
hideAxisLine = false,
189+
strictValuePosition = false,
187190
} = props;
188191
const [svgWidth, setSvgWidth] = useState(0);
189192
const [svgHeight, setSvgHeight] = useState(0);
@@ -302,6 +305,7 @@ export function BeeSwarmChart(props: Props) {
302305
suffix={numberDisplayOptions?.suffix || ''}
303306
prefix={numberDisplayOptions?.prefix || ''}
304307
precision={numberDisplayOptions?.precision ?? 2}
308+
strictValuePosition={strictValuePosition}
305309
/>
306310
) : null}
307311
</GraphArea>

src/Components/Graphs/UnitChart/index.tsx

Lines changed: 98 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,15 @@ interface Props {
6161
// Graph Parameters
6262
/** Size of the visualization */
6363
size?: number;
64-
/** No. of dots in a single row */
64+
/** No. of grid icons in a single row */
6565
gridSize?: number;
66-
/** Spacing between 2 dots */
66+
/** Defines the `d` attribute values for the SVG paths used as grid icons. Each path should be designed for a 24px × 24px viewBox. The order of paths determines the rendering order. If no paths are provided, a circle icon is used as the default. */
67+
gridIcon?: string[];
68+
/** Spacing between 2 grid icons */
6769
unitPadding?: number;
68-
/** Total no. of dot that are rendered in the chart */
70+
/** Total no. of grid icon that are rendered in the chart */
6971
totalNoOfDots?: number;
70-
/** Toggle visibility of stroke for the unfilled dots */
72+
/** Toggle visibility of stroke for the unfilled grid icons. */
7173
showStrokeForWhiteDots?: boolean;
7274
/** Toggles if the graph animates in when loaded. */
7375
animate?: boolean | AnimateDataType;
@@ -123,6 +125,7 @@ export function UnitChart(props: Props) {
123125
animate = false,
124126
naLabel = 'NA',
125127
numberDisplayOptions,
128+
gridIcon,
126129
} = props;
127130
const svgRef = useRef(null);
128131
const animateValue =
@@ -141,15 +144,19 @@ export function UnitChart(props: Props) {
141144
if (radius <= 0) {
142145
console.error(
143146
'The size of single unit is less than or equal to zero. Check values for ((dimension / gridSize) - (padding * 2)) / 2 is not less than or equal to 0.',
147+
`size: ${size}`,
148+
`gridSize: ${gridSize}`,
149+
`gridDimension: ${gridDimension}`,
150+
`unitPadding: ${unitPadding}`,
144151
);
145152
return null;
146153
}
147154

148-
const cellsData: { color: string }[] = [];
155+
const cellsData: { color: string; index: number }[] = [];
149156
data.forEach((item, index) => {
150157
const count = Math.round((item.value / totalValue) * totalNoOfDots);
151158
for (let i = 0; i < count; i += 1) {
152-
cellsData.push({ color: colors[index] });
159+
cellsData.push({ color: colors[index], index });
153160
}
154161
});
155162
return (
@@ -267,46 +274,91 @@ export function UnitChart(props: Props) {
267274
>
268275
<AnimatePresence>
269276
<g>
270-
{cellsData.map((d, i) => (
271-
<motion.circle
272-
// biome-ignore lint/suspicious/noArrayIndexKey: index is the unique identifier
273-
key={i}
274-
style={{
275-
strokeWidth: 1,
276-
}}
277-
variants={{
278-
initial: {
279-
fill: '#fff',
280-
opacity: 0,
281-
...(!showStrokeForWhiteDots ? { stroke: d.color } : {}),
282-
strokeWidth: 1,
283-
},
284-
whileInView: {
285-
fill: d.color,
286-
opacity: 1,
287-
...(!showStrokeForWhiteDots ? { stroke: d.color } : {}),
288-
strokeWidth: 1,
289-
cx: (i % gridSize) * gridDimension + gridDimension / 2,
290-
cy: Math.floor(i / gridSize) * gridDimension + gridDimension / 2,
291-
transition: {
292-
duration: 0,
293-
delay: (animateValue.duration / cellsData.length) * i,
294-
},
295-
},
296-
}}
297-
initial='initial'
298-
animate={isInView ? 'whileInView' : 'initial'}
299-
className={
300-
(d.color.toLowerCase() === '#fff' ||
301-
d.color.toLowerCase() === '#ffffff' ||
302-
d.color.toLowerCase() === 'white') &&
303-
showStrokeForWhiteDots
304-
? 'stroke-primary-gray-400 dark:stroke-primary-gray-500'
305-
: ''
306-
}
307-
r={radius}
308-
/>
309-
))}
277+
{gridIcon
278+
? cellsData.map((d, i) => (
279+
<motion.svg
280+
key={d.index}
281+
style={{
282+
strokeWidth: 1,
283+
}}
284+
viewBox='0 0 24 24'
285+
width={radius * 2}
286+
height={radius * 2}
287+
variants={{
288+
initial: {
289+
fill: '#fff',
290+
opacity: 0,
291+
...(!showStrokeForWhiteDots ? { stroke: d.color } : {}),
292+
strokeWidth: 1,
293+
},
294+
whileInView: {
295+
fill: d.color,
296+
opacity: 1,
297+
...(!showStrokeForWhiteDots ? { stroke: d.color } : {}),
298+
strokeWidth: 1,
299+
x: (i % gridSize) * gridDimension,
300+
y: Math.floor(i / gridSize) * gridDimension,
301+
transition: {
302+
duration: 0,
303+
delay: (animateValue.duration / cellsData.length) * i,
304+
},
305+
},
306+
}}
307+
initial='initial'
308+
animate={isInView ? 'whileInView' : 'initial'}
309+
className={
310+
(d.color.toLowerCase() === '#fff' ||
311+
d.color.toLowerCase() === '#ffffff' ||
312+
d.color.toLowerCase() === 'white') &&
313+
showStrokeForWhiteDots
314+
? 'stroke-primary-gray-400 dark:stroke-primary-gray-500'
315+
: ''
316+
}
317+
>
318+
{gridIcon.map((el) => (
319+
<path d={el} key={el} />
320+
))}
321+
</motion.svg>
322+
))
323+
: cellsData.map((d, i) => (
324+
<motion.circle
325+
key={d.index}
326+
style={{
327+
strokeWidth: 1,
328+
}}
329+
variants={{
330+
initial: {
331+
fill: '#fff',
332+
opacity: 0,
333+
...(!showStrokeForWhiteDots ? { stroke: d.color } : {}),
334+
strokeWidth: 1,
335+
},
336+
whileInView: {
337+
fill: d.color,
338+
opacity: 1,
339+
...(!showStrokeForWhiteDots ? { stroke: d.color } : {}),
340+
strokeWidth: 1,
341+
cx: (i % gridSize) * gridDimension + gridDimension / 2,
342+
cy: Math.floor(i / gridSize) * gridDimension + gridDimension / 2,
343+
transition: {
344+
duration: 0,
345+
delay: (animateValue.duration / cellsData.length) * i,
346+
},
347+
},
348+
}}
349+
initial='initial'
350+
animate={isInView ? 'whileInView' : 'initial'}
351+
className={
352+
(d.color.toLowerCase() === '#fff' ||
353+
d.color.toLowerCase() === '#ffffff' ||
354+
d.color.toLowerCase() === 'white') &&
355+
showStrokeForWhiteDots
356+
? 'stroke-primary-gray-400 dark:stroke-primary-gray-500'
357+
: ''
358+
}
359+
r={radius}
360+
/>
361+
))}
310362
</g>
311363
</AnimatePresence>
312364
</svg>

src/Types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,8 @@ export interface GraphSettingsDataType {
10611061
xNumberDisplayOptions?: NumberFormatOptions;
10621062
yNumberDisplayOptions?: NumberFormatOptions;
10631063
defaultColor?: string;
1064+
strictValuePosition?: boolean;
1065+
gridIcon?: string[];
10641066
}
10651067

10661068
export interface InfoBoxDataType {

0 commit comments

Comments
 (0)