@automattic/charts 3.2.0 → 4.1.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/CHANGELOG.md +47 -0
- package/SECURITY.md +1 -0
- package/TOKENS.md +53 -58
- package/dist/index.cjs +1483 -664
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +93 -29
- package/dist/index.d.cts +233 -247
- package/dist/index.d.ts +233 -247
- package/dist/index.js +1484 -667
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/charts/area-chart/area-chart.tsx +39 -25
- package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
- package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
- package/src/charts/area-chart/types.ts +4 -7
- package/src/charts/bar-chart/bar-chart.tsx +2 -1
- package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
- package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
- package/src/charts/geo-chart/geo-chart.module.scss +1 -0
- package/src/charts/geo-chart/geo-chart.tsx +5 -10
- package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +52 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +89 -37
- package/src/charts/heatmap-chart/index.ts +2 -1
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +126 -89
- package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/private/index.ts +1 -1
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +3 -0
- package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +142 -12
- package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
- package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
- package/src/charts/heatmap-chart/types.ts +8 -2
- package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
- package/src/charts/leaderboard-chart/types.ts +2 -2
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +128 -26
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +174 -41
- package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
- package/src/charts/line-chart/test/zoned-date-string.test.tsx +37 -0
- package/src/charts/line-chart/types.ts +17 -3
- package/src/charts/pie-chart/pie-chart.module.scss +13 -1
- package/src/charts/pie-chart/pie-chart.tsx +30 -40
- package/src/charts/pie-chart/test/pie-chart.test.tsx +59 -12
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
- package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/axis-formatting.test.tsx +85 -0
- package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
- package/src/charts/private/test/time-axis-options.test.ts +267 -0
- package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
- package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
- package/src/charts/private/test/time-axis.test.ts +9 -69
- package/src/charts/private/time-axis-options.ts +106 -0
- package/src/charts/private/time-axis.ts +318 -136
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +1 -1
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +21 -17
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +227 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +30 -4
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +225 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +379 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +326 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
- package/src/hooks/test/use-chart-margin.test.tsx +173 -3
- package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
- package/src/hooks/use-chart-data-transform.ts +10 -3
- package/src/hooks/use-chart-margin.tsx +54 -9
- package/src/hooks/use-xychart-theme.ts +25 -9
- package/src/index.ts +8 -0
- package/src/providers/chart-context/global-charts-provider.tsx +37 -38
- package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
- package/src/providers/chart-context/index.ts +1 -0
- package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
- package/src/providers/chart-context/private/series-palette.ts +3 -13
- package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
- package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
- package/src/providers/chart-context/themes.ts +7 -52
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +39 -62
- package/src/styles/test/chart-scope.test.ts +19 -47
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/test-utils/zoned-parsing-suite.ts +90 -0
- package/src/types.ts +79 -166
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +157 -0
- package/src/utils/date-parsing.ts +103 -52
- package/src/utils/get-edge-tick-widths.ts +33 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/test/bucket-info.test.ts +185 -0
- package/src/utils/test/color-utils.test.ts +1 -48
- package/src/utils/test/date-formatting.test.ts +89 -0
- package/src/utils/test/date-parsing.auckland.test.ts +6 -0
- package/src/utils/test/date-parsing.santiago.test.ts +6 -0
- package/src/utils/test/date-parsing.test.ts +16 -1
- package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/utils/warn-once.ts +23 -0
- package/src/visx/types.ts +56 -5
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
- package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
- package/src/styles/test/wpds-fallbacks.test.ts +0 -131
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { Tooltip, defaultStyles } from '@visx/tooltip';
|
|
2
|
+
import { DataContext, TooltipContext } from '@visx/xychart';
|
|
3
|
+
import { useCallback, useContext, useState } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { BoundedTooltip } from './private/bounded-tooltip';
|
|
6
|
+
import type {
|
|
7
|
+
CrosshairStyle,
|
|
8
|
+
RenderTooltipGlyphProps,
|
|
9
|
+
XyChartTooltipProps,
|
|
10
|
+
} from '../../visx/types';
|
|
11
|
+
import type { TooltipContextType } from '@visx/xychart';
|
|
12
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
13
|
+
|
|
14
|
+
const CROSSHAIR_STROKE_WIDTH = 1.5;
|
|
15
|
+
const CROSSHAIR_PAINT_PROPERTIES = new Set( [
|
|
16
|
+
'stroke',
|
|
17
|
+
'strokeWidth',
|
|
18
|
+
'strokeOpacity',
|
|
19
|
+
'strokeDasharray',
|
|
20
|
+
'strokeLinecap',
|
|
21
|
+
'opacity',
|
|
22
|
+
] );
|
|
23
|
+
|
|
24
|
+
const crosshairPaintProps = ( props: CrosshairStyle = {} ) => {
|
|
25
|
+
const pickPaint = ( value: object ) =>
|
|
26
|
+
Object.fromEntries(
|
|
27
|
+
Object.entries( value ).filter( ( [ key ] ) => CROSSHAIR_PAINT_PROPERTIES.has( key ) )
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...pickPaint( props ),
|
|
32
|
+
...( props.className === undefined ? {} : { className: props.className } ),
|
|
33
|
+
...( props.style === undefined ? {} : { style: pickPaint( props.style ) } ),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_GLYPH_RADIUS = 4;
|
|
37
|
+
const FALLBACK_COLOR = '#222';
|
|
38
|
+
// Above the chart's own overlays (the zoom reset button sits at 2). The chart
|
|
39
|
+
// wrapper isolates its stacking context, so this never competes with page chrome.
|
|
40
|
+
const DEFAULT_TOOLTIP_Z_INDEX = 3;
|
|
41
|
+
// Portal-era options, accepted for compatibility and dropped before the box
|
|
42
|
+
// renders: it sits in the chart wrapper and moves with it, and nothing
|
|
43
|
+
// measures it any more.
|
|
44
|
+
const PORTAL_OPTIONS = new Set( [ 'scroll', 'debounce', 'resizeObserverPolyfill' ] );
|
|
45
|
+
|
|
46
|
+
type ScaleFn = ( value: unknown ) => number | undefined;
|
|
47
|
+
|
|
48
|
+
const isValidNumber = ( value: unknown ): value is number =>
|
|
49
|
+
typeof value === 'number' && Number.isFinite( value );
|
|
50
|
+
|
|
51
|
+
// Band scales place a datum at the start of its band; snapping targets the band's centre.
|
|
52
|
+
// A d3 scale is a function that carries `bandwidth` as a property.
|
|
53
|
+
const scaleBandwidth = ( scale: unknown ): number => {
|
|
54
|
+
const bandwidth = ( scale as { bandwidth?: unknown } | null | undefined )?.bandwidth;
|
|
55
|
+
return typeof bandwidth === 'function' ? Number( bandwidth() ) : 0;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const DefaultGlyph = < Datum extends object >( {
|
|
59
|
+
x,
|
|
60
|
+
y,
|
|
61
|
+
size,
|
|
62
|
+
color,
|
|
63
|
+
glyphStyle,
|
|
64
|
+
seriesKey,
|
|
65
|
+
}: Omit< RenderTooltipGlyphProps< Datum >, 'key' > & { seriesKey: string } ) => {
|
|
66
|
+
const { theme } = useContext( DataContext ) || {};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<circle
|
|
70
|
+
data-testid={ `xy-chart-tooltip-glyph-${ seriesKey }` }
|
|
71
|
+
cx={ x }
|
|
72
|
+
cy={ y }
|
|
73
|
+
r={ size }
|
|
74
|
+
fill={ color }
|
|
75
|
+
stroke={ theme?.backgroundColor }
|
|
76
|
+
strokeWidth={ CROSSHAIR_STROKE_WIDTH }
|
|
77
|
+
paintOrder="fill"
|
|
78
|
+
{ ...glyphStyle }
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const defaultRenderGlyph = < Datum extends object >( {
|
|
84
|
+
key,
|
|
85
|
+
...props
|
|
86
|
+
}: RenderTooltipGlyphProps< Datum > ) => (
|
|
87
|
+
<DefaultGlyph key={ key } seriesKey={ key } { ...props } />
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
type XyChartTooltipContentProps< Datum extends object > = XyChartTooltipProps< Datum > & {
|
|
91
|
+
tooltipContext: TooltipContextType< Datum >;
|
|
92
|
+
/** The SVG's HTML parent once found. The box waits for it; the SVG overlay does not. */
|
|
93
|
+
container: HTMLElement | null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const XyChartTooltipContent = < Datum extends object >( {
|
|
97
|
+
tooltipContext,
|
|
98
|
+
container,
|
|
99
|
+
renderTooltip,
|
|
100
|
+
renderGlyph = defaultRenderGlyph,
|
|
101
|
+
glyphStyle,
|
|
102
|
+
snapTooltipToDatumX = false,
|
|
103
|
+
snapTooltipToDatumY = false,
|
|
104
|
+
showVerticalCrosshair = false,
|
|
105
|
+
showHorizontalCrosshair = false,
|
|
106
|
+
showDatumGlyph = false,
|
|
107
|
+
showSeriesGlyphs = false,
|
|
108
|
+
verticalCrosshairStyle,
|
|
109
|
+
horizontalCrosshairStyle,
|
|
110
|
+
detectBounds = true,
|
|
111
|
+
tooltipPlacement = 'auto',
|
|
112
|
+
zIndex = DEFAULT_TOOLTIP_Z_INDEX,
|
|
113
|
+
style,
|
|
114
|
+
...rest
|
|
115
|
+
}: XyChartTooltipContentProps< Datum > ) => {
|
|
116
|
+
const tooltipProps = Object.fromEntries(
|
|
117
|
+
Object.entries( rest ).filter( ( [ key ] ) => ! PORTAL_OPTIONS.has( key ) )
|
|
118
|
+
) as Omit< typeof rest, 'scroll' | 'debounce' | 'resizeObserverPolyfill' >;
|
|
119
|
+
const {
|
|
120
|
+
colorScale,
|
|
121
|
+
theme,
|
|
122
|
+
innerHeight = 0,
|
|
123
|
+
innerWidth = 0,
|
|
124
|
+
margin,
|
|
125
|
+
xScale,
|
|
126
|
+
yScale,
|
|
127
|
+
dataRegistry,
|
|
128
|
+
} = useContext( DataContext ) || {};
|
|
129
|
+
|
|
130
|
+
const tooltipContent = renderTooltip ? renderTooltip( { ...tooltipContext, colorScale } ) : null;
|
|
131
|
+
if ( tooltipContent == null ) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const scaleX = xScale as ScaleFn | undefined;
|
|
136
|
+
const scaleY = yScale as ScaleFn | undefined;
|
|
137
|
+
|
|
138
|
+
// Datum position in SVG coordinates, from the accessors the series registered.
|
|
139
|
+
const getDatumLeftTop = ( key: string, datum: Datum ) => {
|
|
140
|
+
const entry = dataRegistry?.get( key );
|
|
141
|
+
const left =
|
|
142
|
+
scaleX && entry?.xAccessor
|
|
143
|
+
? Number( scaleX( entry.xAccessor( datum ) ) ) + scaleBandwidth( xScale ) / 2
|
|
144
|
+
: undefined;
|
|
145
|
+
const top =
|
|
146
|
+
scaleY && entry?.yAccessor
|
|
147
|
+
? Number( scaleY( entry.yAccessor( datum ) ) ) + scaleBandwidth( yScale ) / 2
|
|
148
|
+
: undefined;
|
|
149
|
+
return { left, top };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const nearestDatum = tooltipContext.tooltipData?.nearestDatum;
|
|
153
|
+
let { tooltipLeft, tooltipTop } = tooltipContext;
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
nearestDatum &&
|
|
157
|
+
( snapTooltipToDatumX || snapTooltipToDatumY || tooltipPlacement === 'below-axis' )
|
|
158
|
+
) {
|
|
159
|
+
const { left, top } = getDatumLeftTop( nearestDatum.key, nearestDatum.datum );
|
|
160
|
+
if ( ( snapTooltipToDatumX || tooltipPlacement === 'below-axis' ) && isValidNumber( left ) ) {
|
|
161
|
+
tooltipLeft = left;
|
|
162
|
+
}
|
|
163
|
+
if ( snapTooltipToDatumY && isValidNumber( top ) ) {
|
|
164
|
+
tooltipTop = top;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const glyphs: ReactNode[] = [];
|
|
169
|
+
if ( showDatumGlyph || showSeriesGlyphs ) {
|
|
170
|
+
const size = Number( glyphStyle?.radius ?? DEFAULT_GLYPH_RADIUS );
|
|
171
|
+
const labelColor = theme?.htmlLabel?.color ?? FALLBACK_COLOR;
|
|
172
|
+
// visx colours a lone nearest-datum glyph like the gridlines and series glyphs like labels.
|
|
173
|
+
const fallbackColor = showSeriesGlyphs ? labelColor : theme?.gridStyles?.stroke ?? labelColor;
|
|
174
|
+
let entries: Array< { key: string; datum: Datum; index: number } > = [];
|
|
175
|
+
if ( showSeriesGlyphs ) {
|
|
176
|
+
entries = Object.values( tooltipContext.tooltipData?.datumByKey ?? {} );
|
|
177
|
+
} else if ( nearestDatum ) {
|
|
178
|
+
entries = [ nearestDatum ];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for ( const { key, datum, index } of entries ) {
|
|
182
|
+
const { left, top } = getDatumLeftTop( key, datum );
|
|
183
|
+
if ( ! isValidNumber( left ) || ! isValidNumber( top ) ) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// The group carries the position and the renderer draws at the origin,
|
|
187
|
+
// as with visx's portal: a renderer that ignores `x` and `y` still lands
|
|
188
|
+
// on its datum.
|
|
189
|
+
glyphs.push(
|
|
190
|
+
<g
|
|
191
|
+
key={ key }
|
|
192
|
+
className="visx-tooltip-glyph"
|
|
193
|
+
data-testid={ `xy-chart-tooltip-glyph-group-${ key }` }
|
|
194
|
+
transform={ `translate(${ left }, ${ top })` }
|
|
195
|
+
>
|
|
196
|
+
{ renderGlyph( {
|
|
197
|
+
key,
|
|
198
|
+
color: colorScale?.( key ) ?? fallbackColor,
|
|
199
|
+
datum,
|
|
200
|
+
index,
|
|
201
|
+
size,
|
|
202
|
+
x: 0,
|
|
203
|
+
y: 0,
|
|
204
|
+
glyphStyle,
|
|
205
|
+
isNearestDatum: nearestDatum?.key === key,
|
|
206
|
+
} ) }
|
|
207
|
+
</g>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const crosshairStroke = theme?.gridStyles?.stroke ?? theme?.htmlLabel?.color ?? FALLBACK_COLOR;
|
|
213
|
+
const marginTop = margin?.top ?? 0;
|
|
214
|
+
const marginLeft = margin?.left ?? 0;
|
|
215
|
+
|
|
216
|
+
const TooltipComponent =
|
|
217
|
+
detectBounds || tooltipPlacement === 'below-axis' ? BoundedTooltip : Tooltip;
|
|
218
|
+
const boxStyle: CSSProperties = {
|
|
219
|
+
...defaultStyles,
|
|
220
|
+
zIndex,
|
|
221
|
+
backgroundColor: theme?.backgroundColor ?? 'white',
|
|
222
|
+
boxShadow: `0 1px 2px ${
|
|
223
|
+
theme?.htmlLabel?.color ? `${ theme.htmlLabel.color }55` : '#22222255'
|
|
224
|
+
}`,
|
|
225
|
+
...theme?.htmlLabel,
|
|
226
|
+
...style,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<>
|
|
231
|
+
<g className="visx-tooltip-overlay" pointerEvents="none">
|
|
232
|
+
{ showVerticalCrosshair && isValidNumber( tooltipLeft ) && (
|
|
233
|
+
<line
|
|
234
|
+
className="visx-crosshair visx-crosshair-vertical"
|
|
235
|
+
data-testid="xy-chart-tooltip-crosshair-vertical"
|
|
236
|
+
stroke={ crosshairStroke }
|
|
237
|
+
strokeWidth={ CROSSHAIR_STROKE_WIDTH }
|
|
238
|
+
{ ...crosshairPaintProps( verticalCrosshairStyle ) }
|
|
239
|
+
x1={ tooltipLeft }
|
|
240
|
+
x2={ tooltipLeft }
|
|
241
|
+
y1={ marginTop }
|
|
242
|
+
y2={ marginTop + innerHeight }
|
|
243
|
+
/>
|
|
244
|
+
) }
|
|
245
|
+
{ showHorizontalCrosshair && isValidNumber( tooltipTop ) && (
|
|
246
|
+
<line
|
|
247
|
+
className="visx-crosshair visx-crosshair-horizontal"
|
|
248
|
+
data-testid="xy-chart-tooltip-crosshair-horizontal"
|
|
249
|
+
stroke={ crosshairStroke }
|
|
250
|
+
strokeWidth={ CROSSHAIR_STROKE_WIDTH }
|
|
251
|
+
{ ...crosshairPaintProps( horizontalCrosshairStyle ) }
|
|
252
|
+
x1={ marginLeft }
|
|
253
|
+
x2={ marginLeft + innerWidth }
|
|
254
|
+
y1={ tooltipTop }
|
|
255
|
+
y2={ tooltipTop }
|
|
256
|
+
/>
|
|
257
|
+
) }
|
|
258
|
+
{ glyphs }
|
|
259
|
+
</g>
|
|
260
|
+
{ container &&
|
|
261
|
+
createPortal(
|
|
262
|
+
<TooltipComponent
|
|
263
|
+
left={ tooltipLeft }
|
|
264
|
+
top={
|
|
265
|
+
tooltipPlacement === 'below-axis'
|
|
266
|
+
? marginTop + innerHeight + ( margin?.bottom ?? 0 )
|
|
267
|
+
: tooltipTop
|
|
268
|
+
}
|
|
269
|
+
style={ boxStyle }
|
|
270
|
+
applyPositionStyle
|
|
271
|
+
{ ...tooltipProps }
|
|
272
|
+
{ ...( tooltipPlacement === 'below-axis' && { placement: tooltipPlacement } ) }
|
|
273
|
+
>
|
|
274
|
+
{ tooltipContent }
|
|
275
|
+
</TooltipComponent>,
|
|
276
|
+
container
|
|
277
|
+
) }
|
|
278
|
+
</>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* In-tree replacement for `@visx/xychart`'s `Tooltip`.
|
|
284
|
+
*
|
|
285
|
+
* visx renders its tooltip box, glyphs and crosshairs through portals appended
|
|
286
|
+
* to `document.body`. That puts them in the page's root stacking context, where
|
|
287
|
+
* they paint above every sticky or fixed element that lives inside a nested
|
|
288
|
+
* stacking context, and no z-index on that element can change the order. This
|
|
289
|
+
* component draws the glyphs and crosshairs straight into the chart SVG and
|
|
290
|
+
* renders the tooltip box into the SVG's parent element, so all of them stack
|
|
291
|
+
* as ordinary descendants of the chart.
|
|
292
|
+
*
|
|
293
|
+
* Render it as a child of `XYChart`. The element wrapping that `XYChart` must
|
|
294
|
+
* be `position: relative` with the SVG at its origin, and `isolation: isolate`:
|
|
295
|
+
* the box is placed with the SVG-local coordinates visx reports, and the
|
|
296
|
+
* isolation keeps the box's `zIndex` from competing with page chrome. The box
|
|
297
|
+
* flips and clamps to stay inside the nearest ancestor that clips its overflow
|
|
298
|
+
* (or the viewport), so it may extend past the chart wrapper but is never cut
|
|
299
|
+
* off unless that ancestor is smaller than the box itself. Below-axis placement
|
|
300
|
+
* pins it beneath the x-axis and applies horizontal bounds only.
|
|
301
|
+
*
|
|
302
|
+
* @param props - visx's `Tooltip` options. `scroll`, `debounce` and `resizeObserverPolyfill` are accepted and ignored.
|
|
303
|
+
* @return An anchor in the SVG, plus the overlay and the tooltip box while the tooltip is open.
|
|
304
|
+
*/
|
|
305
|
+
export const XyChartTooltip = < Datum extends object >( props: XyChartTooltipProps< Datum > ) => {
|
|
306
|
+
const tooltipContext = useContext( TooltipContext ) as TooltipContextType< Datum > | null;
|
|
307
|
+
const [ container, setContainer ] = useState< HTMLElement | null >( null );
|
|
308
|
+
|
|
309
|
+
// Children of XYChart render inside its SVG, and the box needs an HTML parent.
|
|
310
|
+
const anchorRef = useCallback( ( node: SVGGElement | null ) => {
|
|
311
|
+
setContainer( node?.ownerSVGElement?.parentElement ?? null );
|
|
312
|
+
}, [] );
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<>
|
|
316
|
+
<g ref={ anchorRef } data-testid="xy-chart-tooltip-anchor" />
|
|
317
|
+
{ tooltipContext?.tooltipOpen && (
|
|
318
|
+
<XyChartTooltipContent
|
|
319
|
+
{ ...props }
|
|
320
|
+
tooltipContext={ tooltipContext }
|
|
321
|
+
container={ container }
|
|
322
|
+
/>
|
|
323
|
+
) }
|
|
324
|
+
</>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
display: inline-flex;
|
|
3
3
|
align-items: center;
|
|
4
4
|
gap: 0.125em;
|
|
5
|
-
font-size: var(--wpds-typography-font-size-md
|
|
6
|
-
font-weight: var(--wpds-typography-font-weight-emphasis
|
|
5
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
6
|
+
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
7
7
|
line-height: 1;
|
|
8
8
|
|
|
9
9
|
&--up {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { GlobalChartsProvider } from '../../providers';
|
|
3
|
+
import { useChartDataTransform } from '../use-chart-data-transform';
|
|
4
|
+
import type { SeriesData } from '../../types';
|
|
5
|
+
|
|
6
|
+
// One reference, reused across both renders, so only the zone can invalidate the memo.
|
|
7
|
+
const data: SeriesData[] = [
|
|
8
|
+
{ label: 'Series A', data: [ { dateString: '2026-08-02', value: 1 } ] },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const Probe = () => {
|
|
12
|
+
const [ series ] = useChartDataTransform( data );
|
|
13
|
+
const point = series.data[ 0 ] as { date?: Date };
|
|
14
|
+
|
|
15
|
+
return <span data-testid="instant">{ point.date?.toISOString() }</span>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe( 'useChartDataTransform', () => {
|
|
19
|
+
it( 're-dates its points when only the provider time zone changes', () => {
|
|
20
|
+
const { rerender } = render(
|
|
21
|
+
<GlobalChartsProvider timeZone="Asia/Tokyo">
|
|
22
|
+
<Probe />
|
|
23
|
+
</GlobalChartsProvider>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect( screen.getByTestId( 'instant' ) ).toHaveTextContent( '2026-08-01T15:00:00.000Z' );
|
|
27
|
+
|
|
28
|
+
rerender(
|
|
29
|
+
<GlobalChartsProvider timeZone="Pacific/Auckland">
|
|
30
|
+
<Probe />
|
|
31
|
+
</GlobalChartsProvider>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect( screen.getByTestId( 'instant' ) ).toHaveTextContent( '2026-08-01T12:00:00.000Z' );
|
|
35
|
+
} );
|
|
36
|
+
} );
|
|
@@ -9,6 +9,13 @@ jest.mock( '../../utils/get-longest-tick-width', () => ( {
|
|
|
9
9
|
getLongestTickWidth: ( ...args: unknown[] ) => mockGetLongestTickWidth( ...args ),
|
|
10
10
|
} ) );
|
|
11
11
|
|
|
12
|
+
// jsdom has no getComputedTextLength, so the real measurement always returns null.
|
|
13
|
+
const mockGetEdgeTickWidths = jest.fn();
|
|
14
|
+
jest.mock( '../../utils/get-edge-tick-widths', () => ( {
|
|
15
|
+
...jest.requireActual( '../../utils/get-edge-tick-widths' ),
|
|
16
|
+
getEdgeTickWidths: ( ...args: unknown[] ) => mockGetEdgeTickWidths( ...args ),
|
|
17
|
+
} ) );
|
|
18
|
+
|
|
12
19
|
describe( 'useChartMargin', () => {
|
|
13
20
|
const baseTheme = {
|
|
14
21
|
axisStyles: {
|
|
@@ -43,6 +50,8 @@ describe( 'useChartMargin', () => {
|
|
|
43
50
|
beforeEach( () => {
|
|
44
51
|
mockGetLongestTickWidth.mockReset();
|
|
45
52
|
mockGetLongestTickWidth.mockReturnValue( 40 );
|
|
53
|
+
mockGetEdgeTickWidths.mockReset();
|
|
54
|
+
mockGetEdgeTickWidths.mockReturnValue( { first: 0, last: 0 } );
|
|
46
55
|
} );
|
|
47
56
|
|
|
48
57
|
it( 'calculates left margin for left y axis', () => {
|
|
@@ -62,7 +71,7 @@ describe( 'useChartMargin', () => {
|
|
|
62
71
|
expect( mockGetLongestTickWidth ).toHaveBeenCalledWith(
|
|
63
72
|
expect.any( Array ),
|
|
64
73
|
options.axis.y.tickFormat,
|
|
65
|
-
|
|
74
|
+
{ fontSize: '12px' }
|
|
66
75
|
);
|
|
67
76
|
// 40 label width + 8 tick length + ceil(11 * 0.25) label dx offset
|
|
68
77
|
expect( result.current.left ).toBe( 51 );
|
|
@@ -85,7 +94,7 @@ describe( 'useChartMargin', () => {
|
|
|
85
94
|
expect( mockGetLongestTickWidth ).toHaveBeenCalledWith(
|
|
86
95
|
expect.any( Array ),
|
|
87
96
|
options.axis.y.tickFormat,
|
|
88
|
-
|
|
97
|
+
{ fontSize: '12px' }
|
|
89
98
|
);
|
|
90
99
|
// 40 label width + 8 tick length + ceil(11 * 0.25) label dx offset
|
|
91
100
|
expect( result.current.right ).toBe( 51 );
|
|
@@ -108,7 +117,7 @@ describe( 'useChartMargin', () => {
|
|
|
108
117
|
expect( mockGetLongestTickWidth ).toHaveBeenCalledWith(
|
|
109
118
|
[ 0, 1000 ],
|
|
110
119
|
options.axis.y.tickFormat,
|
|
111
|
-
|
|
120
|
+
{ fontSize: '12px' }
|
|
112
121
|
);
|
|
113
122
|
} );
|
|
114
123
|
|
|
@@ -185,6 +194,167 @@ describe( 'useChartMargin', () => {
|
|
|
185
194
|
expect( result.current.bottom ).toBe( 25 );
|
|
186
195
|
} );
|
|
187
196
|
|
|
197
|
+
describe( 'x-axis edge tick labels', () => {
|
|
198
|
+
const tickFormat = ( value: number ) => new Date( value ).toDateString();
|
|
199
|
+
const tickValues = [ 1, 2, 3 ];
|
|
200
|
+
const datedXOptions = ( xOverrides = {} ) => ( {
|
|
201
|
+
...optionsBase,
|
|
202
|
+
axis: { ...optionsBase.axis, x: { tickValues, tickFormat, ...xOverrides } },
|
|
203
|
+
} );
|
|
204
|
+
|
|
205
|
+
it( 'reserves half of the last label on the right', () => {
|
|
206
|
+
mockGetEdgeTickWidths.mockReturnValue( { first: 0, last: 60 } );
|
|
207
|
+
|
|
208
|
+
const { result } = renderHook( () =>
|
|
209
|
+
useChartMargin( 300, datedXOptions(), data, baseTheme )
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect( result.current.right ).toBe( 30 );
|
|
213
|
+
} );
|
|
214
|
+
|
|
215
|
+
it( 'keeps the default right margin when the last label fits inside it', () => {
|
|
216
|
+
mockGetEdgeTickWidths.mockReturnValue( { first: 0, last: 30 } );
|
|
217
|
+
|
|
218
|
+
const { result } = renderHook( () =>
|
|
219
|
+
useChartMargin( 300, datedXOptions(), data, baseTheme )
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect( result.current.right ).toBe( 20 );
|
|
223
|
+
} );
|
|
224
|
+
|
|
225
|
+
it( 'widens the left margin past the y-axis reservation when the first label needs it', () => {
|
|
226
|
+
mockGetEdgeTickWidths.mockReturnValue( { first: 120, last: 0 } );
|
|
227
|
+
|
|
228
|
+
const { result } = renderHook( () =>
|
|
229
|
+
useChartMargin( 300, datedXOptions(), data, baseTheme )
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// 60 for the label's overhanging half, over the 51 the y-axis ticks need.
|
|
233
|
+
expect( result.current.left ).toBe( 60 );
|
|
234
|
+
} );
|
|
235
|
+
|
|
236
|
+
it( 'measures the axis tick values with the x tick label style', () => {
|
|
237
|
+
const theme = {
|
|
238
|
+
...baseTheme,
|
|
239
|
+
axisStyles: {
|
|
240
|
+
...baseTheme.axisStyles,
|
|
241
|
+
x: {
|
|
242
|
+
bottom: { tickLabel: { fontSize: 11 }, tickLength: 8 } as unknown as never,
|
|
243
|
+
top: {} as unknown as never,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
} as XYChartTheme;
|
|
247
|
+
|
|
248
|
+
renderHook( () => useChartMargin( 300, datedXOptions(), data, theme ) );
|
|
249
|
+
|
|
250
|
+
expect( mockGetEdgeTickWidths ).toHaveBeenCalledWith( tickValues, tickFormat, {
|
|
251
|
+
fontSize: '11px',
|
|
252
|
+
} );
|
|
253
|
+
} );
|
|
254
|
+
|
|
255
|
+
it( 'falls back to the raw tick label style when its font size is a relative unit', () => {
|
|
256
|
+
const theme = {
|
|
257
|
+
...baseTheme,
|
|
258
|
+
axisStyles: {
|
|
259
|
+
...baseTheme.axisStyles,
|
|
260
|
+
x: {
|
|
261
|
+
bottom: { tickLabel: { fontSize: '0.875rem' }, tickLength: 8 } as unknown as never,
|
|
262
|
+
top: {} as unknown as never,
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
} as XYChartTheme;
|
|
266
|
+
|
|
267
|
+
renderHook( () => useChartMargin( 300, datedXOptions(), data, theme ) );
|
|
268
|
+
|
|
269
|
+
expect( mockGetEdgeTickWidths ).toHaveBeenCalledWith( tickValues, tickFormat, {
|
|
270
|
+
fontSize: '0.875rem',
|
|
271
|
+
} );
|
|
272
|
+
} );
|
|
273
|
+
|
|
274
|
+
it( 'reserves the edge labels on a top x axis too', () => {
|
|
275
|
+
mockGetEdgeTickWidths.mockReturnValue( { first: 120, last: 60 } );
|
|
276
|
+
|
|
277
|
+
const { result } = renderHook( () =>
|
|
278
|
+
useChartMargin( 300, datedXOptions( { orientation: 'top' } ), data, baseTheme )
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect( result.current.right ).toBe( 30 );
|
|
282
|
+
expect( result.current.left ).toBe( 60 );
|
|
283
|
+
} );
|
|
284
|
+
|
|
285
|
+
it( 'reserves nothing for a hidden x axis', () => {
|
|
286
|
+
mockGetEdgeTickWidths.mockReturnValue( { first: 120, last: 60 } );
|
|
287
|
+
|
|
288
|
+
const { result } = renderHook( () =>
|
|
289
|
+
useChartMargin( 300, datedXOptions( { display: false } ), data, baseTheme )
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect( mockGetEdgeTickWidths ).not.toHaveBeenCalled();
|
|
293
|
+
expect( result.current.right ).toBe( 20 );
|
|
294
|
+
expect( result.current.left ).toBe( 51 );
|
|
295
|
+
} );
|
|
296
|
+
} );
|
|
297
|
+
|
|
298
|
+
describe( 'real measurement', () => {
|
|
299
|
+
// Everything else here mocks the measurer out; this block runs it for real,
|
|
300
|
+
// so that a width that never reaches the margin would fail something.
|
|
301
|
+
const actual = jest.requireActual( '../../utils/get-edge-tick-widths' );
|
|
302
|
+
type Measurable = { getComputedTextLength?: () => number };
|
|
303
|
+
|
|
304
|
+
afterEach( () => {
|
|
305
|
+
delete ( window.SVGElement.prototype as Measurable ).getComputedTextLength;
|
|
306
|
+
} );
|
|
307
|
+
|
|
308
|
+
it( 'turns a measured edge label into a reserved margin', () => {
|
|
309
|
+
// jsdom ships no getComputedTextLength, so @visx/text cannot measure at all.
|
|
310
|
+
( window.SVGElement.prototype as Measurable ).getComputedTextLength = function (
|
|
311
|
+
this: SVGElement
|
|
312
|
+
) {
|
|
313
|
+
return ( this.textContent ?? '' ).length * 8;
|
|
314
|
+
};
|
|
315
|
+
mockGetEdgeTickWidths.mockImplementation( actual.getEdgeTickWidths );
|
|
316
|
+
|
|
317
|
+
const options = {
|
|
318
|
+
...optionsBase,
|
|
319
|
+
axis: {
|
|
320
|
+
...optionsBase.axis,
|
|
321
|
+
x: {
|
|
322
|
+
tickValues: [ 1, 2 ],
|
|
323
|
+
tickFormat: ( _value: number, index: number ) =>
|
|
324
|
+
index === 0 ? 'AA' : 'MEASURED-LAST',
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const { result } = renderHook( () => useChartMargin( 300, options, data, baseTheme ) );
|
|
330
|
+
|
|
331
|
+
// 'MEASURED-LAST' is 13 characters, so 104px wide, and half of it is reserved.
|
|
332
|
+
expect( result.current.right ).toBe( 52 );
|
|
333
|
+
} );
|
|
334
|
+
} );
|
|
335
|
+
|
|
336
|
+
describe( 'y axis gutter', () => {
|
|
337
|
+
it( 'measures a caller-pinned domain rather than the data range', () => {
|
|
338
|
+
const options = { ...optionsBase, yScale: { domain: [ 0, 1 ] as [ number, number ] } };
|
|
339
|
+
|
|
340
|
+
renderHook( () => useChartMargin( 300, options, data, baseTheme ) );
|
|
341
|
+
|
|
342
|
+
const ticks = mockGetLongestTickWidth.mock.calls[ 0 ][ 0 ] as number[];
|
|
343
|
+
expect( Math.max( ...ticks ) ).toBeLessThanOrEqual( 1 );
|
|
344
|
+
} );
|
|
345
|
+
|
|
346
|
+
it( 'reserves no gutter for a hidden y axis', () => {
|
|
347
|
+
const options = {
|
|
348
|
+
...optionsBase,
|
|
349
|
+
axis: { ...optionsBase.axis, y: { ...optionsBase.axis.y, display: false } },
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const { result } = renderHook( () => useChartMargin( 300, options, data, baseTheme ) );
|
|
353
|
+
|
|
354
|
+
expect( result.current.left ).toBe( 20 );
|
|
355
|
+
} );
|
|
356
|
+
} );
|
|
357
|
+
|
|
188
358
|
describe( 'horizontal y ticks', () => {
|
|
189
359
|
const horizontalOptions = ( tickFormat: ( value: string | number ) => string ) => ( {
|
|
190
360
|
...optionsBase,
|
|
@@ -23,7 +23,7 @@ describe( 'useXYChartTheme', () => {
|
|
|
23
23
|
document.body.removeChild( scope );
|
|
24
24
|
} );
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// jsdom does not compute `var()`, so what a pointer resolves *to* is covered in Storybook.
|
|
27
27
|
it( 'hands visx the catalog pointer for paint-only colors rather than a resolved value', () => {
|
|
28
28
|
const scope = document.createElement( 'div' );
|
|
29
29
|
document.body.appendChild( scope );
|
|
@@ -36,10 +36,10 @@ describe( 'useXYChartTheme', () => {
|
|
|
36
36
|
|
|
37
37
|
expect( result.current.gridStyles.stroke ).toBe( 'var(--a8c-charts-color-grid, #dbdbdb)' );
|
|
38
38
|
expect( result.current.axisStyles.x.bottom.axisLine.stroke ).toBe(
|
|
39
|
-
'var(--a8c-charts-color-axis, #dbdbdb)'
|
|
39
|
+
'var(--a8c-charts-color-axis-x, #dbdbdb)'
|
|
40
40
|
);
|
|
41
41
|
expect( result.current.axisStyles.x.bottom.tickLine.stroke ).toBe(
|
|
42
|
-
'var(--a8c-charts-color-tick, #dbdbdb)'
|
|
42
|
+
'var(--a8c-charts-color-tick-x, #dbdbdb)'
|
|
43
43
|
);
|
|
44
44
|
expect( result.current.axisStyles.x.bottom.tickLabel.fill ).toBe(
|
|
45
45
|
'var(--a8c-charts-color-label-axis, #1e1e1e)'
|
|
@@ -67,4 +67,17 @@ describe( 'useXYChartTheme', () => {
|
|
|
67
67
|
|
|
68
68
|
document.body.removeChild( scope );
|
|
69
69
|
} );
|
|
70
|
+
|
|
71
|
+
it( 'gives the y axis its own roles, resolving to none', () => {
|
|
72
|
+
const { result } = renderHook( () => useXYChartTheme( [] ) );
|
|
73
|
+
|
|
74
|
+
for ( const side of [ 'left', 'right' ] as const ) {
|
|
75
|
+
expect( result.current.axisStyles.y[ side ].axisLine.stroke ).toBe(
|
|
76
|
+
'var(--a8c-charts-color-axis-y, none)'
|
|
77
|
+
);
|
|
78
|
+
expect( result.current.axisStyles.y[ side ].tickLine.stroke ).toBe(
|
|
79
|
+
'var(--a8c-charts-color-tick-y, none)'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} );
|
|
70
83
|
} );
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
// Imported from the module rather than the `providers` barrel: that barrel pulls
|
|
3
|
+
// `use-chart-registration`, which imports this file's own barrel back.
|
|
4
|
+
import { useChartFormatting } from '../providers/chart-context/hooks/use-chart-formatting';
|
|
2
5
|
import { parseAsLocalDate } from '../utils';
|
|
3
6
|
import type { SeriesData } from '../types';
|
|
4
7
|
|
|
@@ -7,7 +10,7 @@ import type { SeriesData } from '../types';
|
|
|
7
10
|
*
|
|
8
11
|
* This hook extracts the common data transformation logic used in both line-chart
|
|
9
12
|
* and bar-chart components. It:
|
|
10
|
-
* 1. Parses date strings into Date objects
|
|
13
|
+
* 1. Parses date strings into Date objects, dated in the provider's time zone
|
|
11
14
|
* 2. Sorts data points by date when date properties are present
|
|
12
15
|
* 3. Returns the original data unchanged when no date properties are found
|
|
13
16
|
*
|
|
@@ -15,6 +18,10 @@ import type { SeriesData } from '../types';
|
|
|
15
18
|
* @return {SeriesData[]} The transformed and sorted data
|
|
16
19
|
*/
|
|
17
20
|
export const useChartDataTransform = ( data: SeriesData[] ) => {
|
|
21
|
+
// A naive `dateString` names no zone, so without this it would be read in the
|
|
22
|
+
// viewer's and then relabeled in the host's, and the two never cancel.
|
|
23
|
+
const { timeZone } = useChartFormatting();
|
|
24
|
+
|
|
18
25
|
return useMemo( () => {
|
|
19
26
|
// Check if the first data point has date or dateString properties
|
|
20
27
|
const firstPoint = data?.[ 0 ]?.data?.[ 0 ];
|
|
@@ -35,7 +42,7 @@ export const useChartDataTransform = ( data: SeriesData[] ) => {
|
|
|
35
42
|
if ( 'date' in point && point.date ) {
|
|
36
43
|
date = point.date;
|
|
37
44
|
} else if ( 'dateString' in point && point.dateString ) {
|
|
38
|
-
date = parseAsLocalDate( point.dateString );
|
|
45
|
+
date = parseAsLocalDate( point.dateString, timeZone );
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
return {
|
|
@@ -48,5 +55,5 @@ export const useChartDataTransform = ( data: SeriesData[] ) => {
|
|
|
48
55
|
return a.date.getTime() - b.date.getTime();
|
|
49
56
|
} ),
|
|
50
57
|
} ) );
|
|
51
|
-
}, [ data ] );
|
|
58
|
+
}, [ data, timeZone ] );
|
|
52
59
|
};
|