@neo4j-ndl/react 3.0.17 → 3.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  *
4
4
  * Copyright (c) "Neo4j"
@@ -19,30 +19,26 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
19
19
  * You should have received a copy of the GNU General Public License
20
20
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
21
21
  */
22
- import { forwardRef, useEffect, useState, useContext, createContext, useImperativeHandle, useRef, Children, isValidElement,
23
- // memo,
24
- useMemo, } from 'react';
22
+ import React, { forwardRef, useEffect, useState, useContext, createContext, useImperativeHandle, useRef, Children, isValidElement, useMemo, useCallback, } from 'react';
25
23
  import * as d3 from 'd3';
26
- import { ChartsContext,
27
- // type ChartSize,
28
- // type Metadata,
29
- } from './Charts';
24
+ import { ChartsContext, } from './Charts';
30
25
  import { classNames } from '../_common/defaultImports';
31
- // import { TopXAxis, BottomXAxis, LeftYAxis, RightYAxis } from './Axis';
32
26
  import { Line } from './Line';
33
- // import { LineThreshold } from './LineThreshold';
34
27
  import { Dot } from './Dot';
35
- // import { da, de, he, set } from 'date-fns/locale';
36
28
  import { line } from 'd3-shape';
37
29
  import { ChartTooltip } from './ChartTooltip';
38
30
  import { useDebounceCallback, useResizeObserver } from 'usehooks-ts';
39
31
  import { useThrottle } from './utils';
32
+ // Used to visualize the voronoi polygons.
33
+ const SHOULD_SHOW_VORONOI_DEBUG = false;
34
+ // Used to throttle calls to on mouse move over the polygons.
35
+ const THROTTLE_MOUSE_MOVE_MILLISECONDS = 100;
40
36
  const defaultChartProps = {
41
37
  scales: {},
42
38
  pointSelectionType: 'single',
43
39
  };
44
- export const canvasLeftPadding = 8;
45
- export const canvasRightPadding = 8;
40
+ export const canvasLeftPadding = 0;
41
+ export const canvasRightPadding = 0;
46
42
  // eslint-disable-next-line no-redeclare
47
43
  export const ChartsChartContext = createContext({
48
44
  chartRef: null,
@@ -52,36 +48,99 @@ export const ChartsChartContext = createContext({
52
48
  linePointArray: [],
53
49
  mousePositionRef: undefined,
54
50
  });
55
- const ChartComponent = forwardRef(function ChartComponent({ children, className,
56
- // scales: textScales = defaultChartProps.scales,
57
- // hoverAreaType = defaultChartProps.hoverAreaType,
58
- scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChartProps.pointSelectionType, }, ref) {
59
- // console.info('Chart.tsx - ChartComponent - initiation');
51
+ export function getPointPixels(linePoint, scales) {
52
+ const { accessorXAxis, scaleXAxisId } = linePoint;
53
+ const { accessorYAxis, scaleYAxisId } = linePoint;
54
+ const valueX = linePoint[accessorXAxis];
55
+ const valueY = linePoint[accessorYAxis];
56
+ const scaleXAxisObject = scales[scaleXAxisId];
57
+ const scaleYAxisObject = scales[scaleYAxisId];
58
+ const scaleXAxis = scaleXAxisObject.functionWithContentWidthAsRange;
59
+ const scaleYAxis = scaleYAxisObject.functionWithContentHeightAsRange;
60
+ const pointPixelX = scaleXAxis(valueX);
61
+ const pointPixelY = scaleYAxis(valueY);
62
+ return { pointPixelX, pointPixelY };
63
+ }
64
+ function dotsPropsAreEqual(prevProps, nextProps) {
65
+ const isEqual = JSON.stringify(prevProps.selectedPoints) ==
66
+ JSON.stringify(nextProps.selectedPoints) &&
67
+ JSON.stringify(prevProps.metadata) == JSON.stringify(nextProps.metadata) &&
68
+ prevProps.selectedPointRef.current.dataId ===
69
+ nextProps.selectedPointRef.current.dataId;
70
+ return isEqual;
71
+ }
72
+ const DotsMemoized = React.memo(function Dots({ selectedPoints, metadata, selectedPointRef, tooltipAnchorRefCallback, scales, }) {
73
+ console.info('DotsMemoized >> render');
74
+ return (_jsx(_Fragment, { children: selectedPoints.map((linePoint) => {
75
+ const { key, dataId, pointIndex } = linePoint;
76
+ const { pointPixelX, pointPixelY } = getPointPixels(linePoint, scales);
77
+ const { isVisible, color } = metadata[dataId];
78
+ if (!isVisible)
79
+ return false;
80
+ const shouldAttachTooltipAnchorRef = dataId === selectedPointRef.current.dataId &&
81
+ pointIndex === selectedPointRef.current.pointIndex;
82
+ return (_jsx(Dot, { ref: shouldAttachTooltipAnchorRef ? tooltipAnchorRefCallback : null, x: pointPixelX, y: pointPixelY, color: color }, `chart-dot-${key}`));
83
+ }) }));
84
+ }, dotsPropsAreEqual);
85
+ function tooltipPropsAreEqual(prevProps, nextProps) {
86
+ console.info(prevProps, nextProps);
87
+ const isEqual = JSON.stringify(prevProps.selectedPoints) ==
88
+ JSON.stringify(nextProps.selectedPoints) &&
89
+ JSON.stringify(prevProps.metadata) == JSON.stringify(nextProps.metadata) &&
90
+ prevProps.tooltipAnchorRef.current === nextProps.tooltipAnchorRef.current;
91
+ console.info('tooltip >>>>>>>>>>>>>>>>>>>', { isEqual });
92
+ return isEqual;
93
+ }
94
+ const TooltipMemoized = React.memo(function Tooltip({ selectedPoints, metadata, tooltipAnchorRef, }) {
60
95
  var _a, _b;
61
- // useEffect(() => {
62
- // console.info('component rerendered');
63
- // });
96
+ console.info('Tooltip >> render', tooltipAnchorRef);
97
+ return (_jsxs(ChartTooltip, { anchorRef: tooltipAnchorRef, isOpen: true, children: [_jsx(ChartTooltip.Title, { children: String((_b = selectedPoints[0][(_a = selectedPoints[0]) === null || _a === void 0 ? void 0 : _a.accessorXAxis]) !== null && _b !== void 0 ? _b : '') }), selectedPoints.map((linePoint) => {
98
+ var _a;
99
+ if (linePoint === undefined)
100
+ return null;
101
+ if (metadata[linePoint.dataId].isVisible === false)
102
+ return null;
103
+ const { key, dataId, accessorYAxis } = linePoint;
104
+ const contentKey = `chart-tooltip-content-${key}`;
105
+ console.info(contentKey);
106
+ return (_jsx(ChartTooltip.Content, { leftElement: metadata[dataId].label, rightElement: ((_a = linePoint[accessorYAxis]) === null || _a === void 0 ? void 0 : _a.toString()) || '', indentSquareColor: metadata[dataId].color }, contentKey));
107
+ })] }));
108
+ }, tooltipPropsAreEqual);
109
+ const ChartComponent = forwardRef(function ChartComponent({ children, className, scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChartProps.pointSelectionType, }, ref) {
110
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
111
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
112
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
113
+ console.info('ChartComponent');
64
114
  const chartsContextValue = useContext(ChartsContext);
65
115
  const { data, metadata } = chartsContextValue;
66
116
  const chartRef = useRef(null);
67
117
  const contentRef = useRef(null);
68
- const tooltipAnchorRef = useRef(null);
118
+ // const tooltipAnchorRef = useRef<SVGSVGElement | null>(null);
119
+ const [tooltipAnchorRef, setTooltipAnchorRef] = useState(null);
120
+ const tooltipAnchorRefCallback = useCallback((node) => {
121
+ console.info('useCallback - dep: - set: setTooltipAnchorRef', node);
122
+ setTooltipAnchorRef({
123
+ current: node,
124
+ });
125
+ }, []);
69
126
  const horizontalIntersectionLineRef = useRef(null);
70
127
  const mousePositionRef = useRef({ x: 0, y: 0 });
128
+ const [draggingPosition, setDraggingPosition] = useState(undefined);
129
+ const [isMouseDown, setIsMouseDown] = useState(false);
71
130
  const [hasLines, setHasLines] = useState(false);
72
131
  const [contentSize, setContentSize] = useState(undefined);
73
132
  // The svg paths calculated for the voronoi polygons, includes
74
133
  // the assigned line point for the section for the onHover event.
75
- const [voroniPolygonPaths, setVoroniPolygonPaths] = useState();
76
- const [linePointArray, setLinePointArray] = useState([]);
77
- // The point where the tooltip will be anchored.
134
+ const [voronoiPolygons, setVoronoiPolygons] = useState();
78
135
  const selectedPointRef = useRef({});
79
136
  // The points that will display a thumb and be included in the tooltip.
80
- const [selectedPoints, setSelectedPoints] = useState([]);
81
- // useMemo:initialScales
137
+ const [selectedPoints, setSelectedPoints] = useState(null);
138
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
139
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
140
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
82
141
  const initialScales = useMemo(() => {
83
142
  // Map the string scales to actual d3 function scales.
84
- //Will only re-run if the scalesProp changes.
143
+ // Will only re-run if the scalesProp changes.
85
144
  const initialScales = {};
86
145
  Object.entries(scalesProp).forEach(([key, value]) => {
87
146
  switch (value.type) {
@@ -125,38 +184,32 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
125
184
  return initialScales;
126
185
  }, [scalesProp]);
127
186
  const [scales, setScales] = useState(initialScales);
128
- // Used to visualize the voronoi polygons.
129
- const shouldShowVoronoiDebug = true;
130
- const pointPixelAxis = pointSelectionType === 'x-axis' || pointSelectionType === 'single'
131
- ? 'pointPixelX'
132
- : 'pointPixelY';
133
- // useEffect:contentSize
187
+ const voronoiScale = useMemo(() => {
188
+ const initialVoronoiScale = {
189
+ originalDomain: null,
190
+ functionWithContentWidthAsRange: d3.scaleLinear(),
191
+ functionWithContentHeightAsRange: d3.scaleLinear(),
192
+ function: d3.scaleLinear(),
193
+ };
194
+ return initialVoronoiScale;
195
+ }, []);
134
196
  useEffect(() => {
135
197
  // Needs to run after first render to determine size of content container.
136
- // console.info('Chart.tsx - ChartComponent - useEffect - contentSize');
137
198
  if (!contentRef.current)
138
199
  return;
139
- // console.info(
140
- // 'Chart.tsx - ChartComponent - useEffect - contentSize (past guard)',
141
- // );
142
200
  const contentElement = contentRef.current;
143
201
  if (contentElement && contentElement.getBoundingClientRect) {
144
202
  const newContentSize = {
145
203
  width: contentElement.getBoundingClientRect().width,
146
204
  height: contentElement.getBoundingClientRect().height,
147
205
  };
148
- // console.info(
149
- // 'Chart.tsx - ChartComponent - useEffect - setting contentSize',
150
- // );
151
206
  // Will force a re-render, calculations require
152
207
  // width and height of the content container.
153
208
  setContentSize(newContentSize);
154
209
  }
155
210
  }, [contentRef]);
156
211
  const onResize = useDebounceCallback(() => {
157
- // console.info(
158
- // 'Chart.tsx - ChartComponent - useResizeObserver/useDebounceCallback - contentSize',
159
- // );
212
+ console.info('useDebounceCallback - set: contentSize');
160
213
  // Timeout to ensure all rendering is done before we get the size.
161
214
  const contentElement = contentRef.current;
162
215
  if (contentElement && contentElement.getBoundingClientRect()) {
@@ -166,9 +219,6 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
166
219
  canvasRightPadding,
167
220
  height: contentElement.getBoundingClientRect().height,
168
221
  };
169
- // console.info(
170
- // 'Chart.tsx - ChartComponent - useResizeObserver/useDebounceCallback - setting contentSize',
171
- // );
172
222
  setContentSize(newContentSize);
173
223
  }
174
224
  }, 200);
@@ -177,28 +227,18 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
177
227
  onResize: onResize,
178
228
  });
179
229
  useImperativeHandle(ref, () => {
180
- // console.info('Chart.tsx - ChartComponent - useImperativeHandle - ref');
181
230
  const svgChart = chartRef.current;
182
231
  if (!svgChart) {
183
232
  return null;
184
233
  }
185
234
  return svgChart;
186
235
  });
187
- // useEffect:line
188
- // TODO: We don't need to run this after a render, we want this to run when we
189
- // have the contentRef ref and contentSize state set. This will force a re-render
190
- // when setting the states at the end.
191
- useEffect(() => {
192
- // console.info('Chart.tsx - ChartComponent - useEffect - line');
236
+ // The initial line point array is the representation of the full data
237
+ // that came into the component via the prop.
238
+ // useMemo:initialLinePointArray
239
+ const { initialLinePointArray, lineMap } = useMemo(() => {
193
240
  if (!data)
194
- return;
195
- if (!contentRef.current)
196
- return;
197
- const width = contentSize === null || contentSize === void 0 ? void 0 : contentSize.width;
198
- const height = contentSize === null || contentSize === void 0 ? void 0 : contentSize.height;
199
- if (!width || !height)
200
- return;
201
- // console.info('Chart.tsx - useEffect - line (past guard)');
241
+ return { initialLinePointArray: [], lineMap: {} };
202
242
  // This use effect is specific for Line components.
203
243
  // If there are no line components then we don't need to do anything.
204
244
  const arrayChildren = Children.toArray(children);
@@ -206,20 +246,18 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
206
246
  if (!hasLines)
207
247
  return;
208
248
  setHasLines(true);
249
+ const lineMap = {};
250
+ // Used by react for the key properties.
251
+ let key = 0;
209
252
  // Here we are figuring out the min and max values for all the scales.
210
253
  // To do this we need to iterate through all of the lines and assign
211
254
  // each lines designated scale to a min max object specific for that scale.
212
- const tempLinePointArray = [];
213
- let newScales = Object.assign({}, scales);
255
+ const newInitialLinePointArray = [];
214
256
  arrayChildren.forEach((child) => {
215
- var _a, _b, _c, _d;
216
257
  // Per line.
217
258
  if (!isValidElement(child) || child.type !== Line)
218
259
  return;
219
- const { scaleXAxis: scaleXAxisId, scaleYAxis: scaleYAxisId, accessorXAxis: initialAccessorXAxis, accessorYAxis: initialAccessorYAxis, dataId, seriesInterval, seriesIntervalStartValue, } = child.props;
220
- // If it's not visible, don't include it.
221
- if (metadata[dataId].isVisible === false)
222
- return;
260
+ const { scaleXAxis: scaleXAxisId, scaleYAxis: scaleYAxisId, accessorXAxis: initialAccessorXAxis, accessorYAxis: initialAccessorYAxis, accessorHollowDot, dataId, seriesInterval, seriesIntervalStartValue, } = child.props;
223
261
  // Need to set these values for series type data.
224
262
  const accessorXAxis = seriesInterval
225
263
  ? 'xAxisValue'
@@ -238,17 +276,20 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
238
276
  const seriesLineData = lineData;
239
277
  singleLinePointArray = seriesLineData.data.map((dataPoint, index) => {
240
278
  const newLinePoint = {
279
+ key,
241
280
  dataId,
242
281
  pointIndex: index,
243
282
  dataIndex: lineDataIndex,
244
283
  accessorXAxis,
245
284
  accessorYAxis,
285
+ accessorHollowDot,
246
286
  xAxisValue: dataPoint !== undefined && dataPoint !== null
247
287
  ? seriesIntervalStartValue + seriesInterval * index
248
288
  : null,
249
289
  yAxisValue: dataPoint,
250
290
  scaleXAxisId,
251
291
  scaleYAxisId,
292
+ selectionGroup: null,
252
293
  };
253
294
  return newLinePoint;
254
295
  });
@@ -258,13 +299,16 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
258
299
  const nonSeriesData = lineData;
259
300
  singleLinePointArray = nonSeriesData.data.map((dataPointArray, index) => {
260
301
  const newLinePoint = {
302
+ key,
261
303
  dataId,
262
304
  pointIndex: index,
263
305
  dataIndex: lineDataIndex,
264
306
  accessorXAxis,
265
307
  accessorYAxis,
308
+ accessorHollowDot,
266
309
  scaleXAxisId,
267
310
  scaleYAxisId,
311
+ selectionGroup: null,
268
312
  };
269
313
  dataPointArray.forEach((dataPoint, index) => {
270
314
  newLinePoint[nonSeriesData.metadata[index]] = dataPoint;
@@ -272,19 +316,45 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
272
316
  return newLinePoint;
273
317
  });
274
318
  }
319
+ key += 1;
275
320
  // Add this single line to the array that holds all of the lines.
276
- tempLinePointArray.push(...singleLinePointArray);
321
+ newInitialLinePointArray.push(...singleLinePointArray);
322
+ // Line map for easier access to points from the same line. No need
323
+ // to iterate through the line point array.
324
+ // console.info('setting lineMap', lineMap);
325
+ lineMap[dataId] = singleLinePointArray;
326
+ });
327
+ const initialLinePointArray = newInitialLinePointArray;
328
+ return { initialLinePointArray, lineMap };
329
+ }, [data, children]) || {};
330
+ useEffect(() => {
331
+ if (!contentRef.current)
332
+ return;
333
+ const width = contentSize === null || contentSize === void 0 ? void 0 : contentSize.width;
334
+ const height = contentSize === null || contentSize === void 0 ? void 0 : contentSize.height;
335
+ if (!width || !height)
336
+ return;
337
+ if (initialLinePointArray === undefined)
338
+ return;
339
+ // Need to reset the originalDomain for each scale.
340
+ let newScales = {};
341
+ Object.entries(scales).forEach(([key, scale]) => {
342
+ newScales[key] = Object.assign(Object.assign({}, scale), { originalDomain: null });
343
+ });
344
+ initialLinePointArray.forEach((linePoint) => {
345
+ var _a, _b;
346
+ const { accessorXAxis, accessorYAxis, scaleXAxisId, scaleYAxisId } = linePoint;
277
347
  // Get min and max values for both x and y axis from the data point array.
278
- const minValueXAxis = d3.min(tempLinePointArray,
348
+ const minValueXAxis = d3.min(initialLinePointArray,
279
349
  // @ts-expect-error d3 types are not correct
280
350
  (linePoint) => linePoint[accessorXAxis]);
281
- const maxValueXAxis = d3.max(tempLinePointArray,
351
+ const maxValueXAxis = d3.max(initialLinePointArray,
282
352
  // @ts-expect-error d3 types are not correct
283
353
  (linePoint) => linePoint[accessorXAxis]);
284
- const minValueYAxis = d3.min(tempLinePointArray,
354
+ const minValueYAxis = d3.min(initialLinePointArray,
285
355
  // @ts-expect-error d3 types are not correct
286
356
  (linePoint) => linePoint[accessorYAxis]);
287
- const maxValueYAxis = d3.max(tempLinePointArray,
357
+ const maxValueYAxis = d3.max(initialLinePointArray,
288
358
  // @ts-expect-error d3 types are not correct
289
359
  (linePoint) => linePoint[accessorYAxis]);
290
360
  const scaleXAxis = newScales[scaleXAxisId];
@@ -297,24 +367,36 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
297
367
  let scaleYAxisMin = undefined;
298
368
  let scaleYAxisMax = undefined;
299
369
  if (scalesProp[scaleXAxisId].domain !== undefined) {
370
+ // The domain value is set manually.
300
371
  scaleXAxisMin = scalesProp[scaleXAxisId].domain[0];
301
372
  scaleXAxisMax = scalesProp[scaleXAxisId].domain[1];
302
373
  }
303
- else {
374
+ else if (scaleXAxis !== undefined &&
375
+ (scaleXAxis === null || scaleXAxis === void 0 ? void 0 : scaleXAxis.originalDomain) !== null) {
304
376
  //@ts-expect-error d3 types are not correct - it can handle undefined
305
- scaleXAxisMin = d3.min([(_a = scaleXAxis.originalDomain) === null || _a === void 0 ? void 0 : _a[0], minValueXAxis]);
377
+ scaleXAxisMin = d3.min([scaleXAxis.originalDomain[0], minValueXAxis]);
306
378
  //@ts-expect-error d3 types are not correct - it can handle undefined
307
- scaleXAxisMax = d3.max([(_b = scaleXAxis.originalDomain) === null || _b === void 0 ? void 0 : _b[1], maxValueXAxis]);
379
+ scaleXAxisMax = d3.max([scaleXAxis.originalDomain[1], maxValueXAxis]);
380
+ }
381
+ else {
382
+ scaleXAxisMin = minValueXAxis;
383
+ scaleXAxisMax = maxValueXAxis;
308
384
  }
309
385
  if (scalesProp[scaleYAxisId].domain !== undefined) {
386
+ // The domain value is set manually.
310
387
  scaleYAxisMin = scalesProp[scaleYAxisId].domain[0];
311
388
  scaleYAxisMax = scalesProp[scaleYAxisId].domain[1];
312
389
  }
313
- else {
390
+ else if (scaleYAxis !== undefined &&
391
+ scaleXAxis.originalDomain !== null) {
314
392
  //@ts-expect-error d3 types are not correct - it can handle undefined
315
- scaleYAxisMin = d3.min([(_c = scaleYAxis.originalDomain) === null || _c === void 0 ? void 0 : _c[0], minValueYAxis]);
393
+ scaleYAxisMin = d3.min([(_a = scaleYAxis.originalDomain) === null || _a === void 0 ? void 0 : _a[0], minValueYAxis]);
316
394
  //@ts-expect-error d3 types are not correct - it can handle undefined
317
- scaleYAxisMax = d3.max([(_d = scaleYAxis.originalDomain) === null || _d === void 0 ? void 0 : _d[1], maxValueYAxis]);
395
+ scaleYAxisMax = d3.max([(_b = scaleYAxis.originalDomain) === null || _b === void 0 ? void 0 : _b[1], maxValueYAxis]);
396
+ }
397
+ else {
398
+ scaleYAxisMin = minValueYAxis;
399
+ scaleYAxisMax = maxValueYAxis;
318
400
  }
319
401
  // D3 types are not correct it will be the same type as the original domain.
320
402
  // casting it to [number, number] to avoid type errors.
@@ -337,85 +419,265 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
337
419
  // Update the specific scale.
338
420
  newScales = Object.assign(Object.assign({}, newScales), { [scaleXAxisId]: scaleXAxis, [scaleYAxisId]: scaleYAxis });
339
421
  });
340
- // The points must be created after the line points have been created.
341
- // This is because the points are based on the scalers created from the line points.
342
- // The min max values are constantly updated. So looping twice is inevitable.
343
- const newLinePointArray = [];
344
- const delaunayPointArray = [];
345
- tempLinePointArray.forEach((linePoint) => {
346
- const { accessorXAxis, accessorYAxis, scaleXAxisId, scaleYAxisId } = linePoint;
422
+ voronoiScale.function.domain([0, width !== null && width !== void 0 ? width : 0]);
423
+ voronoiScale.originalDomain = [0, width !== null && width !== void 0 ? width : 0];
424
+ voronoiScale.functionWithContentWidthAsRange.domain([0, width !== null && width !== void 0 ? width : 0]);
425
+ voronoiScale.functionWithContentWidthAsRange.range([0, width !== null && width !== void 0 ? width : 0]);
426
+ setScales(newScales);
427
+ }, [contentSize, scalesProp, children, initialLinePointArray]);
428
+ const { linePointArray, pointSelectionGroups } = useMemo(() => {
429
+ if (initialLinePointArray === undefined)
430
+ return {};
431
+ // The following is only used for pointSelectionType x-axis and y-axis
432
+ const pointSelectionGroups = {};
433
+ const linePointArray = initialLinePointArray.map((linePoint) => {
434
+ const { pointPixelX, pointPixelY } = getPointPixels(linePoint, scales);
435
+ const pointPixelAxis = pointSelectionType === 'x-axis' || pointSelectionType === 'single'
436
+ ? pointPixelX
437
+ : pointPixelY;
438
+ if (pointPixelAxis === undefined)
439
+ return linePoint;
440
+ linePoint.selectionGroup = pointPixelAxis;
441
+ if (!pointSelectionGroups[pointPixelAxis]) {
442
+ pointSelectionGroups[pointPixelAxis] = [];
443
+ pointSelectionGroups[pointPixelAxis].push(linePoint);
444
+ }
445
+ else {
446
+ // Pixel point already exists.
447
+ pointSelectionGroups[pointPixelAxis].push(linePoint);
448
+ }
449
+ return linePoint;
450
+ });
451
+ return { linePointArray, pointSelectionGroups };
452
+ }, [pointSelectionType, initialLinePointArray, scales]);
453
+ const voronoiPolygonPointsArray = useMemo(() => {
454
+ const width = contentSize === null || contentSize === void 0 ? void 0 : contentSize.width;
455
+ const height = contentSize === null || contentSize === void 0 ? void 0 : contentSize.height;
456
+ if (!width || !height)
457
+ return;
458
+ if (lineMap === undefined)
459
+ return;
460
+ if (linePointArray === undefined)
461
+ return;
462
+ const visibleLinePointArray = linePointArray.filter((linePoint) => {
463
+ return metadata[linePoint.dataId].isVisible;
464
+ });
465
+ const delaunay = d3.Delaunay.from(visibleLinePointArray,
466
+ // @ts-expect-error - d3 types are not correct, it can handle null being returned.
467
+ (linePoint) => {
468
+ const { accessorXAxis, scaleXAxisId } = linePoint;
347
469
  const valueX = linePoint[accessorXAxis];
348
- const valueY = linePoint[accessorYAxis];
349
- const scaleXAxisObject = newScales[scaleXAxisId];
350
- const scaleYAxisObject = newScales[scaleYAxisId];
470
+ const scaleXAxisObject = scales[scaleXAxisId];
351
471
  const scaleXAxis = scaleXAxisObject.functionWithContentWidthAsRange;
472
+ const scaleXAxisCopy = scaleXAxis.copy();
473
+ scaleXAxisCopy.domain(scaleXAxisObject.originalDomain);
474
+ const pointPixelX = scaleXAxisCopy(valueX);
475
+ // We must return null otherwise some points won't work with voronoi/tooltips.
476
+ return pointPixelX === undefined ? null : pointPixelX;
477
+ }, (linePoint) => {
478
+ const { accessorYAxis, scaleYAxisId } = linePoint;
479
+ const valueY = linePoint[accessorYAxis];
480
+ const scaleYAxisObject = scales[scaleYAxisId];
352
481
  const scaleYAxis = scaleYAxisObject.functionWithContentHeightAsRange;
353
- const pointPixelX = scaleXAxis(valueX);
482
+ const scaleYAxisCopy = scaleYAxis.copy();
483
+ scaleYAxisCopy.domain(scaleYAxisObject.originalDomain);
354
484
  const pointPixelY = scaleYAxis(valueY);
355
- const newLinePoint = Object.assign(Object.assign({}, linePoint), { pointPixelX,
356
- pointPixelY });
357
- newLinePointArray.push(newLinePoint);
358
- // We diss line points that have null values as they don't require
359
- // an on hover event and therfor polygon paths.
360
- if (valueX === null || valueY === null)
361
- return;
362
- if (pointPixelX === null || pointPixelY === null)
363
- return;
364
- // Calculating the voroni polygons requires knowing the exact
365
- // pixel positions of all the line points.
366
- delaunayPointArray.push([pointPixelX, pointPixelY, newLinePoint]);
485
+ // We must return null otherwise some points won't work with voronoi/tooltips.
486
+ return pointPixelY === undefined ? null : pointPixelY;
367
487
  });
368
- // We have now created the line points and scales.
369
- // From those we can now calculate the on hover voroni polygons.
370
- // This must also be calculated after all updates as the polygon
371
- // paths will change depending on the line points available.
372
- const delaunayPointArrayLike = Object.assign(Object.assign({}, delaunayPointArray), { length: delaunayPointArray.length });
373
- const delaunay = d3.Delaunay.from(delaunayPointArrayLike);
374
488
  const voronoi = delaunay.voronoi([0, 0, width, height]);
375
489
  const voronoiPolygons = voronoi.cellPolygons();
376
490
  const voronoiPolygonPointsArray = Array.from(voronoiPolygons);
491
+ return voronoiPolygonPointsArray;
492
+ }, [metadata, linePointArray]);
493
+ useEffect(() => {
494
+ if (isMouseDown)
495
+ return;
496
+ if (voronoiPolygonPointsArray === undefined)
497
+ return;
498
+ if (linePointArray === undefined)
499
+ return;
500
+ const visibleLinePointArray = linePointArray.filter((linePoint) => {
501
+ return metadata[linePoint.dataId].isVisible;
502
+ });
377
503
  const delaunayLineFn = line();
378
- const voroniPolygonPaths = voronoiPolygonPointsArray.map((voronoiPolygonPoints, index) => {
379
- const path = delaunayLineFn(voronoiPolygonPoints);
380
- const linePoint = delaunayPointArray[index][2];
381
- return { path, linePoint };
504
+ const newVoronoiPolygons = voronoiPolygonPointsArray.map((voronoiPolygonPoints) => {
505
+ const newVoronoiPolygonPoints = voronoiPolygonPoints.map((voronoiPolygonPoint) => {
506
+ const [x, y] = voronoiPolygonPoint;
507
+ const scale = voronoiScale.functionWithContentWidthAsRange;
508
+ const newX = scale(x);
509
+ const newVoronoiPolygonPoint = [newX, y];
510
+ return newVoronoiPolygonPoint;
511
+ });
512
+ newVoronoiPolygonPoints.index = voronoiPolygonPoints.index;
513
+ const { index } = newVoronoiPolygonPoints;
514
+ const path = delaunayLineFn(newVoronoiPolygonPoints);
515
+ const linePoint = visibleLinePointArray[index];
516
+ const voronoiIndex = index;
517
+ return {
518
+ path,
519
+ linePoint,
520
+ voronoiIndex,
521
+ };
382
522
  });
383
- setVoroniPolygonPaths(voroniPolygonPaths);
384
- setLinePointArray(newLinePointArray);
385
- setScales(newScales);
386
- // eslint-disable-next-line react-hooks/exhaustive-deps
387
- }, [contentSize, scalesProp, children, data, metadata]);
388
- const handleMouseMove = (event, linePoint) => {
523
+ setVoronoiPolygons(newVoronoiPolygons);
524
+ }, [voronoiPolygonPointsArray, linePointArray, scales, isMouseDown]);
525
+ const handleMouseMove = (event, voronoiPolygon) => {
526
+ const { linePoint } = voronoiPolygon;
389
527
  const [xm, ym] = d3.pointer(event, contentRef.current);
390
528
  const mousePixelX = xm;
391
529
  const mousePixelY = ym;
392
530
  mousePositionRef.current.x = mousePixelX;
393
531
  mousePositionRef.current.y = mousePixelY;
394
- const { pointPixelX, pointPixelY, pointIndex, dataId } = linePoint;
532
+ // Zooming
533
+ if (isMouseDown) {
534
+ // We care about zooming when dragging because we want to fill space
535
+ // showing what is being selected.
536
+ const newDraggingPosition = Object.assign(Object.assign({}, draggingPosition), { endX: mousePixelX });
537
+ setDraggingPosition(newDraggingPosition);
538
+ return;
539
+ }
540
+ const { pointPixelX, pointPixelY } = getPointPixels(linePoint, scales);
541
+ if (pointPixelX === undefined || pointPixelY === undefined)
542
+ return;
543
+ // Line point
544
+ const { pointIndex, dataId } = linePoint;
395
545
  const distance = Math.hypot(pointPixelX - mousePixelX, pointPixelY - mousePixelY);
396
546
  const radius = 32;
397
- const shouldTrigger = distance < radius;
547
+ const shouldTrigger = distance < radius && pointSelectionGroups !== undefined;
398
548
  if (shouldTrigger) {
399
- // console.info('trigger');
400
- const pointPixelAxisValue = linePoint[pointPixelAxis];
401
- const newSelectedPoints = pointSelectionGroups[pointPixelAxisValue];
402
- if (newSelectedPoints !== selectedPoints)
403
- // console.info('set selected points');
549
+ const selectionGroup = linePoint.selectionGroup;
550
+ const hasSelectionGroup = selectionGroup !== null;
551
+ if (!hasSelectionGroup) {
552
+ console.warn('No selection group for point');
553
+ return;
554
+ }
555
+ const newSelectedPoints = pointSelectionGroups[selectionGroup];
556
+ const isNewSelectedPoints = newSelectedPoints !== selectedPoints;
557
+ if (isNewSelectedPoints) {
404
558
  selectedPointRef.current = { dataId, pointIndex };
559
+ }
405
560
  setSelectedPoints(newSelectedPoints);
406
561
  }
407
- else if (selectedPoints.length > 0) {
408
- // console.info('unset selected points');
562
+ else if (selectedPoints !== null && selectedPoints.length > 0) {
409
563
  selectedPointRef.current = {};
410
- setSelectedPoints([]);
564
+ setSelectedPoints(null);
411
565
  }
412
566
  };
413
- const throttledHandleMouseMove = useThrottle(handleMouseMove, 100);
567
+ const throttledHandleMouseMove = useThrottle(handleMouseMove, THROTTLE_MOUSE_MOVE_MILLISECONDS);
414
568
  const handleMouseLeave = () => {
569
+ if (isMouseDown) {
570
+ return;
571
+ }
572
+ setIsMouseDown(false);
573
+ setDraggingPosition(undefined);
415
574
  // Mouse moves outside of the content area.
416
- if (selectedPoints.length > 0)
417
- setSelectedPoints([]);
575
+ if (selectedPoints !== null && selectedPoints.length > 0)
576
+ setSelectedPoints(null);
418
577
  };
578
+ const handleMouseDown = (event) => {
579
+ const amountOfMouseClicks = event.detail;
580
+ const isDoubleClick = amountOfMouseClicks === 2;
581
+ if (isDoubleClick &&
582
+ contentSize !== undefined &&
583
+ contentSize.width !== undefined) {
584
+ // If zoomed in, return to the original view.
585
+ Object.entries(scales).forEach(([scaleId, scale]) => {
586
+ if (scale.originalDomain === null)
587
+ return;
588
+ const newScale = Object.assign({}, scale);
589
+ newScale.function.domain(scale.originalDomain);
590
+ newScale.functionWithContentWidthAsRange.domain(scale.originalDomain);
591
+ scales[scaleId] = newScale;
592
+ });
593
+ voronoiScale.function.domain([0, contentSize.width]);
594
+ voronoiScale.functionWithContentWidthAsRange.domain([
595
+ 0,
596
+ contentSize.width,
597
+ ]);
598
+ }
599
+ // Set start position for dragging.
600
+ const [xm, ym] = d3.pointer(event, contentRef.current);
601
+ const mousePixelX = xm;
602
+ const mousePixelY = ym;
603
+ mousePositionRef.current.x = mousePixelX;
604
+ mousePositionRef.current.y = mousePixelY;
605
+ // Initial dragging position.
606
+ const newDraggingPosition = {
607
+ startX: mousePixelX,
608
+ endX: undefined,
609
+ };
610
+ // Needed for the dragging motion.
611
+ setIsMouseDown(true);
612
+ setDraggingPosition(newDraggingPosition);
613
+ };
614
+ // must attach mouse listener to window
615
+ const handleMouseUp = () => {
616
+ const [xm] = d3.pointer(event, contentRef.current);
617
+ const mousePixelX = xm;
618
+ if ((draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX) === mousePixelX) {
619
+ // The user clicked on the same place, no zooming.
620
+ setIsMouseDown(false);
621
+ setDraggingPosition(undefined);
622
+ return;
623
+ }
624
+ const leftX = (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX) > (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.endX)
625
+ ? draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.endX
626
+ : draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX;
627
+ const rightX = (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX) > (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.endX)
628
+ ? draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX
629
+ : draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.endX;
630
+ Object.entries(scales).forEach(([scaleId, scale]) => {
631
+ const newScale = Object.assign({}, scale);
632
+ newScale.function.domain([
633
+ newScale.function.invert(leftX),
634
+ newScale.function.invert(rightX),
635
+ ]);
636
+ newScale.functionWithContentWidthAsRange.domain([
637
+ newScale.functionWithContentWidthAsRange.invert(leftX),
638
+ newScale.functionWithContentWidthAsRange.invert(rightX),
639
+ ]);
640
+ scales[scaleId] = newScale;
641
+ });
642
+ // update voronoi scale
643
+ voronoiScale.function.domain([
644
+ voronoiScale.function.invert(leftX),
645
+ voronoiScale.function.invert(rightX),
646
+ ]);
647
+ voronoiScale.functionWithContentWidthAsRange.domain([
648
+ voronoiScale.functionWithContentWidthAsRange.invert(leftX),
649
+ voronoiScale.functionWithContentWidthAsRange.invert(rightX),
650
+ ]);
651
+ setIsMouseDown(false);
652
+ setDraggingPosition(undefined);
653
+ };
654
+ const isCanvasComponent = (child) => {
655
+ return isValidElement(child) && child.type === Line;
656
+ };
657
+ const width = (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.endX) && (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX)
658
+ ? draggingPosition.endX > draggingPosition.startX
659
+ ? draggingPosition.endX - draggingPosition.startX
660
+ : draggingPosition.startX - draggingPosition.endX
661
+ : 1;
662
+ const leftX = (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.endX) && (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX)
663
+ ? (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX) > (draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.endX)
664
+ ? draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.endX
665
+ : draggingPosition === null || draggingPosition === void 0 ? void 0 : draggingPosition.startX
666
+ : undefined;
667
+ const shouldRenderDots = hasLines &&
668
+ contentSize !== undefined &&
669
+ selectedPoints !== null &&
670
+ selectedPoints.length > 0 &&
671
+ metadata !== undefined;
672
+ const shouldRenderTooltip = hasLines &&
673
+ contentSize !== undefined &&
674
+ selectedPoints !== null &&
675
+ selectedPoints.length > 0 &&
676
+ metadata !== undefined &&
677
+ tooltipAnchorRef !== null &&
678
+ tooltipAnchorRef.current !== null;
679
+ if (linePointArray === undefined)
680
+ return null;
419
681
  const chartContextValue = {
420
682
  chartRef,
421
683
  contentRef,
@@ -425,90 +687,42 @@ scales: scalesProp = defaultChartProps.scales, pointSelectionType = defaultChart
425
687
  mousePositionRef,
426
688
  };
427
689
  const chartClasses = classNames(`ndl-charts-chart`, className);
428
- const contentClasses = classNames(`ndl-charts-chart-content`, className);
429
- // useMemo:pointSelectionGroups
430
- const pointSelectionGroups = useMemo(() => {
431
- // console.info('Chart.tsx - ChartComponent - useMemo - pointSelectionGroups');
432
- // The following is only used for pointSelectionType x-axis and y-axis
433
- const pointSelectionGroups = {};
434
- for (let i = 0; i < linePointArray.length; i += 1) {
435
- const linePoint = linePointArray[i];
436
- const pointPixelAxis = pointSelectionType === 'x-axis' || pointSelectionType === 'single'
437
- ? 'pointPixelX'
438
- : 'pointPixelY';
439
- const pointPixelAxisValue = linePoint[pointPixelAxis];
440
- if (pointPixelAxisValue === undefined)
441
- continue;
442
- if (!pointSelectionGroups[pointPixelAxisValue]) {
443
- pointSelectionGroups[pointPixelAxisValue] = [];
444
- pointSelectionGroups[pointPixelAxisValue].push(linePoint);
445
- }
446
- else {
447
- // Pixel point already exists.
448
- pointSelectionGroups[pointPixelAxisValue].push(linePoint);
449
- }
450
- }
451
- return pointSelectionGroups;
452
- }, [pointSelectionType, linePointArray]);
453
- const isCanvasComponent = (child) => {
454
- return isValidElement(child) && child.type === Line;
455
- };
456
- return (_jsx(ChartsChartContext.Provider, { value: chartContextValue, children: _jsxs("div", { ref: chartRef, className: chartClasses, onMouseLeave: handleMouseLeave, children: [_jsxs("svg", { ref: contentRef, className: contentClasses, children: [_jsxs("g", { children: [Children.toArray(children).map((child) => {
690
+ const contentClasses = classNames(`ndl-charts-chart-content`);
691
+ const zoomingClasses = classNames(`ndl-charts-chart-zoom`);
692
+ return (_jsx(ChartsChartContext.Provider, { value: chartContextValue, children: _jsxs("div", { ref: chartRef, className: chartClasses, onMouseLeave: handleMouseLeave, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, children: [_jsxs("svg", { ref: contentRef, className: contentClasses, children: [_jsxs("g", { children: [draggingPosition && contentSize && (
693
+ // The zooming rectangle visualizing the selected area.
694
+ _jsx("g", { children: _jsx("rect", { className: zoomingClasses, x: leftX, width: width, height: contentSize.height }) })), Children.toArray(children).map((child) => {
695
+ // Render the lines.
457
696
  return isCanvasComponent(child) ? child : null;
458
697
  }), hasLines &&
459
698
  contentSize &&
460
699
  selectedPoints &&
461
700
  selectedPoints.length > 0 && (
462
701
  // Dotted line for x-axis selection.
463
- _jsx("line", { ref: horizontalIntersectionLineRef, x1: selectedPoints[0].pointPixelX, x2: selectedPoints[0].pointPixelX, y1: "0", y2: contentSize.height, style: {
702
+ _jsx("line", { ref: horizontalIntersectionLineRef, x1: getPointPixels(selectedPoints[0], scales).pointPixelX, x2: getPointPixels(selectedPoints[0], scales).pointPixelX, y1: "0", y2: contentSize.height, style: {
464
703
  strokeDasharray: '5,7',
465
704
  stroke: 'rgb(111, 117, 126)',
466
- } })), hasLines &&
467
- contentSize &&
468
- selectedPoints &&
469
- selectedPoints.length > 0 &&
470
- // The point thumb for x-axis selection.
471
- selectedPoints.map((linePoint, index) => {
472
- const { dataId, pointIndex, pointPixelX, pointPixelY } = linePoint;
473
- const { isVisible, color } = metadata[dataId];
474
- if (isVisible) {
475
- // We want to attach the tooltip anchor to the correctly selected point.
476
- const { dataId: selectedPointRefDataId, pointIndex: selectedPointRefPointIndex, } = selectedPointRef.current;
477
- const shouldAttachTooltipAnchorRef = dataId === selectedPointRefDataId &&
478
- pointIndex === selectedPointRefPointIndex;
479
- return (_jsx(Dot, { ref: shouldAttachTooltipAnchorRef ? tooltipAnchorRef : null, x: pointPixelX, y: pointPixelY, color: color }, index));
480
- }
481
- return false;
482
- })] }), _jsx("g", { children: hasLines &&
483
- voroniPolygonPaths &&
484
- voroniPolygonPaths.map((voroniPolygonPath, index) => {
485
- const { path, linePoint } = voroniPolygonPath;
486
- return (_jsx("path", { d: path, className: "action-voronoi", onMouseMove: (event) => {
487
- throttledHandleMouseMove(event, linePoint);
705
+ } })), shouldRenderDots && (_jsx(DotsMemoized
706
+ // Dots together with tooltip to be rendered
707
+ , {
708
+ // Dots together with tooltip to be rendered
709
+ selectedPoints: selectedPoints, metadata: metadata, selectedPointRef: selectedPointRef, tooltipAnchorRefCallback: tooltipAnchorRefCallback, scales: scales }))] }), _jsx("g", { children: hasLines &&
710
+ voronoiPolygons &&
711
+ voronoiPolygons.map((voronoiPolygon, index) => {
712
+ // Hover area for each line point.
713
+ const { path } = voronoiPolygon;
714
+ return (_jsx("path", { d: path, onMouseMove: (event) => {
715
+ throttledHandleMouseMove(event, voronoiPolygon);
488
716
  }, style: {
489
717
  strokeWidth: 1,
490
718
  stroke: 'black',
491
719
  strokeOpacity: 0.5,
492
720
  fill: '#fff',
493
- opacity: shouldShowVoronoiDebug ? 0.5 : 0,
721
+ opacity: SHOULD_SHOW_VORONOI_DEBUG ? 0.5 : 0,
494
722
  } }, index));
495
- }) })] }), _jsx(ChartTooltip
496
- //@ts-expect-error SVG element ref will work in this case.
497
- , {
498
- //@ts-expect-error SVG element ref will work in this case.
499
- anchorRef: tooltipAnchorRef, isOpen: Boolean(hasLines &&
500
- contentSize &&
501
- selectedPoints &&
502
- selectedPoints.length > 0), children: hasLines &&
503
- contentSize &&
504
- selectedPoints &&
505
- selectedPoints.length > 0 && (_jsxs(_Fragment, { children: [_jsx(ChartTooltip.Title, { children: String((_b = selectedPoints[0][(_a = selectedPoints[0]) === null || _a === void 0 ? void 0 : _a.accessorXAxis]) !== null && _b !== void 0 ? _b : '') }), selectedPoints.map((linePoint) => {
506
- var _a;
507
- if (linePoint === undefined)
508
- return null;
509
- const { dataId, accessorYAxis } = linePoint;
510
- return (_jsx(ChartTooltip.Content, { leftElement: chartsContextValue.metadata[dataId].label, rightElement: ((_a = linePoint[accessorYAxis]) === null || _a === void 0 ? void 0 : _a.toString()) || '', indentSquareColor: chartsContextValue.metadata[dataId].color }, dataId));
511
- })] })) })] }) }));
723
+ }) })] }), shouldRenderTooltip && (
724
+ // Tooltip together with the dots to be rendered
725
+ _jsx(TooltipMemoized, { selectedPoints: selectedPoints, metadata: metadata, tooltipAnchorRef: tooltipAnchorRef }))] }) }));
512
726
  });
513
727
  // Issue with TypeScript forwardRef and subcomponents: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34757#issuecomment-894053907
514
728
  const Chart = Object.assign(ChartComponent, {