@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.94 → 2.0.0-next.95

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 (29) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +153 -23
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +66 -49
  4. package/dist/building-blocks/chart-line/chart-line-base.d.ts +95 -33
  5. package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
  6. package/dist/building-blocks/chart-line/chart-line-base.js +88 -23
  7. package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
  8. package/dist/charthelpers/index.d.ts +1 -0
  9. package/dist/charthelpers/index.d.ts.map +1 -1
  10. package/dist/charthelpers/index.js +4 -0
  11. package/dist/charthelpers/index.js.map +1 -1
  12. package/dist/charthelpers/x-value.d.ts +72 -0
  13. package/dist/charthelpers/x-value.d.ts.map +1 -0
  14. package/dist/charthelpers/x-value.js +60 -0
  15. package/dist/charthelpers/x-value.js.map +1 -0
  16. package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts +16 -1
  17. package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
  18. package/dist/navigation-instruments/gauge-trend/gauge-trend.js +12 -0
  19. package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/bars-graphs/area-graph/area-graph.stories.ts +83 -1
  22. package/src/bars-graphs/line-graph/line-graph.stories.ts +148 -1
  23. package/src/building-blocks/chart-line/chart-line-base.stories.ts +1 -1
  24. package/src/building-blocks/chart-line/chart-line-base.ts +198 -70
  25. package/src/charthelpers/index.ts +1 -0
  26. package/src/charthelpers/x-value.spec.ts +102 -0
  27. package/src/charthelpers/x-value.ts +134 -0
  28. package/src/navigation-instruments/gauge-trend/gauge-trend.stories.ts +42 -0
  29. package/src/navigation-instruments/gauge-trend/gauge-trend.ts +38 -1
@@ -93,7 +93,7 @@ const meta: Meta = {
93
93
  // Axis and layout
94
94
  xAxisType: {
95
95
  control: {type: 'radio'},
96
- options: [XAxisType.category, XAxisType.time],
96
+ options: [XAxisType.category, XAxisType.time, XAxisType.number],
97
97
  },
98
98
  yAxisPosition: {
99
99
  control: {type: 'radio'},
@@ -188,6 +188,153 @@ export const SingleSeries: Story = {
188
188
  `,
189
189
  };
190
190
 
191
+ const UNEVEN_TIME_DATA = [
192
+ {x: '2026-07-06T10:00:00Z', value: 10},
193
+ {x: '2026-07-06T10:02:00Z', value: 14},
194
+ {x: '2026-07-06T10:03:00Z', value: 12},
195
+ {x: '2026-07-06T10:10:00Z', value: 18},
196
+ {x: '2026-07-06T10:30:00Z', value: 8},
197
+ {x: '2026-07-06T11:00:00Z', value: 15},
198
+ ];
199
+
200
+ export const UnevenTimeIntervals: Story = {
201
+ name: 'Uneven Time Intervals (Time Axis)',
202
+ play: async ({canvasElement}) => {
203
+ await document.fonts.ready;
204
+ const chart = canvasElement.querySelector('obc-line-graph') as
205
+ | (HTMLElement & {chart?: {update(): void}})
206
+ | null;
207
+ chart?.chart?.update();
208
+ },
209
+ args: {
210
+ xAxisType: XAxisType.time,
211
+ timeDisplay: TimeDisplay.minutes,
212
+ },
213
+ render: (_args) => html`
214
+ <obc-line-graph
215
+ .data=${UNEVEN_TIME_DATA}
216
+ .lineMode=${_args.lineMode}
217
+ .xAxisType=${_args.xAxisType}
218
+ .timeDisplay=${_args.timeDisplay}
219
+ .showGrid=${_args.showGrid}
220
+ .showGridX=${_args.showGridX}
221
+ .showGridY=${_args.showGridY}
222
+ .showTickMarks=${_args.showTickMarks}
223
+ .showPoints=${_args.showPoints}
224
+ .priority=${_args.priority}
225
+ .width=${_args.width}
226
+ .height=${_args.height}
227
+ ></obc-line-graph>
228
+ `,
229
+ };
230
+
231
+ const DATE_OBJECT_TIME_DATA = [
232
+ {x: new Date('2026-07-06T10:00:00Z'), value: 10},
233
+ {x: new Date('2026-07-06T10:04:00Z'), value: 14},
234
+ {x: '2026-07-06T10:12:00Z', value: 12},
235
+ {x: Date.parse('2026-07-06T10:30:00Z'), value: 18},
236
+ ];
237
+
238
+ export const DateObjectsTimeAxis: Story = {
239
+ name: 'Date Objects (Time Axis)',
240
+ play: async ({canvasElement}) => {
241
+ await document.fonts.ready;
242
+ const chart = canvasElement.querySelector('obc-line-graph') as
243
+ | (HTMLElement & {chart?: {update(): void}})
244
+ | null;
245
+ chart?.chart?.update();
246
+ },
247
+ args: {
248
+ xAxisType: XAxisType.time,
249
+ timeDisplay: TimeDisplay.minutes,
250
+ },
251
+ render: (_args) => html`
252
+ <obc-line-graph
253
+ .data=${DATE_OBJECT_TIME_DATA}
254
+ .lineMode=${_args.lineMode}
255
+ .xAxisType=${_args.xAxisType}
256
+ .timeDisplay=${_args.timeDisplay}
257
+ .showGrid=${_args.showGrid}
258
+ .showGridX=${_args.showGridX}
259
+ .showGridY=${_args.showGridY}
260
+ .showTickMarks=${_args.showTickMarks}
261
+ .showPoints=${_args.showPoints}
262
+ .priority=${_args.priority}
263
+ .width=${_args.width}
264
+ .height=${_args.height}
265
+ ></obc-line-graph>
266
+ `,
267
+ };
268
+
269
+ const NUMBER_AXIS_DATA = [
270
+ {x: 0, value: 2},
271
+ {x: 1, value: 5},
272
+ {x: 2.5, value: 3},
273
+ {x: 7, value: 9},
274
+ {x: 10, value: 6},
275
+ ];
276
+
277
+ export const NumberAxis: Story = {
278
+ name: 'Number X-Axis',
279
+ play: async ({canvasElement}) => {
280
+ await document.fonts.ready;
281
+ const chart = canvasElement.querySelector('obc-line-graph') as
282
+ | (HTMLElement & {chart?: {update(): void}})
283
+ | null;
284
+ chart?.chart?.update();
285
+ },
286
+ args: {
287
+ xAxisType: XAxisType.number,
288
+ },
289
+ render: (_args) => html`
290
+ <obc-line-graph
291
+ .data=${NUMBER_AXIS_DATA}
292
+ .lineMode=${_args.lineMode}
293
+ .xAxisType=${_args.xAxisType}
294
+ .showGrid=${_args.showGrid}
295
+ .showGridX=${_args.showGridX}
296
+ .showGridY=${_args.showGridY}
297
+ .showTickMarks=${_args.showTickMarks}
298
+ .showPoints=${_args.showPoints}
299
+ .priority=${_args.priority}
300
+ .width=${_args.width}
301
+ .height=${_args.height}
302
+ ></obc-line-graph>
303
+ `,
304
+ };
305
+
306
+ export const TemporalInput: Story = {
307
+ name: 'Temporal API Input',
308
+ tags: ['skip-test'],
309
+ render: () => {
310
+ const T = (
311
+ globalThis as {
312
+ Temporal?: {Instant: {from(s: string): {epochMilliseconds: number}}};
313
+ }
314
+ ).Temporal;
315
+ if (!T) {
316
+ return html`<p>
317
+ Temporal API is not available in this browser — pass ISO strings, Date
318
+ objects or epoch milliseconds instead.
319
+ </p>`;
320
+ }
321
+ const chart = document.createElement('obc-line-graph');
322
+ chart.xAxisType = XAxisType.time;
323
+ chart.timeDisplay = TimeDisplay.minutes;
324
+ chart.showGrid = true;
325
+ chart.showGridX = true;
326
+ chart.showGridY = true;
327
+ chart.showTickMarks = true;
328
+ chart.data = [
329
+ {x: T.Instant.from('2026-07-06T10:00:00Z'), value: 10},
330
+ {x: T.Instant.from('2026-07-06T10:04:00Z'), value: 14},
331
+ {x: T.Instant.from('2026-07-06T10:05:00Z'), value: 12},
332
+ {x: T.Instant.from('2026-07-06T10:20:00Z'), value: 18},
333
+ ];
334
+ return chart;
335
+ },
336
+ };
337
+
191
338
  export const SingleSeriesExternalScales: Story = {
192
339
  name: 'Single-Series Line Graph (with external scales)',
193
340
  tags: ['skip-test'],
@@ -210,7 +210,7 @@ Abstract base class for line and area chart components built on Chart.js.
210
210
  // Axis and layout
211
211
  xAxisType: {
212
212
  control: {type: 'radio'},
213
- options: [XAxisType.category, XAxisType.time],
213
+ options: [XAxisType.category, XAxisType.time, XAxisType.number],
214
214
  },
215
215
  yAxisPosition: {
216
216
  control: {type: 'radio'},
@@ -38,7 +38,11 @@ import {
38
38
  getChartTooltipOptions,
39
39
  generateLegendHTML,
40
40
  applyAlphaToColor,
41
+ normalizeXValue,
42
+ formatXValue,
43
+ XValueMode,
41
44
  } from '../../charthelpers/index.js';
45
+ import type {ChartXValue} from '../../charthelpers/x-value.js';
42
46
  import {
43
47
  EXTERNAL_SCALE_BORDER_RADIUS_CSS_VAR,
44
48
  readExternalScaleBorderRadiusPx,
@@ -94,9 +98,12 @@ interface ExternalScaleElement extends HTMLElement {
94
98
  borderRadiusPosition?: BorderRadiusPosition;
95
99
  }
96
100
 
101
+ export type {ChartXValue, TemporalLike} from '../../charthelpers/x-value.js';
102
+
97
103
  export enum XAxisType {
98
104
  category = 'category',
99
105
  time = 'time',
106
+ number = 'number',
100
107
  }
101
108
 
102
109
  export enum YAxisPosition {
@@ -115,8 +122,17 @@ export enum TimeDisplay {
115
122
  date = 'date',
116
123
  }
117
124
 
125
+ export type ChartLinePoint = number | {x: ChartXValue; y: number};
126
+
118
127
  export type ChartLineDataItem = {
119
- label: string;
128
+ /** Category label. Used when `xAxisType='category'` (the default). */
129
+ label?: string;
130
+ /**
131
+ * X-coordinate for `xAxisType='time'` (epoch ms, ISO string, Date, or
132
+ * Temporal object) or `xAxisType='number'` (plain number). When absent,
133
+ * `label` is parsed as a fallback.
134
+ */
135
+ x?: ChartXValue;
120
136
  value: number;
121
137
  };
122
138
 
@@ -170,7 +186,9 @@ const LINE_GRAPH_RECREATE_PROP_NAMES = [
170
186
  *
171
187
  * ## Features
172
188
  * - **Single or multi-series**: Use `data` for simple single-series or `datasets` for multi-series charts
173
- * - **Time and category axes**: Supports `category` x-axis (labels) and `time` x-axis (ISO dates or timestamps)
189
+ * - **Category, time and number axes**: `category` (labels, evenly spaced), `time` (epoch ms,
190
+ * ISO strings, `Date` or Temporal objects — positioned proportionally, so uneven intervals
191
+ * render unevenly) and `number` (plain numeric x-values on a linear scale)
174
192
  * - **Line styles**: Choose `smooth` (curved), `straight`, or `stepped` line rendering
175
193
  * - **Fill modes**: Area fills with `semitransparent`, `solid`, or `threshold` (red/blue above/below midpoint)
176
194
  * - **Stacked charts**: Enable `stacked` for multi-series datasets to stack values on y-axis
@@ -219,6 +237,36 @@ const LINE_GRAPH_RECREATE_PROP_NAMES = [
219
237
  * </script>
220
238
  * ```
221
239
  *
240
+ * Single-series with time axis (uneven intervals position proportionally;
241
+ * x accepts epoch ms, ISO strings, Date or Temporal objects):
242
+ * ```html
243
+ * <obc-line-graph></obc-line-graph>
244
+ * <script>
245
+ * const chart = document.querySelector('obc-line-graph');
246
+ * chart.xAxisType = 'time';
247
+ * chart.timeDisplay = 'minutes';
248
+ * chart.data = [
249
+ * {x: '2026-07-06T10:00:00Z', value: 10},
250
+ * {x: new Date('2026-07-06T10:03:00Z'), value: 14},
251
+ * {x: Temporal.Instant.from('2026-07-06T10:15:00Z'), value: 12}
252
+ * ];
253
+ * </script>
254
+ * ```
255
+ *
256
+ * Single-series with numeric x-axis:
257
+ * ```html
258
+ * <obc-line-graph></obc-line-graph>
259
+ * <script>
260
+ * const chart = document.querySelector('obc-line-graph');
261
+ * chart.xAxisType = 'number';
262
+ * chart.data = [
263
+ * {x: 0, value: 2},
264
+ * {x: 2.5, value: 3},
265
+ * {x: 10, value: 6}
266
+ * ];
267
+ * </script>
268
+ * ```
269
+ *
222
270
  * Stacked area chart with solid fill:
223
271
  * ```html
224
272
  * <obc-line-graph></obc-line-graph>
@@ -263,12 +311,12 @@ const LINE_GRAPH_RECREATE_PROP_NAMES = [
263
311
  * </script>
264
312
  * ```
265
313
  *
266
- * @property {Array<{label: string, value: number}>} data - Single-series data array. Each object must have `label` (string) and `value` (number). Used when `datasets` is not provided.
267
- * @property {ChartDataset<'line', (number | {x: string|number|Date; y: number})[]>[]} datasets - Multi-series Chart.js datasets. Takes precedence over `data`. Each dataset can have `label`, `data` (numeric array or `{x, y}` points), and visual properties like `borderColor`, `backgroundColor`, `fill`, etc.
314
+ * @property {Array<{label?: string, x?: number|string|Date|TemporalLike, value: number}>} data - Single-series data array. In `category` mode each item needs `label`; in `time`/`number` mode each item needs `x` (epoch ms, ISO string, Date, or Temporal object — `label` is parsed as a fallback). Used when `datasets` is not provided. Points are drawn in array order (no sorting); Temporal Plain* values are interpreted in the system time zone.
315
+ * @property {ChartDataset<'line', (number | {x: number|string|Date|TemporalLike; y: number})[]>[]} datasets - Multi-series Chart.js datasets. Takes precedence over `data`. Each dataset can have `label`, `data` (numeric array or `{x, y}` points), and visual properties like `borderColor`, `backgroundColor`, `fill`, etc. In `time`/`number` mode point x-values are normalized like single-series `x`.
268
316
  * @property {(string|number)[]} labels - Explicit labels for category x-axis. If omitted, labels are derived from `data` property or dataset x-values.
269
317
  * @property {string[]} colors - Custom color palette (CSS variable names or color strings). Falls back to theme default colors if not provided.
270
- * @property {'category'|'time'} xAxisType - X-axis mode. `'category'` for labeled data points, `'time'` for time-based data (ISO strings or timestamps). Default: `'category'`.
271
- * @property {'minutes'|'date'} timeDisplay - Time axis label format when `xAxisType='time'`. `'date'` shows full date/time, `'minutes'` shows minutes relative to first data point. Default: `'date'`.
318
+ * @property {'category'|'time'|'number'} xAxisType - X-axis mode. `'category'` for labeled, evenly spaced data points; `'time'` for time-based data positioned proportionally (numbers are always epoch ms — `xStepSize`/`xTicksLimit` operate in ms); `'number'` for plain numeric x-values. Default: `'category'`.
319
+ * @property {'minutes'|'date'} timeDisplay - Time axis label format when `xAxisType='time'`. `'date'` shows a locale date, `'minutes'` shows minutes relative to the latest data point. Default: `'date'`.
272
320
  * @property {'left'|'right'} yAxisPosition - Single y-axis position. Use this for simple charts with one y-axis. For multiple y-axes, use `yAxes` property instead. Default: `'left'`.
273
321
  * @property {Array<{id?: string; position?: 'left'|'right'; min?: number; max?: number; grid?: boolean}>} yAxes - Multiple y-axis definitions for complex charts. Each axis can specify `id` (referenced by dataset `yAxisID`), `position`, `min`/`max` range, and `grid` visibility.
274
322
  * @property {boolean} showGrid - Show vertical grid lines (x-axis). When combined with `showGridX` and `showGridY`, controls full grid visibility. Default: `false`.
@@ -293,16 +341,17 @@ const LINE_GRAPH_RECREATE_PROP_NAMES = [
293
341
  * @experimental
294
342
  */
295
343
  export class ObcChartLineBase extends LitElement {
296
- /** Simple single-series data (array of {label, value}). */
344
+ /**
345
+ * Simple single-series data. `{label, value}` items for the category axis;
346
+ * `{x, value}` items for time/number axes (x: epoch ms, ISO string, Date,
347
+ * or Temporal object). Points are drawn in array order (no sorting).
348
+ */
297
349
  @property({type: Array, attribute: false})
298
350
  data: ChartLineDataItem[] = [];
299
351
 
300
352
  /** Chart.js-style datasets for multi-series use. If provided, takes precedence over `data`. */
301
353
  @property({type: Array, attribute: false})
302
- datasets?: ChartDataset<
303
- 'line',
304
- (number | {x: string | number | Date; y: number})[]
305
- >[] = undefined;
354
+ datasets?: ChartDataset<'line', ChartLinePoint[]>[] = undefined;
306
355
 
307
356
  /** Optional explicit labels for the x-axis (category mode). If omitted labels are derived from `data` */
308
357
  @property({type: Array, attribute: false})
@@ -346,7 +395,11 @@ export class ObcChartLineBase extends LitElement {
346
395
  @property({type: Number})
347
396
  scaleReferenceSize = 384;
348
397
 
349
- /** X-axis mode: 'category' for labeled data points, 'time' for time-based data. */
398
+ /**
399
+ * X-axis mode: 'category' for labeled, evenly spaced data points; 'time'
400
+ * for time-based data positioned proportionally; 'number' for plain
401
+ * numeric x-values.
402
+ */
350
403
  @property({type: String})
351
404
  xAxisType: XAxisType = XAxisType.category;
352
405
 
@@ -400,6 +453,33 @@ export class ObcChartLineBase extends LitElement {
400
453
  @property({type: Boolean, attribute: false})
401
454
  hasLabelPadding = true;
402
455
 
456
+ /** @internal - True when the x-axis positions points by numeric value. */
457
+ protected get isNumericXAxis(): boolean {
458
+ return (
459
+ this.xAxisType === XAxisType.time || this.xAxisType === XAxisType.number
460
+ );
461
+ }
462
+
463
+ /** @internal - Normalization mode for the current x-axis type. */
464
+ protected get xValueMode(): XValueMode {
465
+ return this.xAxisType === XAxisType.number
466
+ ? XValueMode.number
467
+ : XValueMode.time;
468
+ }
469
+
470
+ /** @internal - Last data/datasets reference already warned about. */
471
+ private lastWarnedXSource?: unknown;
472
+
473
+ /** @internal - Warn once per data assignment about unparseable x-values. */
474
+ private warnOnInvalidX(xValues: number[], sourceRef: unknown) {
475
+ const invalid = xValues.filter((x) => !Number.isFinite(x)).length;
476
+ if (invalid === 0 || this.lastWarnedXSource === sourceRef) return;
477
+ this.lastWarnedXSource = sourceRef;
478
+ console.warn(
479
+ `[obc-chart] ${invalid} x value(s) could not be parsed for xAxisType='${this.xAxisType}'; the points render as gaps.`
480
+ );
481
+ }
482
+
403
483
  // Internal default tension used when `lineMode` is 'smooth'. Not exposed as a property.
404
484
  private readonly DEFAULT_TENSION = 0.4;
405
485
 
@@ -1825,28 +1905,21 @@ export class ObcChartLineBase extends LitElement {
1825
1905
  protected buildDataset(
1826
1906
  data:
1827
1907
  | number[]
1828
- | ChartDataset<
1829
- 'line',
1830
- (number | {x: string | number | Date; y: number})[]
1831
- >,
1908
+ | {x: number; y: number}[]
1909
+ | ChartDataset<'line', ChartLinePoint[]>,
1832
1910
  index: number,
1833
1911
  chartColors: string[],
1834
1912
  totalCount = 1
1835
- ): ChartDataset<
1836
- 'line',
1837
- number[] | (number | {x: string | number | Date; y: number})[]
1838
- > {
1913
+ ): ChartDataset<'line', ChartLinePoint[]> {
1839
1914
  const currentColor = chartColors[index % chartColors.length];
1840
1915
 
1841
- // Check if input is existing dataset (has 'data' property) or raw values array
1842
- const existingDataset =
1843
- 'data' in (data as object)
1844
- ? (data as ChartDataset<
1845
- 'line',
1846
- (number | {x: string | number | Date; y: number})[]
1847
- >)
1848
- : null;
1849
- const values = existingDataset ? null : (data as number[]);
1916
+ // Raw input is an array (values or points); anything else is an existing dataset
1917
+ const existingDataset = Array.isArray(data)
1918
+ ? null
1919
+ : (data as ChartDataset<'line', ChartLinePoint[]>);
1920
+ const values = Array.isArray(data)
1921
+ ? (data as number[] | {x: number; y: number}[])
1922
+ : null;
1850
1923
 
1851
1924
  const borderColor = existingDataset?.borderColor ?? currentColor;
1852
1925
  const fillFlag = existingDataset?.fill ?? this.shouldApplyFill();
@@ -1905,33 +1978,34 @@ export class ObcChartLineBase extends LitElement {
1905
1978
  }),
1906
1979
  };
1907
1980
 
1908
- return result as ChartDataset<
1909
- 'line',
1910
- number[] | (number | {x: string | number | Date; y: number})[]
1911
- >;
1981
+ return result as ChartDataset<'line', ChartLinePoint[]>;
1912
1982
  }
1913
1983
 
1914
1984
  /**
1915
- * Create threshold mode datasets: invisible baseline + main dataset with above/below fills
1985
+ * Create threshold mode datasets: invisible baseline + main dataset with
1986
+ * above/below fills. Accepts plain values (category mode) or {x, y} points
1987
+ * (time/number mode); the baseline mirrors the input x-positions.
1916
1988
  */
1917
1989
  protected createThresholdDatasets(
1918
- values: number[],
1990
+ values: number[] | {x: number; y: number}[],
1919
1991
  chartColors: string[]
1920
- ): ChartDataset<'line', number[]>[] {
1921
- const numericValues = values
1922
- .map((v) => Number(v))
1992
+ ): ChartDataset<'line', ChartLinePoint[]>[] {
1993
+ const yValues = values
1994
+ .map((v) => (typeof v === 'number' ? v : v.y))
1923
1995
  .filter((n) => Number.isFinite(n));
1924
- const minV = numericValues.length ? Math.min(...numericValues) : 0;
1925
- const maxV = numericValues.length ? Math.max(...numericValues) : 100;
1996
+ const minV = yValues.length ? Math.min(...yValues) : 0;
1997
+ const maxV = yValues.length ? Math.max(...yValues) : 100;
1926
1998
  const threshold = (minV + maxV) / 2;
1927
- const baselineData = numericValues.map(() => threshold);
1999
+ const baselineData: ChartLinePoint[] = values.map((v) =>
2000
+ typeof v === 'number' ? threshold : {x: v.x, y: threshold}
2001
+ );
1928
2002
 
1929
2003
  const lowRaw = LINE_GRAPH_GRID_CONFIG.thresholdLowColorVar;
1930
2004
  const highRaw = LINE_GRAPH_GRID_CONFIG.thresholdHighColorVar;
1931
2005
  const highFill = applyAlphaToColor(this, highRaw, 0.35);
1932
2006
  const lowFill = applyAlphaToColor(this, lowRaw, 0.35);
1933
2007
 
1934
- const baselineDataset: ChartDataset<'line', number[]> = {
2008
+ const baselineDataset: ChartDataset<'line', ChartLinePoint[]> = {
1935
2009
  label: 'threshold-baseline',
1936
2010
  data: baselineData,
1937
2011
  borderColor: 'transparent',
@@ -1943,10 +2017,7 @@ export class ObcChartLineBase extends LitElement {
1943
2017
  spanGaps: true,
1944
2018
  };
1945
2019
 
1946
- const main = this.buildDataset(values, 0, chartColors) as ChartDataset<
1947
- 'line',
1948
- number[]
1949
- >;
2020
+ const main = this.buildDataset(values, 0, chartColors);
1950
2021
  (main as unknown as Record<string, unknown>).fill = {
1951
2022
  target: 0,
1952
2023
  above: highFill,
@@ -1959,7 +2030,9 @@ export class ObcChartLineBase extends LitElement {
1959
2030
  }
1960
2031
 
1961
2032
  /**
1962
- * Prepare normalized datasets for multi-series charts
2033
+ * Prepare normalized datasets for multi-series charts.
2034
+ * In time/number mode every point's x is normalized (epoch ms / number)
2035
+ * so strings, Dates and Temporal objects position correctly.
1963
2036
  */
1964
2037
  protected prepareMultiSeriesDatasets() {
1965
2038
  const defaultPalette =
@@ -1972,19 +2045,48 @@ export class ObcChartLineBase extends LitElement {
1972
2045
  defaultPalette
1973
2046
  );
1974
2047
 
2048
+ // Normalize first, then warn once per assignment with the aggregate
2049
+ // count across ALL series — warning inside the per-dataset loop would
2050
+ // either flood the console or (with ref-based dedup) silently swallow
2051
+ // failures in every series after the first.
2052
+ const normalized = this.datasets!.map((ds) => this.normalizeDatasetX(ds));
2053
+ if (this.isNumericXAxis) {
2054
+ this.warnOnInvalidX(
2055
+ normalized.flatMap((ds) =>
2056
+ (ds.data ?? []).map((pt) =>
2057
+ pt && typeof pt === 'object' ? (pt.x as number) : 0
2058
+ )
2059
+ ),
2060
+ this.datasets
2061
+ );
2062
+ }
2063
+
1975
2064
  const totalCount = this.datasets!.length;
1976
- return this.datasets!.map((ds, i) =>
2065
+ return normalized.map((ds, i) =>
1977
2066
  this.buildDataset(ds, i, chartColors, totalCount)
1978
2067
  );
1979
2068
  }
1980
2069
 
2070
+ /** @internal - Return a copy of the dataset with normalized point x-values. */
2071
+ private normalizeDatasetX(
2072
+ ds: ChartDataset<'line', ChartLinePoint[]>
2073
+ ): ChartDataset<'line', ChartLinePoint[]> {
2074
+ if (!this.isNumericXAxis || !ds.data) return ds;
2075
+ const data = ds.data.map((pt) =>
2076
+ pt && typeof pt === 'object' && 'x' in pt
2077
+ ? {...pt, x: normalizeXValue(pt.x, this.xValueMode)}
2078
+ : pt
2079
+ );
2080
+ return {...ds, data};
2081
+ }
2082
+
1981
2083
  /**
1982
- * Prepare datasets for single-series charts
1983
- * Handles both regular and threshold fill modes
2084
+ * Prepare datasets for single-series charts.
2085
+ * Category mode: labels + numeric values (unchanged legacy path).
2086
+ * Time/number mode: normalized {x, y} points on a linear scale.
2087
+ * Handles both regular and threshold fill modes.
1984
2088
  */
1985
2089
  protected prepareSingleSeriesDatasets() {
1986
- const values = this.data.map((d) => d.value);
1987
- const labels = this.data.map((d) => d.label);
1988
2090
  const defaultPalette =
1989
2091
  this.priority === Priority.enhanced
1990
2092
  ? CHART_SECTOR_ENHANCED_COLORS
@@ -1997,6 +2099,25 @@ export class ObcChartLineBase extends LitElement {
1997
2099
  const fill = this.shouldApplyFill();
1998
2100
  const fillMode = this.getFillMode();
1999
2101
 
2102
+ if (this.isNumericXAxis) {
2103
+ const points = this.data.map((d) => ({
2104
+ x: normalizeXValue(d.x ?? d.label ?? NaN, this.xValueMode),
2105
+ y: d.value,
2106
+ }));
2107
+ this.warnOnInvalidX(
2108
+ points.map((p) => p.x),
2109
+ this.data
2110
+ );
2111
+ const datasets =
2112
+ fill && fillMode === 'threshold'
2113
+ ? this.createThresholdDatasets(points, chartColors)
2114
+ : [this.buildDataset(points, 0, chartColors)];
2115
+ return {datasets, labels: [] as (string | number)[]};
2116
+ }
2117
+
2118
+ const values = this.data.map((d) => d.value);
2119
+ const labels = this.data.map((d) => d.label ?? String(d.x ?? ''));
2120
+
2000
2121
  const datasets =
2001
2122
  fill && fillMode === 'threshold'
2002
2123
  ? this.createThresholdDatasets(values, chartColors)
@@ -2118,13 +2239,22 @@ export class ObcChartLineBase extends LitElement {
2118
2239
  callbacks: {
2119
2240
  title: () => '',
2120
2241
  label: (context) => {
2121
- const label = context.label ?? '';
2122
2242
  const value =
2123
2243
  typeof context.parsed === 'object' && context.parsed !== null
2124
2244
  ? (context.parsed as {y: number}).y
2125
2245
  : (context.parsed as number);
2126
2246
  const numericValue = formatNumericValue(value, 1, false, 0);
2127
2247
  const unit = this.unit ? `${this.unit}` : '';
2248
+ let label = context.label ?? '';
2249
+ if (this.isNumericXAxis) {
2250
+ const x =
2251
+ typeof context.parsed === 'object' && context.parsed !== null
2252
+ ? (context.parsed as {x: number}).x
2253
+ : NaN;
2254
+ const relativeTo =
2255
+ this.timeDisplay === TimeDisplay.minutes ? refTs : undefined;
2256
+ label = formatXValue(x, this.xValueMode, relativeTo);
2257
+ }
2128
2258
  return `${label} ${numericValue}${unit}`;
2129
2259
  },
2130
2260
  },
@@ -2140,23 +2270,27 @@ export class ObcChartLineBase extends LitElement {
2140
2270
  * Returns earliest timestamp for 'date' mode, latest for 'minutes' mode.
2141
2271
  */
2142
2272
  private computeTimeReference(): number | undefined {
2273
+ if (this.xAxisType !== XAxisType.time) return undefined;
2274
+
2143
2275
  const timestamps: number[] = [];
2144
2276
 
2145
2277
  // Collect timestamps from datasets
2146
2278
  if (this.datasets?.length) {
2147
2279
  this.datasets.forEach((ds) => {
2148
2280
  if (!ds.data) return;
2149
- (ds.data as (number | {x: unknown; y: number})[]).forEach((pt) => {
2281
+ ds.data.forEach((pt) => {
2150
2282
  if (pt && typeof pt === 'object' && 'x' in pt) {
2151
- const xVal = (pt as {x: unknown}).x;
2152
- const ts =
2153
- typeof xVal === 'string'
2154
- ? new Date(String(xVal)).getTime()
2155
- : Number(xVal);
2283
+ const ts = normalizeXValue(pt.x, XValueMode.time);
2156
2284
  if (Number.isFinite(ts)) timestamps.push(ts);
2157
2285
  }
2158
2286
  });
2159
2287
  });
2288
+ } else if (this.data?.length) {
2289
+ // Collect timestamps from single-series data items
2290
+ this.data.forEach((d) => {
2291
+ const ts = normalizeXValue(d.x ?? d.label ?? NaN, XValueMode.time);
2292
+ if (Number.isFinite(ts)) timestamps.push(ts);
2293
+ });
2160
2294
  }
2161
2295
 
2162
2296
  // Collect timestamps from labels if no dataset timestamps found
@@ -2228,7 +2362,7 @@ export class ObcChartLineBase extends LitElement {
2228
2362
  const fontConfig = {family: fontFamily, size: fontSize, weight: fontWeight};
2229
2363
 
2230
2364
  const x = {
2231
- type: this.xAxisType === 'time' ? 'linear' : 'category',
2365
+ type: this.xAxisType === XAxisType.category ? 'category' : 'linear',
2232
2366
  offset: false, // Always edge-to-edge (no padding on x-axis)
2233
2367
  grace: 0, // No extra margin
2234
2368
  bounds: 'data', // Use data bounds for edge-to-edge rendering
@@ -2247,18 +2381,12 @@ export class ObcChartLineBase extends LitElement {
2247
2381
  maxTicksLimit: this.xTicksLimit,
2248
2382
  stepSize: this.xStepSize,
2249
2383
  callback: (value: unknown) => {
2250
- if (this.xAxisType !== 'time') return String(value);
2384
+ if (!this.isNumericXAxis) return String(value);
2251
2385
  const n = Number(value);
2252
2386
  if (!Number.isFinite(n)) return String(value);
2253
- if (
2254
- this.timeDisplay === 'minutes' &&
2255
- minX !== undefined &&
2256
- Number.isFinite(minX)
2257
- ) {
2258
- const minutes = Math.round((n - minX) / 60000);
2259
- return `${minutes}min`;
2260
- }
2261
- return new Date(n).toLocaleDateString();
2387
+ const relativeTo =
2388
+ this.timeDisplay === TimeDisplay.minutes ? minX : undefined;
2389
+ return formatXValue(n, this.xValueMode, relativeTo);
2262
2390
  },
2263
2391
  },
2264
2392
  border: {
@@ -6,3 +6,4 @@ export * from './canvas-layout.js';
6
6
  export * from './rectangular-chart-layout.js';
7
7
  export * from './tooltip.js';
8
8
  export * from './legend.js';
9
+ export * from './x-value.js';