Skip to content

Commit 34c0d6f

Browse files
committed
feat: interactionMode + tooltip.groupingMode
1 parent c9c34eb commit 34c0d6f

File tree

18 files changed

+2745
-302
lines changed

18 files changed

+2745
-302
lines changed

examples/simple/src/components/Bubble.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default function Bubble() {
4141
data,
4242
primaryAxis,
4343
secondaryAxes,
44-
groupingMode: "single",
44+
interactionMode: "closest",
4545
getSeriesStyle: () => ({ line: { opacity: 0 } }),
4646
getDatumStyle: (datum) =>
4747
({

examples/simple/src/components/CustomStyles.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ function MyChart({
4040
activeSeriesIndex,
4141
setState,
4242
}: any) {
43-
const { data, grouping, randomizeData } = useDemoConfig({
43+
const { data, interactionMode, randomizeData } = useDemoConfig({
4444
series: 4,
45-
grouping: "primary",
45+
interactionMode: "primary",
4646
dataType: "ordinal",
47-
show: ["elementType", "grouping"],
47+
show: ["elementType", "interactionMode"],
4848
});
4949

5050
const primaryAxis = React.useMemo<
@@ -77,7 +77,7 @@ function MyChart({
7777
<Chart
7878
options={{
7979
data,
80-
groupingMode: grouping,
80+
interactionMode,
8181
primaryAxis,
8282
secondaryAxes,
8383
getDatumStyle: (datum, status) =>

examples/simple/src/components/GroupingModes.tsx renamed to examples/simple/src/components/InteractionMode.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ export default function GroupingModes() {
1010
focused: null,
1111
});
1212

13-
const { data, grouping, elementType, randomizeData, Options } = useDemoConfig(
14-
{
13+
const { data, interactionMode, elementType, randomizeData, Options } =
14+
useDemoConfig({
1515
series: 10,
16-
show: ["elementType", "grouping"],
17-
}
18-
);
16+
show: ["elementType", "interactionMode"],
17+
});
1918

2019
const primaryAxis = React.useMemo<
2120
AxisOptions<typeof data[number]["data"][number]>
@@ -49,7 +48,7 @@ export default function GroupingModes() {
4948
<Chart
5049
options={{
5150
data,
52-
groupingMode: grouping,
51+
interactionMode,
5352
primaryAxis,
5453
secondaryAxes,
5554

examples/simple/src/index.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1+
import "./styles.css";
2+
import useLagRadar from "./useLagRadar";
3+
import React from "react";
4+
import ReactDOM from "react-dom";
5+
6+
//
7+
18
import Area from "./components/Area";
29
import Band from "./components/Band";
310
import Bar from "./components/Bar";
411
import Bubble from "./components/Bubble";
512
import CustomStyles from "./components/CustomStyles";
613
import DarkMode from "./components/DarkMode";
714
import DynamicContainer from "./components/DynamicContainer";
8-
import GroupingModes from "./components/GroupingModes";
15+
import InteractionMode from "./components/InteractionMode";
916
import Line from "./components/Line";
1017
import MultipleAxes from "./components/MultipleAxes";
1118
import Steam from "./components/Steam";
1219
import BarHorizontal from "./components/BarHorizontal";
1320
import SparkChart from "./components/SparkChart";
14-
import "./styles.css";
15-
import useLagRadar from "./useLagRadar";
16-
import React from "react";
17-
import ReactDOM from "react-dom";
1821
import SyncedCursors from "./components/SyncedCursors";
1922
import StressTest from "./components/StressTest";
2023

@@ -28,7 +31,7 @@ const components = [
2831
["Steam", Steam],
2932
["Spark Chart", SparkChart],
3033
["Multiple Axes", MultipleAxes],
31-
["Grouping Modes", GroupingModes],
34+
["Interaction Modes", InteractionMode],
3235
["Dark Mode", DarkMode],
3336
["Dynamic / Overflow Container", DynamicContainer],
3437
["Custom Styles", CustomStyles],

examples/simple/src/useDemoConfig.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const options = {
1111
secondaryAxisStack: [true, false],
1212
primaryAxisShow: [true, false],
1313
secondaryAxisShow: [true, false],
14-
grouping: ["single", "series", "primary", "secondary"],
14+
interactionMode: ["primary", "closest"],
15+
tooltipGroupingMode: ["single", "primary", "secondary", "series"],
1516
tooltipAnchor: [
1617
"closest",
1718
"top",
@@ -49,7 +50,8 @@ type PrimaryAxisPosition = typeof options["primaryAxisPosition"][number];
4950
type SecondaryAxisPosition = typeof options["secondaryAxisPosition"][number];
5051
type TooltipAnchor = typeof options["tooltipAnchor"][number];
5152
type TooltipAlign = typeof options["tooltipAlign"][number];
52-
type Grouping = typeof options["grouping"][number];
53+
type InteractionMode = typeof options["interactionMode"][number];
54+
type TooltipGroupingMode = typeof options["tooltipGroupingMode"][number];
5355

5456
const optionKeys = Object.keys(options) as (keyof typeof options)[];
5557

@@ -73,7 +75,8 @@ export default function useChartConfig({
7375
secondaryAxisShow = true,
7476
tooltipAnchor = "closest",
7577
tooltipAlign = "auto",
76-
grouping = "primary",
78+
interactionMode = "primary",
79+
tooltipGroupingMode = "primary",
7780
snapCursor = true,
7881
}: {
7982
series: number;
@@ -95,7 +98,8 @@ export default function useChartConfig({
9598
secondaryAxisShow?: boolean;
9699
tooltipAnchor?: TooltipAnchor;
97100
tooltipAlign?: TooltipAlign;
98-
grouping?: Grouping;
101+
interactionMode?: InteractionMode;
102+
tooltipGroupingMode?: TooltipGroupingMode;
99103
snapCursor?: boolean;
100104
}) {
101105
const [state, setState] = React.useState({
@@ -114,7 +118,8 @@ export default function useChartConfig({
114118
secondaryAxisShow,
115119
tooltipAnchor,
116120
tooltipAlign,
117-
grouping,
121+
interactionMode,
122+
tooltipGroupingMode,
118123
snapCursor,
119124
datums,
120125
data: makeDataFrom(dataType, series, datums, useR),

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"@rollup/plugin-replace": "^2.3.3",
8080
"@svgr/rollup": "^5.4.0",
8181
"@testing-library/react": "^12.0.0",
82+
"@types/d3-delaunay": "^6.0.0",
8283
"@types/jest": "^26.0.4",
8384
"@typescript-eslint/eslint-plugin": "^4.8.1",
8485
"@typescript-eslint/parser": "^4.8.1",
@@ -131,6 +132,7 @@
131132
"@types/react": "^17.0.14",
132133
"@types/react-dom": "^17.0.9",
133134
"d3-array": "^2.12.1",
135+
"d3-delaunay": "^6.0.2",
134136
"d3-scale": "^3.3.0",
135137
"d3-shape": "^2.1.0",
136138
"d3-time": "^2.1.1",

src/components/Chart.tsx

Lines changed: 79 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { groups, sort, sum } from 'd3-array'
2-
import { stack, stackOffsetNone } from 'd3-shape'
32
import React, { ComponentPropsWithoutRef } from 'react'
43

54
import useGetLatest from '../hooks/useGetLatest'
@@ -26,13 +25,14 @@ import {
2625
materializeStyles,
2726
getSeriesStatus,
2827
getDatumStatus,
28+
sortDatums,
2929
} from '../utils/Utils'
3030
import buildAxisLinear from '../utils/buildAxis.linear'
3131
import { ChartContextProvider } from '../utils/chartContext'
3232
import AxisLinear from './AxisLinear'
3333
// import Brush from './Brush'
3434
import Cursors from './Cursors'
35-
import Tooltip from './Tooltip'
35+
import Tooltip, { defaultTooltip } from './Tooltip'
3636
import Voronoi from './Voronoi'
3737

3838
//
@@ -69,13 +69,12 @@ function defaultChartOptions<TDatum>(
6969
initialHeight: options.initialHeight ?? 200,
7070
getSeriesOrder:
7171
options.getSeriesOrder ?? ((series: Series<TDatum>[]) => series),
72-
groupingMode: options.groupingMode ?? 'primary',
72+
interactionMode: options.interactionMode ?? 'primary',
7373
showVoronoi: options.showVoronoi ?? false,
7474
defaultColors: options.defaultColors ?? defaultColorScheme,
7575
useIntersectionObserver: options.useIntersectionObserver ?? true,
7676
intersectionObserverRootMargin:
7777
options.intersectionObserverRootMargin ?? '1000px',
78-
tooltip: options.tooltip ?? true,
7978
primaryCursor: options.primaryCursor ?? true,
8079
secondaryCursor: options.secondaryCursor ?? true,
8180
padding: options.padding ?? defaultPadding,
@@ -275,8 +274,30 @@ function ChartInner<TDatum>({
275274
)
276275
}, [options.data, options.secondaryAxes, primaryAxisOptions])
277276

277+
// Resolve Tooltip Option
278+
const tooltipOptions = React.useMemo(() => {
279+
const tooltipOptions = defaultTooltip(options?.tooltip)
280+
tooltipOptions.groupingMode =
281+
tooltipOptions.groupingMode ??
282+
(() => {
283+
if (options.interactionMode === 'closest') {
284+
return 'single'
285+
}
286+
return 'primary'
287+
})()
288+
289+
return tooltipOptions
290+
}, [options.interactionMode, options?.tooltip])
291+
292+
options = {
293+
...options,
294+
tooltip: tooltipOptions,
295+
}
296+
297+
//
298+
278299
const svgRef = React.useRef<SVGSVGElement>(null)
279-
const getOptions = useGetLatest(options)
300+
const getOptions = useGetLatest({ ...options, tooltip: tooltipOptions })
280301

281302
const axisDimensionsState = React.useState<AxisDimensions>({
282303
left: {},
@@ -400,99 +421,92 @@ function ChartInner<TDatum>({
400421
}
401422
}
402423

403-
if (secondaryAxesOptions.some(axisOptions => axisOptions.stacked)) {
404-
secondaryAxesOptions
405-
.filter(d => d.stacked)
406-
.forEach(secondaryAxis => {
407-
const axisSeries = series.filter(
408-
s => s.secondaryAxisId === secondaryAxis.id
409-
)
410-
const seriesIndices = Object.keys(axisSeries)
411-
const stacker = stack()
412-
.keys(seriesIndices)
413-
.value((_, seriesIndex, index) => {
414-
const val = secondaryAxis.getValue(
415-
axisSeries[Number(seriesIndex)].datums[index].originalDatum
416-
)
417-
418-
if (typeof val === 'undefined' || val === null) {
419-
return 0
420-
}
421-
422-
return val
423-
})
424-
.offset(secondaryAxis.stackOffset ?? stackOffsetNone)
425-
426-
const stacked = stacker(
427-
Array.from({
428-
length: axisSeries.sort(
429-
(a, b) => b.datums.length - a.datums.length
430-
)[0].datums.length,
431-
})
432-
)
433-
434-
stacked.forEach((s, sIndex) => {
435-
s.forEach((datum, i) => {
436-
// @ts-ignore
437-
datum.data = axisSeries[sIndex].datums[i]
438-
439-
axisSeries[sIndex].datums[i].stackData =
440-
datum as unknown as StackDatum<TDatum>
441-
})
442-
})
443-
})
444-
}
445-
446424
return series
447-
}, [options.data, secondaryAxesOptions])
425+
}, [options.data])
426+
427+
let allDatums = React.useMemo(() => {
428+
return series.map(s => s.datums).flat(2)
429+
}, [series])
448430

449431
const primaryAxis = React.useMemo(() => {
450432
return buildAxisLinear<TDatum>(
451433
true,
452434
primaryAxisOptions,
453435
series,
436+
allDatums,
454437
gridDimensions,
455438
width,
456439
height
457440
)
458-
}, [gridDimensions, height, primaryAxisOptions, series, width])
441+
}, [allDatums, gridDimensions, height, primaryAxisOptions, series, width])
459442

460443
const secondaryAxes = React.useMemo(() => {
461444
return secondaryAxesOptions.map(secondaryAxis => {
462445
return buildAxisLinear<TDatum>(
463446
false,
464447
secondaryAxis,
465448
series,
449+
allDatums,
466450
gridDimensions,
467451
width,
468452
height
469453
)
470454
})
471-
}, [gridDimensions, height, secondaryAxesOptions, series, width])
455+
}, [allDatums, gridDimensions, height, secondaryAxesOptions, series, width])
472456

473-
const groupedDatums = React.useMemo(() => {
474-
const groupedDatums = new Map<any, Datum<TDatum>[]>()
457+
const [datumsByInteractionGroup, datumsByTooltipGroup] = React.useMemo(() => {
458+
const datumsByInteractionGroup = new Map<any, Datum<TDatum>[]>()
459+
const datumsByTooltipGroup = new Map<any, Datum<TDatum>[]>()
475460

476-
const allDatums = series.map(s => s.datums).flat(2)
461+
let getInteractionKey = (datum: Datum<TDatum>) => `${datum.primaryValue}`
462+
let getTooltipKey = (datum: Datum<TDatum>) => `${datum.primaryValue}`
463+
464+
if (options.interactionMode === 'closest') {
465+
getInteractionKey = datum =>
466+
`${datum.primaryValue}_${datum.secondaryValue}`
467+
}
468+
469+
if (tooltipOptions.groupingMode === 'single') {
470+
getTooltipKey = datum => `${datum.primaryValue}_${datum.secondaryValue}`
471+
} else if (tooltipOptions.groupingMode === 'secondary') {
472+
getTooltipKey = datum => `${datum.secondaryValue}`
473+
} else if (tooltipOptions.groupingMode === 'series') {
474+
getTooltipKey = datum => `${datum.seriesIndex}`
475+
}
477476

478477
allDatums.forEach(datum => {
479-
const primaryValue = `${primaryAxis.getValue(datum.originalDatum)}`
478+
const interactionKey = (getInteractionKey as Function)(datum)
479+
const tooltipKey = (getTooltipKey as Function)(datum)
480+
481+
if (!datumsByInteractionGroup.has(interactionKey)) {
482+
datumsByInteractionGroup.set(interactionKey, [])
483+
}
480484

481-
if (!groupedDatums.has(primaryValue)) {
482-
groupedDatums.set(primaryValue, [])
485+
if (!datumsByTooltipGroup.has(tooltipKey)) {
486+
datumsByTooltipGroup.set(tooltipKey, [])
483487
}
484488

485-
groupedDatums.get(primaryValue)!.push(datum)
489+
datumsByInteractionGroup.get(interactionKey)!.push(datum)
490+
datumsByTooltipGroup.get(tooltipKey)!.push(datum)
486491
})
487492

488-
allDatums.forEach(datum => {
489-
const primaryValue = `${primaryAxis.getValue(datum.originalDatum)}`
493+
datumsByInteractionGroup.forEach((value, key) => {
494+
datumsByInteractionGroup.set(key, sortDatums(value, secondaryAxes))
495+
})
490496

491-
datum.group = groupedDatums.get(primaryValue)
497+
datumsByTooltipGroup.forEach((value, key) => {
498+
datumsByTooltipGroup.set(key, sortDatums(value, secondaryAxes))
499+
})
500+
501+
allDatums.forEach(datum => {
502+
const interactionKey = (getInteractionKey as Function)(datum)
503+
const tooltipKey = (getTooltipKey as Function)(datum)
504+
datum.interactiveGroup = datumsByInteractionGroup.get(interactionKey)
505+
datum.tooltipGroup = datumsByTooltipGroup.get(tooltipKey)
492506
})
493507

494-
return groupedDatums
495-
}, [primaryAxis, series])
508+
return [datumsByInteractionGroup, datumsByTooltipGroup]
509+
}, [allDatums, options.interactionMode, tooltipOptions.groupingMode])
496510

497511
const getSeriesStatusStyle = React.useCallback(
498512
(series: Series<TDatum>, focusedDatum: Datum<TDatum> | null) => {
@@ -556,7 +570,8 @@ function ChartInner<TDatum>({
556570
secondaryAxes,
557571
series,
558572
orderedSeries,
559-
groupedDatums,
573+
datumsByInteractionGroup,
574+
datumsByTooltipGroup,
560575
width,
561576
height,
562577
getSeriesStatusStyle,

0 commit comments

Comments
 (0)