@automattic/charts 1.4.3 → 1.5.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Display charts within Automattic products.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
6
6
  "bugs": {
@@ -83,9 +83,9 @@
83
83
  "@visx/vendor": "^3.12.0",
84
84
  "@visx/xychart": "^3.12.0",
85
85
  "@wordpress/i18n": "^6.0.0",
86
- "@wordpress/icons": "^12.0.0",
86
+ "@wordpress/icons": "^13.0.0",
87
87
  "@wordpress/theme": "0.13.0",
88
- "@wordpress/ui": "0.11.0",
88
+ "@wordpress/ui": "0.13.0",
89
89
  "clsx": "2.1.1",
90
90
  "date-fns": "^4.1.0",
91
91
  "deepmerge": "4.3.1",
@@ -35,6 +35,7 @@ import { SingleChartContext, type SingleChartRef } from '../private/single-chart
35
35
  import { SvgEmptyState } from '../private/svg-empty-state';
36
36
  import { getCurveType, getFormatter, guessOptimalNumTicks } from '../private/time-axis';
37
37
  import { withResponsive } from '../private/with-responsive';
38
+ import { useXZoom, ZoomResetButton, ZoomSelectionRect } from '../private/x-zoom';
38
39
  import styles from './area-chart.module.scss';
39
40
  import { AreaChartScalesRef, HoverGlyphs, validateData } from './private';
40
41
  import type { AreaChartProps } from './types';
@@ -68,6 +69,8 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
68
69
  onPointerUp,
69
70
  onPointerMove,
70
71
  onPointerOut,
72
+ zoomable = false,
73
+ rescaleYOnLegendToggle = true,
71
74
  children,
72
75
  gridVisibility,
73
76
  gap = 'md',
@@ -86,6 +89,12 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
86
89
  const [ isNavigating, setIsNavigating ] = useState( false );
87
90
  const internalChartRef = useRef< SingleChartRef >( null );
88
91
 
92
+ const zoom = useXZoom< Date >( {
93
+ enabled: zoomable,
94
+ chartRef: internalChartRef,
95
+ userHandlers: { onPointerDown, onPointerMove, onPointerUp },
96
+ } );
97
+
89
98
  const { legendChildren, nonLegendChildren } = useChartChildren( children, 'AreaChart' );
90
99
  const [ measuredChartHeight, setMeasuredChartHeight ] = useState< number | undefined >();
91
100
 
@@ -136,12 +145,14 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
136
145
  } );
137
146
 
138
147
  // Computed from the full data set (ignoring legend visibility) so the y-axis stays
139
- // fixed when series are toggled off otherwise visx auto-fits to the remaining data
140
- // and the chart appears to rescale. Skipped for non-default stack offsets, which
141
- // reshape the y-extent (`expand` [0,1], `wiggle`/`silhouette` → centred around
142
- // zero) letting visx derive the domain is correct there.
148
+ // fixed when series are toggled off - otherwise visx auto-fits to the remaining
149
+ // data and the chart's baseline appears to move. Opt-in via
150
+ // `rescaleYOnLegendToggle={ false }`. Skipped for non-default stack offsets,
151
+ // which reshape the y-extent (`expand` -> [0,1], `wiggle`/`silhouette` -> centred
152
+ // around zero); letting visx derive the domain is correct there.
143
153
  const fixedYDomain = useMemo< [ number, number ] | undefined >( () => {
144
154
  if (
155
+ rescaleYOnLegendToggle ||
145
156
  ! legendInteractive ||
146
157
  ! dataSorted.length ||
147
158
  ! dataSorted[ 0 ].data.length ||
@@ -184,7 +195,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
184
195
  }
185
196
  if ( max === -Infinity ) return undefined;
186
197
  return [ Math.min( 0, min ), max ];
187
- }, [ dataSorted, stacked, stackOffset, legendInteractive ] );
198
+ }, [ dataSorted, stacked, stackOffset, legendInteractive, rescaleYOnLegendToggle ] );
188
199
 
189
200
  const chartOptions = useMemo( () => {
190
201
  const formatter = options?.axis?.x?.tickFormat || getFormatter( dataSorted );
@@ -209,6 +220,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
209
220
  xScale: {
210
221
  type: 'time' as const,
211
222
  ...options?.xScale,
223
+ ...( zoom.domain ? { domain: zoom.domain } : {} ),
212
224
  },
213
225
  yScale: {
214
226
  type: 'linear' as const,
@@ -219,7 +231,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
219
231
  ...options?.yScale,
220
232
  },
221
233
  };
222
- }, [ options, dataSorted, width, stacked, fixedYDomain ] );
234
+ }, [ options, dataSorted, width, stacked, fixedYDomain, zoom.domain ] );
223
235
 
224
236
  const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme );
225
237
 
@@ -378,7 +390,8 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
378
390
  onBlur={ onChartBlur }
379
391
  >
380
392
  { chartHeight > 0 && (
381
- <div ref={ chartRef }>
393
+ <div ref={ chartRef } style={ { position: 'relative' } }>
394
+ { zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
382
395
  <XYChart
383
396
  theme={ theme }
384
397
  width={ width }
@@ -386,9 +399,9 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
386
399
  margin={ { ...defaultMargin, ...margin } }
387
400
  xScale={ chartOptions.xScale }
388
401
  yScale={ chartOptions.yScale }
389
- onPointerDown={ onPointerDown }
390
- onPointerUp={ onPointerUp }
391
- onPointerMove={ onPointerMove }
402
+ onPointerDown={ zoom.handlers.onPointerDown }
403
+ onPointerUp={ zoom.handlers.onPointerUp }
404
+ onPointerMove={ zoom.handlers.onPointerMove }
392
405
  onPointerOut={ onPointerOut }
393
406
  pointerEventsDataKey="nearest"
394
407
  >
@@ -456,6 +469,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
456
469
  height={ height || chartHeight }
457
470
  margin={ margin }
458
471
  />
472
+ { zoomable && <ZoomSelectionRect drag={ zoom.drag } /> }
459
473
  </XYChart>
460
474
  </div>
461
475
  ) }
@@ -240,7 +240,7 @@ describe( 'AreaChart', () => {
240
240
  expect( keyboardCall![ 0 ].tooltipData?.nearestDatum?.key ).not.toBe( 'Series A' );
241
241
  } );
242
242
 
243
- test( 'y-axis domain stays fixed across legend toggles', async () => {
243
+ test( 'y-axis rescales across legend toggles by default', async () => {
244
244
  const user = userEvent.setup();
245
245
  const ref = createRef< SingleChartRef >();
246
246
  render(
@@ -248,7 +248,7 @@ describe( 'AreaChart', () => {
248
248
  <AreaChartUnresponsive
249
249
  { ...defaultProps }
250
250
  showLegend
251
- chartId="test-interactive-domain"
251
+ chartId="test-interactive-domain-rescale"
252
252
  legend={ { interactive: true } }
253
253
  ref={ ref }
254
254
  />
@@ -262,13 +262,82 @@ describe( 'AreaChart', () => {
262
262
 
263
263
  await user.click( screen.getByText( 'Series A' ) );
264
264
 
265
+ const afterToggleDomain = (
266
+ ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
267
+ )?.domain();
268
+ // Hiding Series A drops the upper bound; visx should refit.
269
+ expect( afterToggleDomain ).toBeDefined();
270
+ expect( afterToggleDomain![ 1 ] ).toBeLessThan( initialDomain![ 1 ] );
271
+ } );
272
+
273
+ test( 'y-axis stays pinned for unstacked area when rescaleYOnLegendToggle is false', async () => {
274
+ // Exercises the non-stacked branch of fixedYDomain, which scans the
275
+ // raw min/max across all series rather than summing stack columns.
276
+ const user = userEvent.setup();
277
+ const ref = createRef< SingleChartRef >();
278
+ render(
279
+ <GlobalChartsProvider>
280
+ <AreaChartUnresponsive
281
+ { ...defaultProps }
282
+ showLegend
283
+ chartId="test-interactive-domain-pin-unstacked"
284
+ legend={ { interactive: true } }
285
+ stacked={ false }
286
+ rescaleYOnLegendToggle={ false }
287
+ ref={ ref }
288
+ />
289
+ </GlobalChartsProvider>
290
+ );
291
+
292
+ const initialDomain = (
293
+ ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
294
+ )?.domain();
295
+ expect( initialDomain ).toBeDefined();
296
+ // defaultProps has values up to 20; pinned-unstacked should cover the max.
297
+ expect( initialDomain![ 1 ] ).toBeGreaterThanOrEqual( 20 );
298
+
299
+ await user.click( screen.getByText( 'Series A' ) );
300
+
301
+ const afterToggleDomain = (
302
+ ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
303
+ )?.domain();
304
+ expect( afterToggleDomain ).toEqual( initialDomain );
305
+ } );
306
+
307
+ test( 'y-axis stays pinned when rescaleYOnLegendToggle is false', async () => {
308
+ const user = userEvent.setup();
309
+ const ref = createRef< SingleChartRef >();
310
+ render(
311
+ <GlobalChartsProvider>
312
+ <AreaChartUnresponsive
313
+ { ...defaultProps }
314
+ showLegend
315
+ chartId="test-interactive-domain-pin"
316
+ legend={ { interactive: true } }
317
+ rescaleYOnLegendToggle={ false }
318
+ ref={ ref }
319
+ />
320
+ </GlobalChartsProvider>
321
+ );
322
+
323
+ const initialDomain = (
324
+ ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
325
+ )?.domain();
326
+ expect( initialDomain ).toBeDefined();
327
+
328
+ await user.click( screen.getByText( 'Series A' ) );
329
+
265
330
  const afterToggleDomain = (
266
331
  ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
267
332
  )?.domain();
268
333
  expect( afterToggleDomain ).toEqual( initialDomain );
269
334
  } );
270
335
 
271
- test( 'supports negative stacked values without clipping', () => {
336
+ test( 'supports negative stacked values without clipping (with pinned Y)', () => {
337
+ // The mixed-sign full-extent pin only kicks in when the consumer
338
+ // opts into pinned-Y behavior; visx's natural domain derivation for
339
+ // a `stackOffset: 'none'` stack does not extend below zero for
340
+ // purely-negative series, which is what this test guards against.
272
341
  const ref = createRef< SingleChartRef >();
273
342
  render(
274
343
  <GlobalChartsProvider>
@@ -278,6 +347,7 @@ describe( 'AreaChart', () => {
278
347
  chartId="test-interactive-negative"
279
348
  showLegend
280
349
  legend={ { interactive: true } }
350
+ rescaleYOnLegendToggle={ false }
281
351
  data={ [
282
352
  {
283
353
  label: 'Pos',
@@ -47,5 +47,20 @@ export interface AreaChartProps extends BaseChartProps< SeriesData[] > {
47
47
  * @default false when stacked, true when overlapping
48
48
  */
49
49
  withStroke?: boolean;
50
+ /**
51
+ * Enable drag-to-zoom on the X axis. The user drags horizontally to
52
+ * select a range; the X axis rescales to that range. A small reset
53
+ * button appears in the top-right of the chart while zoomed.
54
+ */
55
+ zoomable?: boolean;
56
+ /**
57
+ * When using an interactive legend, controls whether the Y axis rescales
58
+ * to fit only the visible series. Defaults to `true`, matching the
59
+ * intuitive default for LineChart and BarChart. Set to `false` to pin
60
+ * the Y axis to the full data extent so toggling legend items off does
61
+ * not move the chart's baseline.
62
+ * @default true
63
+ */
64
+ rescaleYOnLegendToggle?: boolean;
50
65
  children?: ReactNode;
51
66
  }
@@ -37,6 +37,7 @@ import { SingleChartContext, type SingleChartRef } from '../private/single-chart
37
37
  import { SvgEmptyState } from '../private/svg-empty-state';
38
38
  import { getCurveType, getFormatter, guessOptimalNumTicks } from '../private/time-axis';
39
39
  import { withResponsive } from '../private/with-responsive';
40
+ import { useXZoom, ZoomResetButton, ZoomSelectionRect } from '../private/x-zoom';
40
41
  import styles from './line-chart.module.scss';
41
42
  import { LineChartAnnotation, LineChartAnnotationsOverlay, LineChartGlyph } from './private';
42
43
  import type { RenderLineGlyphProps, LineChartProps, TooltipDatum } from './types';
@@ -177,6 +178,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
177
178
  onPointerUp = undefined,
178
179
  onPointerMove = undefined,
179
180
  onPointerOut = undefined,
181
+ zoomable = false,
180
182
  children,
181
183
  gridVisibility,
182
184
  gap = 'md',
@@ -195,6 +197,12 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
195
197
  const [ isNavigating, setIsNavigating ] = useState( false );
196
198
  const internalChartRef = useRef< SingleChartRef >( null );
197
199
 
200
+ const zoom = useXZoom< Date >( {
201
+ enabled: zoomable,
202
+ chartRef: internalChartRef,
203
+ userHandlers: { onPointerDown, onPointerMove, onPointerUp },
204
+ } );
205
+
198
206
  // Process children for composition API (Legend, etc.)
199
207
  const { legendChildren, nonLegendChildren } = useChartChildren( children, 'LineChart' );
200
208
  const [ measuredChartHeight, setMeasuredChartHeight ] = useState< number | undefined >();
@@ -273,6 +281,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
273
281
  xScale: {
274
282
  type: 'time' as const,
275
283
  ...options?.xScale,
284
+ ...( zoom.domain ? { domain: zoom.domain } : {} ),
276
285
  },
277
286
  yScale: {
278
287
  type: 'linear' as const,
@@ -281,7 +290,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
281
290
  ...options?.yScale,
282
291
  },
283
292
  };
284
- }, [ options, dataSorted, width ] );
293
+ }, [ options, dataSorted, width, zoom.domain ] );
285
294
 
286
295
  const tooltipRenderGlyph = useMemo( () => {
287
296
  return ( props: GlyphProps< DataPointDate > ) => {
@@ -412,7 +421,8 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
412
421
  onBlur={ onChartBlur }
413
422
  >
414
423
  { chartHeight > 0 && (
415
- <div ref={ chartRef }>
424
+ <div ref={ chartRef } style={ { position: 'relative' } }>
425
+ { zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
416
426
  <XYChart
417
427
  theme={ theme }
418
428
  width={ width }
@@ -424,9 +434,9 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
424
434
  // xScale and yScale could be set in Axis as well, but they are `scale` props there.
425
435
  xScale={ chartOptions.xScale }
426
436
  yScale={ chartOptions.yScale }
427
- onPointerDown={ onPointerDown }
428
- onPointerUp={ onPointerUp }
429
- onPointerMove={ onPointerMove }
437
+ onPointerDown={ zoom.handlers.onPointerDown }
438
+ onPointerUp={ zoom.handlers.onPointerUp }
439
+ onPointerMove={ zoom.handlers.onPointerMove }
430
440
  onPointerOut={ onPointerOut }
431
441
  pointerEventsDataKey="nearest"
432
442
  >
@@ -556,6 +566,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
556
566
  height={ height }
557
567
  margin={ margin }
558
568
  />
569
+ { zoomable && <ZoomSelectionRect drag={ zoom.drag } /> }
559
570
  </XYChart>
560
571
  </div>
561
572
  ) }
@@ -41,6 +41,12 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] > {
41
41
  showVertical?: boolean;
42
42
  showHorizontal?: boolean;
43
43
  };
44
+ /**
45
+ * Enable drag-to-zoom on the X axis. The user drags horizontally to
46
+ * select a range; the X axis rescales to that range. A small reset
47
+ * button appears in the top-right of the chart while zoomed.
48
+ */
49
+ zoomable?: boolean;
44
50
  children?: ReactNode;
45
51
  }
46
52
 
@@ -0,0 +1,142 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { useRef } from 'react';
3
+ import { useXZoom } from '../x-zoom';
4
+ import type { SingleChartRef } from '../single-chart-context';
5
+ import type { EventHandlerParams } from '@visx/xychart';
6
+
7
+ // A fake scale that lets the hook convert pixel positions to data values.
8
+ // The hook only uses `.invert()`; everything else can be stubbed.
9
+ const fakeXScale = ( ( x: number ) => x * 2 ) as unknown as ( ( v: number ) => number ) & {
10
+ invert: ( v: number ) => number;
11
+ };
12
+ ( fakeXScale as unknown as { invert: ( v: number ) => number } ).invert = v => v * 2;
13
+
14
+ const makeChartRef = (
15
+ scale: { invert?: ( v: number ) => unknown } | null = fakeXScale as unknown as {
16
+ invert: ( v: number ) => unknown;
17
+ }
18
+ ) =>
19
+ ( {
20
+ current: {
21
+ getScales: () => ( scale ? { xScale: scale, yScale: scale } : null ),
22
+ getChartDimensions: () => ( { width: 0, height: 0, margin: {} } ),
23
+ },
24
+ } ) as unknown as ReturnType< typeof useRef< SingleChartRef > >;
25
+
26
+ const makeParams = ( x: number ): EventHandlerParams< object > =>
27
+ ( {
28
+ key: '',
29
+ index: 0,
30
+ datum: {},
31
+ event: new Event( 'pointerdown' ) as unknown as PointerEvent,
32
+ svgPoint: { x, y: 0 },
33
+ } ) as unknown as EventHandlerParams< object >;
34
+
35
+ describe( 'useXZoom', () => {
36
+ test( 'commits a domain on pointerup after dragging more than minDragPixels', () => {
37
+ const chartRef = makeChartRef();
38
+ const { result } = renderHook( () => useXZoom< number >( { enabled: true, chartRef } ) );
39
+
40
+ act( () => result.current.handlers.onPointerDown( makeParams( 100 ) ) );
41
+ act( () => result.current.handlers.onPointerMove( makeParams( 200 ) ) );
42
+ act( () => result.current.handlers.onPointerUp( makeParams( 200 ) ) );
43
+
44
+ // fakeXScale.invert(x) = x * 2.
45
+ expect( result.current.domain ).toEqual( [ 200, 400 ] );
46
+ expect( result.current.drag ).toBeNull();
47
+ } );
48
+
49
+ test( 'discards drags shorter than minDragPixels', () => {
50
+ const chartRef = makeChartRef();
51
+ const { result } = renderHook( () => useXZoom< number >( { enabled: true, chartRef } ) );
52
+
53
+ act( () => result.current.handlers.onPointerDown( makeParams( 100 ) ) );
54
+ act( () => result.current.handlers.onPointerMove( makeParams( 103 ) ) );
55
+ act( () => result.current.handlers.onPointerUp( makeParams( 103 ) ) );
56
+
57
+ expect( result.current.domain ).toBeNull();
58
+ expect( result.current.drag ).toBeNull();
59
+ } );
60
+
61
+ test( 'normalises right-to-left drags', () => {
62
+ const chartRef = makeChartRef();
63
+ const { result } = renderHook( () => useXZoom< number >( { enabled: true, chartRef } ) );
64
+
65
+ act( () => result.current.handlers.onPointerDown( makeParams( 300 ) ) );
66
+ act( () => result.current.handlers.onPointerMove( makeParams( 100 ) ) );
67
+ act( () => result.current.handlers.onPointerUp( makeParams( 100 ) ) );
68
+
69
+ expect( result.current.domain ).toEqual( [ 200, 600 ] );
70
+ } );
71
+
72
+ test( 'reset clears the committed domain', () => {
73
+ const chartRef = makeChartRef();
74
+ const { result } = renderHook( () => useXZoom< number >( { enabled: true, chartRef } ) );
75
+
76
+ act( () => result.current.handlers.onPointerDown( makeParams( 100 ) ) );
77
+ act( () => result.current.handlers.onPointerMove( makeParams( 200 ) ) );
78
+ act( () => result.current.handlers.onPointerUp( makeParams( 200 ) ) );
79
+ expect( result.current.domain ).not.toBeNull();
80
+ act( () => result.current.reset() );
81
+ expect( result.current.domain ).toBeNull();
82
+ } );
83
+
84
+ test( 'is a passthrough when disabled', () => {
85
+ const chartRef = makeChartRef();
86
+ const userOnPointerDown = jest.fn();
87
+ const { result } = renderHook( () =>
88
+ useXZoom< number >( {
89
+ enabled: false,
90
+ chartRef,
91
+ userHandlers: { onPointerDown: userOnPointerDown },
92
+ } )
93
+ );
94
+
95
+ act( () => result.current.handlers.onPointerDown( makeParams( 100 ) ) );
96
+ act( () => result.current.handlers.onPointerMove( makeParams( 200 ) ) );
97
+ act( () => result.current.handlers.onPointerUp( makeParams( 200 ) ) );
98
+
99
+ expect( userOnPointerDown ).toHaveBeenCalledTimes( 1 );
100
+ expect( result.current.domain ).toBeNull();
101
+ expect( result.current.drag ).toBeNull();
102
+ } );
103
+
104
+ test( 'forwards events to user handlers when zoom is enabled', () => {
105
+ const chartRef = makeChartRef();
106
+ const userOnPointerDown = jest.fn();
107
+ const userOnPointerMove = jest.fn();
108
+ const userOnPointerUp = jest.fn();
109
+ const { result } = renderHook( () =>
110
+ useXZoom< number >( {
111
+ enabled: true,
112
+ chartRef,
113
+ userHandlers: {
114
+ onPointerDown: userOnPointerDown,
115
+ onPointerMove: userOnPointerMove,
116
+ onPointerUp: userOnPointerUp,
117
+ },
118
+ } )
119
+ );
120
+
121
+ act( () => result.current.handlers.onPointerDown( makeParams( 100 ) ) );
122
+ act( () => result.current.handlers.onPointerMove( makeParams( 200 ) ) );
123
+ act( () => result.current.handlers.onPointerUp( makeParams( 200 ) ) );
124
+
125
+ expect( userOnPointerDown ).toHaveBeenCalledTimes( 1 );
126
+ expect( userOnPointerMove ).toHaveBeenCalledTimes( 1 );
127
+ expect( userOnPointerUp ).toHaveBeenCalledTimes( 1 );
128
+ } );
129
+
130
+ test( 'leaves domain null when the X scale has no invert function', () => {
131
+ const chartRef = makeChartRef( {
132
+ /* no invert */
133
+ } );
134
+ const { result } = renderHook( () => useXZoom< number >( { enabled: true, chartRef } ) );
135
+
136
+ act( () => result.current.handlers.onPointerDown( makeParams( 100 ) ) );
137
+ act( () => result.current.handlers.onPointerMove( makeParams( 200 ) ) );
138
+ act( () => result.current.handlers.onPointerUp( makeParams( 200 ) ) );
139
+
140
+ expect( result.current.domain ).toBeNull();
141
+ } );
142
+ } );
@@ -0,0 +1,45 @@
1
+ .x-zoom {
2
+
3
+ &__selection {
4
+ fill: var(--charts-zoom-selection-fill, rgba(56, 88, 233, 0.16));
5
+ stroke: var(--charts-zoom-selection-stroke, rgba(56, 88, 233, 0.65));
6
+ stroke-width: var(--wpds-border-width-xs, 1px);
7
+ pointer-events: none;
8
+ }
9
+
10
+ &__reset {
11
+ position: absolute;
12
+ top: var(--wpds-dimension-gap-sm, 8px);
13
+ right: var(--wpds-dimension-gap-sm, 8px);
14
+ z-index: 2;
15
+ display: inline-flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ width: 28px;
19
+ height: 28px;
20
+ padding: 0;
21
+ background: var(--charts-zoom-reset-bg, rgba(255, 255, 255, 0.92));
22
+ color: var(--charts-zoom-reset-fg, #1e1e1e);
23
+ border:
24
+ var(--wpds-border-width-xs, 1px) solid
25
+ var(--charts-zoom-reset-border, rgba(0, 0, 0, 0.16));
26
+ border-radius: var(--wpds-border-radius-md, 4px);
27
+ cursor: pointer;
28
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
29
+
30
+ &:hover {
31
+ background: var(--charts-zoom-reset-bg-hover, rgba(255, 255, 255, 1));
32
+ }
33
+
34
+ &:focus-visible {
35
+ outline: 2px solid var(--charts-zoom-reset-focus, #3858e9);
36
+ outline-offset: 1px;
37
+ }
38
+ }
39
+
40
+ &__reset-icon {
41
+ width: 16px;
42
+ height: 16px;
43
+ flex-shrink: 0;
44
+ }
45
+ }