@perses-dev/timeseries-chart-plugin 0.14.0-beta.7 → 0.14.0-rc.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.
Files changed (132) hide show
  1. package/README.md +11 -0
  2. package/__mf/js/TimeSeriesChart.89eaf4ff.js +7 -0
  3. package/__mf/js/async/1409.855b83fb.js +7 -0
  4. package/__mf/js/async/1616.547457d5.js +1 -0
  5. package/__mf/js/async/2490.8d139ea3.js +2 -0
  6. package/__mf/js/async/392.c9cdaa1e.js +2 -0
  7. package/__mf/js/async/4077.51fc0aad.js +1 -0
  8. package/__mf/js/async/4121.5ad4a1ae.js +2 -0
  9. package/__mf/js/async/{4134.605db9d2.js → 4134.fe1c2e4e.js} +1 -1
  10. package/__mf/js/async/4496.20ca25bf.js +1 -0
  11. package/__mf/js/async/4670.c0d3f8f3.js +2 -0
  12. package/__mf/js/async/5002.89978152.js +1 -0
  13. package/__mf/js/async/5071.722913e8.js +1 -0
  14. package/__mf/js/async/{5182.99021d7d.js → 5182.b8a5b924.js} +2 -2
  15. package/__mf/js/async/5587.b4b20c0d.js +1 -0
  16. package/__mf/js/async/6274.c3436b0e.js +2 -0
  17. package/__mf/js/async/6384.242bb332.js +52 -0
  18. package/__mf/js/async/6609.7dd273f8.js +88 -0
  19. package/__mf/js/async/6625.2e9582af.js +1 -0
  20. package/__mf/js/async/7151.0e3196f4.js +1 -0
  21. package/__mf/js/async/7192.a261b528.js +1 -0
  22. package/__mf/js/async/7319.93d10923.js +1 -0
  23. package/__mf/js/async/7384.58d4a98c.js +1 -0
  24. package/__mf/js/async/7633.cabd9f90.js +2 -0
  25. package/__mf/js/async/7955.9a126c8e.js +1 -0
  26. package/__mf/js/async/7968.c90bbd91.js +1 -0
  27. package/__mf/js/async/7978.e29a754e.js +1 -0
  28. package/__mf/js/async/8356.c463987c.js +1 -0
  29. package/__mf/js/async/836.fbf1cee0.js +1 -0
  30. package/__mf/js/async/8465.cf3d9a6f.js +1 -0
  31. package/__mf/js/async/8470.343715da.js +2 -0
  32. package/__mf/js/async/873.050d950b.js +1 -0
  33. package/__mf/js/async/9235.b6aec078.js +1 -0
  34. package/__mf/js/async/941.1fbc4dad.js +2 -0
  35. package/__mf/js/async/9588.32fa0c1e.js +1 -0
  36. package/__mf/js/async/9700.4a715883.js +1 -0
  37. package/__mf/js/async/__federation_expose_TimeSeriesChart.0445a802.js +1 -0
  38. package/__mf/js/async/lib-router.ecd66a8d.js +2 -0
  39. package/__mf/js/async/{lib-router.f2c27853.js.LICENSE.txt → lib-router.ecd66a8d.js.LICENSE.txt} +3 -3
  40. package/__mf/js/main.3ed11ab8.js +7 -0
  41. package/lib/QuerySettingsEditor.d.ts.map +1 -1
  42. package/lib/QuerySettingsEditor.js +5 -8
  43. package/lib/QuerySettingsEditor.js.map +1 -1
  44. package/lib/TimeSeriesChartBase.d.ts +3 -0
  45. package/lib/TimeSeriesChartBase.d.ts.map +1 -1
  46. package/lib/TimeSeriesChartBase.js +213 -84
  47. package/lib/TimeSeriesChartBase.js.map +1 -1
  48. package/lib/TimeSeriesChartPanel.d.ts +3 -1
  49. package/lib/TimeSeriesChartPanel.d.ts.map +1 -1
  50. package/lib/TimeSeriesChartPanel.js +193 -116
  51. package/lib/TimeSeriesChartPanel.js.map +1 -1
  52. package/lib/VisualOptionsEditor.d.ts.map +1 -1
  53. package/lib/VisualOptionsEditor.js +8 -0
  54. package/lib/VisualOptionsEditor.js.map +1 -1
  55. package/lib/annotations/AnnotationTooltip.d.ts +3 -2
  56. package/lib/annotations/AnnotationTooltip.d.ts.map +1 -1
  57. package/lib/annotations/AnnotationTooltip.js +14 -2
  58. package/lib/annotations/AnnotationTooltip.js.map +1 -1
  59. package/lib/components/ColorSwatches.d.ts +5 -0
  60. package/lib/components/ColorSwatches.d.ts.map +1 -0
  61. package/lib/components/ColorSwatches.js +35 -0
  62. package/lib/components/ColorSwatches.js.map +1 -0
  63. package/lib/components/PaletteSelector.d.ts +8 -0
  64. package/lib/components/PaletteSelector.d.ts.map +1 -0
  65. package/lib/components/PaletteSelector.js +93 -0
  66. package/lib/components/PaletteSelector.js.map +1 -0
  67. package/lib/test/mock-query-results.d.ts +2 -1
  68. package/lib/test/mock-query-results.d.ts.map +1 -1
  69. package/lib/test/mock-query-results.js +40 -0
  70. package/lib/test/mock-query-results.js.map +1 -1
  71. package/lib/time-series-chart-model.d.ts +25 -3
  72. package/lib/time-series-chart-model.d.ts.map +1 -1
  73. package/lib/time-series-chart-model.js +34 -0
  74. package/lib/time-series-chart-model.js.map +1 -1
  75. package/lib/utils/data-transform.d.ts +30 -3
  76. package/lib/utils/data-transform.d.ts.map +1 -1
  77. package/lib/utils/data-transform.js +41 -8
  78. package/lib/utils/data-transform.js.map +1 -1
  79. package/lib/utils/palette-gen.d.ts +2 -2
  80. package/lib/utils/palette-gen.d.ts.map +1 -1
  81. package/lib/utils/palette-gen.js +12 -8
  82. package/lib/utils/palette-gen.js.map +1 -1
  83. package/mf-manifest.json +45 -45
  84. package/mf-stats.json +45 -45
  85. package/package.json +10 -6
  86. package/__mf/js/TimeSeriesChart.e2219fd4.js +0 -7
  87. package/__mf/js/async/1616.0201be33.js +0 -1
  88. package/__mf/js/async/3068.40e94d1b.js +0 -88
  89. package/__mf/js/async/3542.49a3ec8e.js +0 -52
  90. package/__mf/js/async/392.9e11af0e.js +0 -2
  91. package/__mf/js/async/4077.a04f22de.js +0 -1
  92. package/__mf/js/async/4121.bcbce5c8.js +0 -2
  93. package/__mf/js/async/4496.a1356301.js +0 -1
  94. package/__mf/js/async/4670.4afc93af.js +0 -2
  95. package/__mf/js/async/5002.5deda5b9.js +0 -1
  96. package/__mf/js/async/5071.77adf1ef.js +0 -1
  97. package/__mf/js/async/5394.b8a43124.js +0 -7
  98. package/__mf/js/async/5587.f79fd4b6.js +0 -1
  99. package/__mf/js/async/6274.de430473.js +0 -2
  100. package/__mf/js/async/702.f83f9d6b.js +0 -2
  101. package/__mf/js/async/7151.787a673e.js +0 -1
  102. package/__mf/js/async/7192.40bdc52a.js +0 -1
  103. package/__mf/js/async/7319.7c14d9c0.js +0 -1
  104. package/__mf/js/async/7384.d87e02fe.js +0 -1
  105. package/__mf/js/async/7491.0bfedc24.js +0 -1
  106. package/__mf/js/async/7633.1948ca18.js +0 -2
  107. package/__mf/js/async/7955.5c8d8ca0.js +0 -1
  108. package/__mf/js/async/7968.0fc6f62f.js +0 -1
  109. package/__mf/js/async/7978.304ff469.js +0 -1
  110. package/__mf/js/async/8356.944b79cb.js +0 -1
  111. package/__mf/js/async/8465.29071e09.js +0 -1
  112. package/__mf/js/async/8470.b8fae2bf.js +0 -2
  113. package/__mf/js/async/8724.69cc477a.js +0 -1
  114. package/__mf/js/async/873.04fb51e6.js +0 -1
  115. package/__mf/js/async/9235.6e9dbd11.js +0 -1
  116. package/__mf/js/async/941.8ecc9c9a.js +0 -2
  117. package/__mf/js/async/9588.523aa030.js +0 -1
  118. package/__mf/js/async/9700.cd6d1e06.js +0 -1
  119. package/__mf/js/async/__federation_expose_TimeSeriesChart.dfd20d64.js +0 -1
  120. package/__mf/js/async/lib-router.f2c27853.js +0 -2
  121. package/__mf/js/main.3444641d.js +0 -7
  122. /package/__mf/css/async/{5394.85c2cbf6.css → 1409.85c2cbf6.css} +0 -0
  123. /package/__mf/js/async/{5394.b8a43124.js.LICENSE.txt → 1409.855b83fb.js.LICENSE.txt} +0 -0
  124. /package/__mf/js/async/{702.f83f9d6b.js.LICENSE.txt → 2490.8d139ea3.js.LICENSE.txt} +0 -0
  125. /package/__mf/js/async/{392.9e11af0e.js.LICENSE.txt → 392.c9cdaa1e.js.LICENSE.txt} +0 -0
  126. /package/__mf/js/async/{4121.bcbce5c8.js.LICENSE.txt → 4121.5ad4a1ae.js.LICENSE.txt} +0 -0
  127. /package/__mf/js/async/{5182.99021d7d.js.LICENSE.txt → 5182.b8a5b924.js.LICENSE.txt} +0 -0
  128. /package/__mf/js/async/{6274.de430473.js.LICENSE.txt → 6274.c3436b0e.js.LICENSE.txt} +0 -0
  129. /package/__mf/js/async/{3068.40e94d1b.js.LICENSE.txt → 6609.7dd273f8.js.LICENSE.txt} +0 -0
  130. /package/__mf/js/async/{7633.1948ca18.js.LICENSE.txt → 7633.cabd9f90.js.LICENSE.txt} +0 -0
  131. /package/__mf/js/async/{8470.b8fae2bf.js.LICENSE.txt → 8470.343715da.js.LICENSE.txt} +0 -0
  132. /package/__mf/js/async/{941.8ecc9c9a.js.LICENSE.txt → 941.1fbc4dad.js.LICENSE.txt} +0 -0
@@ -12,19 +12,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
  import { Box } from "@mui/material";
15
- import { clearHighlightedSeries, DEFAULT_PINNED_CROSSHAIR, DEFAULT_TOOLTIP_CONFIG, EChart, enableDataZoom, getClosestTimestamp, getCommonTimeScale, getFormattedAxis, getPointInGrid, restoreChart, TimeChartTooltip, useChartsContext, useTimeZone } from "@perses-dev/components";
16
- import { LineChart as EChartsLineChart, BarChart as EChartsBarChart } from "echarts/charts";
15
+ import { clearHighlightedSeries, DEFAULT_PINNED_CROSSHAIR, DEFAULT_TOOLTIP_CONFIG, EChart, enableDataZoom, ExemplarMetadataTooltip, getClosestTimestamp, getCommonTimeScale, getFormattedAxis, getPointInGrid, restoreChart, TimeChartTooltip, useChartsContext, useTimeZone } from "@perses-dev/components";
16
+ import { LineChart as EChartsLineChart, BarChart as EChartsBarChart, ScatterChart as EChartsScatterChart } from "echarts/charts";
17
17
  import { GridComponent, DatasetComponent, DataZoomComponent, MarkAreaComponent, MarkLineComponent, MarkPointComponent, TitleComponent, ToolboxComponent, TooltipComponent } from "echarts/components";
18
18
  import { use as registerECharts } from "echarts/core";
19
19
  import { CanvasRenderer } from "echarts/renderers";
20
20
  import isEqual from "lodash/isEqual";
21
21
  import merge from "lodash/merge";
22
- import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
23
- import { AnnotationTooltip, buildAnnotationSeries } from "./annotations/AnnotationTooltip.js";
22
+ import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from "react";
23
+ import useResizeObserver from "use-resize-observer";
24
+ import { ANNOTATION_Y_AXIS, AnnotationTooltip, buildAnnotationSeries } from "./annotations/AnnotationTooltip.js";
25
+ import { EXEMPLAR_SERIES_ID_PREFIX, EXEMPLAR_SYMBOL_SIZE, getExemplarSeries } from "./utils/data-transform.js";
24
26
  import { createTimezoneAwareAxisFormatter } from "./utils/timezone-formatter.js";
25
27
  registerECharts([
26
28
  EChartsLineChart,
27
29
  EChartsBarChart,
30
+ EChartsScatterChart,
28
31
  GridComponent,
29
32
  DatasetComponent,
30
33
  DataZoomComponent,
@@ -36,7 +39,15 @@ registerECharts([
36
39
  TooltipComponent,
37
40
  CanvasRenderer
38
41
  ]);
39
- export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({ height, data, seriesMapping, annotations, timeScale: timeScaleProp, yAxis, format, seriesFormatMap, grid, isStackedBar = false, tooltipConfig = DEFAULT_TOOLTIP_CONFIG, noDataVariant = 'message', syncGroup, onDataZoom, onDoubleClick, __experimentalEChartsOptionsOverride }, ref) {
42
+ /**
43
+ * Maximum pixel distance from an exemplar marker center for the cursor to count as still
44
+ * hovering it (covers the full diamond bounding box, corners included).
45
+ */ const EXEMPLAR_HOVER_RADIUS = Math.SQRT2 * EXEMPLAR_SYMBOL_SIZE / 2;
46
+ const CHART_SX = {
47
+ width: '100%',
48
+ height: '100%'
49
+ };
50
+ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({ height, data, seriesMapping, exemplars, annotations, timeScale: timeScaleProp, yAxis, format, seriesFormatMap, grid, isStackedBar = false, tooltipConfig = DEFAULT_TOOLTIP_CONFIG, noDataVariant = 'message', syncGroup, onDataZoom, onDoubleClick, __experimentalEChartsOptionsOverride }, ref) {
40
51
  const { chartsTheme, enablePinning, enableSyncGrouping, lastTooltipPinnedCoords, setLastTooltipPinnedCoords } = useChartsContext();
41
52
  const isPinningEnabled = tooltipConfig.enablePinning && enablePinning;
42
53
  const chartRef = useRef();
@@ -47,33 +58,48 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
47
58
  const [startX, setStartX] = useState(0);
48
59
  const [hoveredAnnotation, setHoveredAnnotation] = useState(null);
49
60
  const [pinnedAnnotation, setPinnedAnnotation] = useState(null);
61
+ const [hoveredExemplar, setHoveredExemplar] = useState(null);
62
+ const [pinnedExemplar, setPinnedExemplar] = useState(null);
63
+ const [pinnedExemplarPos, setPinnedExemplarPos] = useState(null);
50
64
  const [pinnedAnnotationPos, setPinnedAnnotationPos] = useState(null);
51
65
  const { timeZone, formatWithUserTimeZone } = useTimeZone();
52
66
  const getTimezoneAwareAxisFormatter = useCallback((rangeMs)=>createTimezoneAwareAxisFormatter(rangeMs, timeZone), [
53
67
  timeZone
54
68
  ]);
55
- let timeScale;
56
- if (timeScaleProp === undefined) {
69
+ const timeScale = useMemo(()=>{
70
+ if (timeScaleProp) return timeScaleProp;
57
71
  const commonTimeScale = getCommonTimeScale(data);
58
- if (commonTimeScale === undefined) {
59
- // set default to past 5 years
60
- const today = new Date();
61
- const pastDate = new Date(today);
62
- pastDate.setFullYear(today.getFullYear() - 5);
63
- const todayMs = today.getTime();
64
- const pastDateMs = pastDate.getTime();
65
- timeScale = {
66
- startMs: pastDateMs,
67
- endMs: todayMs,
68
- stepMs: 1,
69
- rangeMs: todayMs - pastDateMs
70
- };
71
- } else {
72
- timeScale = commonTimeScale;
72
+ if (commonTimeScale) return commonTimeScale;
73
+ // Set the empty chart's default range to the past five years.
74
+ const today = new Date();
75
+ const pastDate = new Date(today);
76
+ pastDate.setFullYear(today.getFullYear() - 5);
77
+ const startMs = pastDate.getTime();
78
+ const endMs = today.getTime();
79
+ return {
80
+ startMs,
81
+ endMs,
82
+ stepMs: 1,
83
+ rangeMs: endMs - startMs
84
+ };
85
+ }, [
86
+ data,
87
+ timeScaleProp
88
+ ]);
89
+ // EChart resizes whenever its sx identity changes. Keep sx stable and observe
90
+ // actual container dimensions so hover/pinning never relayouts every series.
91
+ const handleResize = useCallback(({ width, height: containerHeight })=>{
92
+ const chart = chartRef.current;
93
+ if (width && containerHeight && chart && (chart.getWidth() !== width || chart.getHeight() !== containerHeight)) {
94
+ chart.resize({
95
+ width,
96
+ height: containerHeight
97
+ });
73
98
  }
74
- } else {
75
- timeScale = timeScaleProp;
76
- }
99
+ }, []);
100
+ const { ref: containerRef } = useResizeObserver({
101
+ onResize: handleResize
102
+ });
77
103
  useImperativeHandle(ref, ()=>{
78
104
  return {
79
105
  highlightSeries ({ name }) {
@@ -120,12 +146,24 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
120
146
  enableDataZoom(chartRef.current);
121
147
  }
122
148
  },
123
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
124
149
  mouseover: (params)=>{
150
+ if (params.componentType === 'series' && params.seriesType === 'scatter' && 'seriesId' in params && typeof params.seriesId === 'string' && params.seriesId.startsWith(EXEMPLAR_SERIES_ID_PREFIX)) {
151
+ // This series ID identifies the metadata embedded by getExemplarSeries.
152
+ const point = params.data;
153
+ if (point?.exemplar) {
154
+ setHoveredExemplar({
155
+ exemplar: point.exemplar,
156
+ seriesLabels: point.seriesLabels,
157
+ plottedValue: point.value?.[1] ?? point.exemplar.value
158
+ });
159
+ return;
160
+ }
161
+ }
162
+ setHoveredExemplar(null);
125
163
  // Only markPoint (triangles under the X-axis) opens the annotation tooltip.
126
164
  // Hovering markLine or anything else keeps the regular TimeSeries tooltip visible
127
165
  // and clears any stale hovered annotation (mouseout is sometimes missed by ECharts).
128
- if (annotations && params.componentType === 'markPoint' && params.data?.annotationIndex !== undefined) {
166
+ if (annotations && params.componentType === 'markPoint' && typeof params.data?.annotationIndex === 'number') {
129
167
  const matchedAnnotation = annotations[params.data.annotationIndex] || null;
130
168
  if (matchedAnnotation) {
131
169
  setHoveredAnnotation(matchedAnnotation);
@@ -134,9 +172,12 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
134
172
  }
135
173
  setHoveredAnnotation(null);
136
174
  },
137
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
175
  mouseout: (params)=>{
139
- if (annotations && params.componentType === 'markPoint' && params.data?.annotationIndex !== undefined && annotations) {
176
+ if (params.componentType === 'series' && params.seriesType === 'scatter' && 'seriesId' in params && typeof params.seriesId === 'string' && params.seriesId.startsWith(EXEMPLAR_SERIES_ID_PREFIX)) {
177
+ setHoveredExemplar(null);
178
+ return;
179
+ }
180
+ if (annotations && params.componentType === 'markPoint' && typeof params.data?.annotationIndex === 'number' && annotations) {
140
181
  // Only clear if the mouseout corresponds to the currently hovered annotation, so that
141
182
  // a quick move from one markPoint to another isn't cancelled by a late mouseout event.
142
183
  const leaving = annotations[params.data.annotationIndex] || null;
@@ -148,6 +189,7 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
148
189
  // Cursor left the chart canvas — guarantee the annotation tooltip is dismissed.
149
190
  setHoveredAnnotation(null);
150
191
  }
192
+ setHoveredExemplar(null);
151
193
  }
152
194
  };
153
195
  }, [
@@ -155,43 +197,47 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
155
197
  onDataZoom
156
198
  ]);
157
199
  // Generate annotation series for ECharts markArea (range), markLine (point), and markPoint (markers under X-axis)
158
- const annotationSeries = useMemo(()=>buildAnnotationSeries(annotations), [
159
- annotations
200
+ const annotationYAxisIndex = Array.isArray(yAxis) ? yAxis.length : 1;
201
+ const annotationSeries = useMemo(()=>buildAnnotationSeries(annotations, annotationYAxisIndex), [
202
+ annotations,
203
+ annotationYAxisIndex
204
+ ]);
205
+ const preparedExemplarSeries = useMemo(()=>exemplars?.map(getExemplarSeries) ?? [], [
206
+ exemplars
207
+ ]);
208
+ const exemplarSeries = useMemo(()=>{
209
+ const visibleIds = new Set(seriesMapping.map((series)=>`${EXEMPLAR_SERIES_ID_PREFIX}${series.id}`));
210
+ return preparedExemplarSeries.filter((series)=>visibleIds.has(String(series.id)));
211
+ }, [
212
+ preparedExemplarSeries,
213
+ seriesMapping
160
214
  ]);
161
215
  const { noDataOption } = chartsTheme;
162
- const option = useMemo(()=>{
163
- // The "chart" `noDataVariant` is only used when the `timeSeries` is an
164
- // empty array because a `null` value will throw an error.
165
- if (data === null || data.length === 0 && noDataVariant === 'message') return noDataOption;
166
- // Utilizes ECharts dataset so raw data is separate from series option style properties
167
- // https://apache.github.io/echarts-handbook/en/concepts/dataset/
168
- const dataset = [];
169
- data.map((d, index)=>{
170
- const values = d.values.map(([timestamp, value])=>{
171
- const val = value === null ? '-' : value; // echarts use '-' to represent null data
172
- return [
173
- timestamp,
174
- val
175
- ];
176
- });
177
- dataset.push({
216
+ // Reuse the dataset when pinning a crosshair or updating annotations/axes.
217
+ const dataset = useMemo(()=>data?.map((series, index)=>({
178
218
  id: index,
179
- source: [
180
- ...values
181
- ],
182
219
  dimensions: [
183
220
  'time',
184
221
  'value'
185
- ]
186
- });
187
- });
222
+ ],
223
+ // ECharts treats null as missing data; no per-point conversion/copy is needed.
224
+ source: series.values
225
+ })) ?? [], [
226
+ data
227
+ ]);
228
+ const option = useMemo(()=>{
229
+ // The "chart" `noDataVariant` is only used when the `timeSeries` is an
230
+ // empty array because a `null` value will throw an error.
231
+ if (data === null || data.length === 0 && noDataVariant === 'message') return noDataOption;
188
232
  const updatedSeriesMapping = enablePinning && pinnedCrosshair !== null ? [
189
233
  ...seriesMapping,
190
234
  pinnedCrosshair,
191
- ...annotationSeries
235
+ ...annotationSeries,
236
+ ...exemplarSeries
192
237
  ] : [
193
238
  ...seriesMapping,
194
- ...annotationSeries
239
+ ...annotationSeries,
240
+ ...exemplarSeries
195
241
  ];
196
242
  const option = {
197
243
  dataset: dataset,
@@ -209,7 +255,12 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
209
255
  }
210
256
  },
211
257
  // If yAxis is already an array (multiple Y axes), use it directly; otherwise use getFormattedAxis
212
- yAxis: Array.isArray(yAxis) ? yAxis : getFormattedAxis(yAxis, format),
258
+ yAxis: [
259
+ ...Array.isArray(yAxis) ? yAxis : getFormattedAxis(yAxis, format),
260
+ ...annotationSeries.length > 0 ? [
261
+ ANNOTATION_Y_AXIS
262
+ ] : []
263
+ ],
213
264
  animation: false,
214
265
  tooltip: {
215
266
  show: true,
@@ -243,8 +294,10 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
243
294
  return option;
244
295
  }, [
245
296
  data,
297
+ dataset,
246
298
  seriesMapping,
247
299
  annotationSeries,
300
+ exemplarSeries,
248
301
  timeScale,
249
302
  yAxis,
250
303
  format,
@@ -257,26 +310,30 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
257
310
  pinnedCrosshair,
258
311
  getTimezoneAwareAxisFormatter
259
312
  ]);
260
- // Update adjacent charts so tooltip is unpinned when current chart is clicked.
261
- useEffect(()=>{
262
- // Only allow pinning one tooltip at a time, subsequent tooltip click unpins previous.
263
- // Multiple tooltips can only be pinned if Ctrl or Cmd key is pressed while clicking.
264
- const multipleTooltipsPinned = tooltipPinnedCoords !== null && lastTooltipPinnedCoords !== null;
265
- if (multipleTooltipsPinned) {
266
- if (!isEqual(lastTooltipPinnedCoords, tooltipPinnedCoords)) {
267
- setTooltipPinnedCoords(null);
268
- if (tooltipPinnedCoords !== null && pinnedCrosshair !== null) {
269
- setPinnedCrosshair(null);
270
- }
271
- }
272
- }
273
- // tooltipPinnedCoords CANNOT be in dep array or tooltip pinning breaks in the current chart's onClick
274
- // eslint-disable-next-line react-hooks/exhaustive-deps
275
- }, [
313
+ // Only changes from another chart (or new series) can clear the local pin.
314
+ // A local click may update its coordinates without updating the shared pin.
315
+ const [previousPinState, setPreviousPinState] = useState({
276
316
  lastTooltipPinnedCoords,
277
317
  seriesMapping
278
- ]);
318
+ });
319
+ if (previousPinState.lastTooltipPinnedCoords !== lastTooltipPinnedCoords || previousPinState.seriesMapping !== seriesMapping) {
320
+ setPreviousPinState({
321
+ lastTooltipPinnedCoords,
322
+ seriesMapping
323
+ });
324
+ if (tooltipPinnedCoords !== null && lastTooltipPinnedCoords !== null && !isEqual(lastTooltipPinnedCoords, tooltipPinnedCoords)) {
325
+ setTooltipPinnedCoords(null);
326
+ setPinnedCrosshair(null);
327
+ }
328
+ // A pinned exemplar tooltip is also unpinned when a tooltip is pinned in another chart,
329
+ // unless it is the one just pinned by this chart at these exact coordinates.
330
+ if (pinnedExemplarPos !== null && lastTooltipPinnedCoords !== null && !isEqual(lastTooltipPinnedCoords, pinnedExemplarPos)) {
331
+ setPinnedExemplar(null);
332
+ setPinnedExemplarPos(null);
333
+ }
334
+ }
279
335
  return /*#__PURE__*/ _jsxs(Box, {
336
+ ref: containerRef,
280
337
  style: {
281
338
  height
282
339
  },
@@ -285,6 +342,51 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
285
342
  // e.preventDefault(); // Prevent the default behaviour when right clicked
286
343
  // }}
287
344
  onClick: (e)=>{
345
+ // Allows user to opt-in to multi tooltip pinning when Ctrl or Cmd key held down
346
+ const isControlKeyPressed = e.ctrlKey || e.metaKey;
347
+ if (isControlKeyPressed) {
348
+ e.preventDefault();
349
+ }
350
+ // If clicking while hovering an exemplar marker, toggle the exemplar tooltip pin
351
+ // instead of pinning the TimeChartTooltip. Pinning an exemplar tooltip unpins the
352
+ // pinned TimeChartTooltip, so only one tooltip stays pinned at a time unless
353
+ // Ctrl or Cmd is held down.
354
+ if (hoveredExemplar !== null && e.target instanceof HTMLCanvasElement) {
355
+ const pinnedPos = {
356
+ page: {
357
+ x: e.pageX,
358
+ y: e.pageY
359
+ },
360
+ client: {
361
+ x: e.clientX,
362
+ y: e.clientY
363
+ },
364
+ plotCanvas: {
365
+ x: e.nativeEvent.offsetX,
366
+ y: e.nativeEvent.offsetY
367
+ },
368
+ target: e.target
369
+ };
370
+ const isUnpinClick = pinnedExemplar !== null && pinnedExemplar.exemplar === hoveredExemplar.exemplar;
371
+ // Compute the next state first and call the setters separately: updater functions
372
+ // may run more than once (e.g. StrictMode), so they must stay side-effect free.
373
+ setPinnedExemplar(isUnpinClick ? null : hoveredExemplar);
374
+ setPinnedExemplarPos(isUnpinClick ? null : pinnedPos);
375
+ if (!isUnpinClick && !isControlKeyPressed) {
376
+ // Unpin the pinned TimeChartTooltip and let adjacent charts know a tooltip is
377
+ // pinned at these coordinates, so only one tooltip is pinned at a time.
378
+ setTooltipPinnedCoords(null);
379
+ setPinnedCrosshair(null);
380
+ setLastTooltipPinnedCoords(pinnedPos);
381
+ }
382
+ return;
383
+ }
384
+ // Unpin a pinned exemplar tooltip when clicking elsewhere on the chart canvas,
385
+ // so the same click can pin the TimeChartTooltip instead. Ctrl or Cmd keeps both.
386
+ if (pinnedExemplar !== null && !isControlKeyPressed && e.target instanceof HTMLCanvasElement) {
387
+ setPinnedExemplar(null);
388
+ setPinnedExemplarPos(null);
389
+ }
288
390
  // If clicking while hovering an annotation, toggle the annotation tooltip pin
289
391
  // instead of pinning the TimeChartTooltip, so pinned TimeChartTooltip is preserved.
290
392
  if (hoveredAnnotation !== null && e.target instanceof HTMLCanvasElement) {
@@ -313,11 +415,6 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
313
415
  });
314
416
  return;
315
417
  }
316
- // Allows user to opt-in to multi tooltip pinning when Ctrl or Cmd key held down
317
- const isControlKeyPressed = e.ctrlKey || e.metaKey;
318
- if (isControlKeyPressed) {
319
- e.preventDefault();
320
- }
321
418
  // Determine where on chart canvas to plot pinned crosshair as markLine.
322
419
  const pointInGrid = getPointInGrid(e.nativeEvent.offsetX, e.nativeEvent.offsetY, chartRef.current);
323
420
  if (pointInGrid === null) {
@@ -388,6 +485,28 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
388
485
  return;
389
486
  }
390
487
  const { clientX } = e;
488
+ // ECharts does not reliably emit mouseout when the cursor moves off an exemplar marker
489
+ // onto the plain plot area, which would keep the exemplar tooltip stuck on screen and
490
+ // hide the regular TimeChartTooltip. Clear the hovered exemplar as soon as the cursor
491
+ // is no longer within its marker, so the next hover can take over.
492
+ setHoveredExemplar((current)=>{
493
+ if (current === null || chartRef.current === undefined) return current;
494
+ let markerPixel;
495
+ try {
496
+ // Use the plotted value so negated markers (negativeY) are tracked correctly.
497
+ markerPixel = chartRef.current.convertToPixel('grid', [
498
+ current.exemplar.timestamp,
499
+ current.plottedValue
500
+ ]);
501
+ } catch {
502
+ // Coordinates cannot be resolved (e.g. grid not ready yet), keep the current hover.
503
+ return current;
504
+ }
505
+ const [markerPixelX, markerPixelY] = markerPixel ?? [];
506
+ if (markerPixelX === undefined || markerPixelY === undefined) return current;
507
+ const distance = Math.hypot(e.nativeEvent.offsetX - markerPixelX, e.nativeEvent.offsetY - markerPixelY);
508
+ return distance <= EXEMPLAR_HOVER_RADIUS ? current : null;
509
+ });
391
510
  if (isDragging) {
392
511
  const deltaX = clientX - startX;
393
512
  if (deltaX > 0) {
@@ -405,8 +524,9 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
405
524
  if (tooltipPinnedCoords === null) {
406
525
  setShowTooltip(false);
407
526
  }
408
- // Defensive: clear hovered annotation in case ECharts missed a mouseout event.
527
+ // Defensive: clear hovered annotation and exemplar in case ECharts missed a mouseout event.
409
528
  setHoveredAnnotation(null);
529
+ setHoveredExemplar(null);
410
530
  if (chartRef.current !== undefined) {
411
531
  clearHighlightedSeries(chartRef.current);
412
532
  }
@@ -429,7 +549,7 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
429
549
  }
430
550
  },
431
551
  children: [
432
- showTooltip === true && (tooltipPinnedCoords !== null || hoveredAnnotation === null) && option.tooltip?.showContent === false && tooltipConfig.hidden !== true && /*#__PURE__*/ _jsx(TimeChartTooltip, {
552
+ showTooltip === true && (tooltipPinnedCoords !== null || pinnedExemplar === null && hoveredAnnotation === null && hoveredExemplar === null) && option.tooltip?.showContent === false && tooltipConfig.hidden !== true && /*#__PURE__*/ _jsx(TimeChartTooltip, {
433
553
  containerId: chartsTheme.tooltipPortalContainerId,
434
554
  chartRef: chartRef,
435
555
  data: data,
@@ -446,6 +566,18 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
446
566
  setPinnedCrosshair(null);
447
567
  }
448
568
  }),
569
+ (pinnedExemplar !== null || hoveredExemplar !== null && tooltipPinnedCoords === null) && /*#__PURE__*/ _jsx(ExemplarMetadataTooltip, {
570
+ exemplar: (pinnedExemplar ?? hoveredExemplar).exemplar,
571
+ seriesLabels: (pinnedExemplar ?? hoveredExemplar).seriesLabels,
572
+ containerId: chartsTheme.tooltipPortalContainerId,
573
+ format: format,
574
+ pinnedPos: pinnedExemplar !== null ? pinnedExemplarPos : null,
575
+ enablePinning: isPinningEnabled,
576
+ onUnpinClick: ()=>{
577
+ setPinnedExemplar(null);
578
+ setPinnedExemplarPos(null);
579
+ }
580
+ }),
449
581
  (pinnedAnnotation ?? hoveredAnnotation) && /*#__PURE__*/ _jsx(AnnotationTooltip, {
450
582
  annotation: pinnedAnnotation ?? hoveredAnnotation,
451
583
  containerId: chartsTheme.tooltipPortalContainerId,
@@ -458,10 +590,7 @@ export const TimeSeriesChartBase = /*#__PURE__*/ forwardRef(function TimeChart({
458
590
  }
459
591
  }),
460
592
  /*#__PURE__*/ _jsx(EChart, {
461
- sx: {
462
- width: '100%',
463
- height: '100%'
464
- },
593
+ sx: CHART_SX,
465
594
  option: option,
466
595
  theme: chartsTheme.echartsTheme,
467
596
  onEvents: handleEvents,