@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.
- package/bundle/openbridge-webcomponents.bundle.js +153 -23
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +66 -49
- package/dist/building-blocks/chart-line/chart-line-base.d.ts +95 -33
- package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.js +88 -23
- package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
- package/dist/charthelpers/index.d.ts +1 -0
- package/dist/charthelpers/index.d.ts.map +1 -1
- package/dist/charthelpers/index.js +4 -0
- package/dist/charthelpers/index.js.map +1 -1
- package/dist/charthelpers/x-value.d.ts +72 -0
- package/dist/charthelpers/x-value.d.ts.map +1 -0
- package/dist/charthelpers/x-value.js +60 -0
- package/dist/charthelpers/x-value.js.map +1 -0
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts +16 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.js +12 -0
- package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
- package/package.json +1 -1
- package/src/bars-graphs/area-graph/area-graph.stories.ts +83 -1
- package/src/bars-graphs/line-graph/line-graph.stories.ts +148 -1
- package/src/building-blocks/chart-line/chart-line-base.stories.ts +1 -1
- package/src/building-blocks/chart-line/chart-line-base.ts +198 -70
- package/src/charthelpers/index.ts +1 -0
- package/src/charthelpers/x-value.spec.ts +102 -0
- package/src/charthelpers/x-value.ts +134 -0
- package/src/navigation-instruments/gauge-trend/gauge-trend.stories.ts +42 -0
- package/src/navigation-instruments/gauge-trend/gauge-trend.ts +38 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest';
|
|
2
|
+
import {formatXValue, normalizeXValue, XValueMode} from './x-value.js';
|
|
3
|
+
|
|
4
|
+
describe('normalizeXValue', () => {
|
|
5
|
+
it('passes finite numbers through in both modes', () => {
|
|
6
|
+
expect(normalizeXValue(1751790000000, XValueMode.time)).toBe(1751790000000);
|
|
7
|
+
expect(normalizeXValue(42.5, XValueMode.number)).toBe(42.5);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns NaN for non-finite numbers', () => {
|
|
11
|
+
expect(normalizeXValue(Infinity, XValueMode.time)).toBeNaN();
|
|
12
|
+
expect(normalizeXValue(NaN, XValueMode.number)).toBeNaN();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('converts Date via getTime()', () => {
|
|
16
|
+
const d = new Date('2026-07-06T10:00:00Z');
|
|
17
|
+
expect(normalizeXValue(d, XValueMode.time)).toBe(d.getTime());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('uses epochMilliseconds for Temporal Instant/ZonedDateTime shapes', () => {
|
|
21
|
+
expect(
|
|
22
|
+
normalizeXValue({epochMilliseconds: 1751790000000}, XValueMode.time)
|
|
23
|
+
).toBe(1751790000000);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('prefers epochMilliseconds when a ZonedDateTime-like also has year/month/day', () => {
|
|
27
|
+
expect(
|
|
28
|
+
normalizeXValue(
|
|
29
|
+
{epochMilliseconds: 123, year: 2026, month: 7, day: 6},
|
|
30
|
+
XValueMode.time
|
|
31
|
+
)
|
|
32
|
+
).toBe(123);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('interprets PlainDateTime-like shapes in the system time zone', () => {
|
|
36
|
+
const v = {year: 2026, month: 7, day: 6, hour: 10, minute: 30};
|
|
37
|
+
expect(normalizeXValue(v, XValueMode.time)).toBe(
|
|
38
|
+
new Date(2026, 6, 6, 10, 30, 0, 0).getTime()
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('interprets PlainDate-like shapes as local midnight', () => {
|
|
43
|
+
expect(
|
|
44
|
+
normalizeXValue({year: 2026, month: 7, day: 6}, XValueMode.time)
|
|
45
|
+
).toBe(new Date(2026, 6, 6).getTime());
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('parses ISO strings in time mode', () => {
|
|
49
|
+
expect(normalizeXValue('2026-07-06T10:00:00Z', XValueMode.time)).toBe(
|
|
50
|
+
Date.parse('2026-07-06T10:00:00Z')
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('falls back to Number() for numeric strings in time mode', () => {
|
|
55
|
+
expect(normalizeXValue('1751790000000', XValueMode.time)).toBe(
|
|
56
|
+
1751790000000
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('only accepts numeric strings in number mode', () => {
|
|
61
|
+
expect(normalizeXValue('42.5', XValueMode.number)).toBe(42.5);
|
|
62
|
+
expect(normalizeXValue('2026-07-06', XValueMode.number)).toBeNaN();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns NaN for garbage input', () => {
|
|
66
|
+
expect(normalizeXValue('not a date', XValueMode.time)).toBeNaN();
|
|
67
|
+
expect(normalizeXValue('', XValueMode.time)).toBeNaN();
|
|
68
|
+
expect(normalizeXValue({} as never, XValueMode.time)).toBeNaN();
|
|
69
|
+
expect(normalizeXValue(null as never, XValueMode.time)).toBeNaN();
|
|
70
|
+
expect(normalizeXValue(undefined as never, XValueMode.time)).toBeNaN();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('formatXValue', () => {
|
|
75
|
+
it('formats relative minutes when a reference is given', () => {
|
|
76
|
+
const ref = 1751790000000;
|
|
77
|
+
expect(formatXValue(ref + 5 * 60000, XValueMode.time, ref)).toBe('5min');
|
|
78
|
+
expect(formatXValue(ref - 3 * 60000, XValueMode.time, ref)).toBe('-3min');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('renders a locale date when no reference is given', () => {
|
|
82
|
+
const ts = Date.parse('2026-07-06T10:00:00Z');
|
|
83
|
+
expect(formatXValue(ts, XValueMode.time)).toBe(
|
|
84
|
+
new Date(ts).toLocaleDateString()
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('ignores a non-finite reference', () => {
|
|
89
|
+
const ts = Date.parse('2026-07-06T10:00:00Z');
|
|
90
|
+
expect(formatXValue(ts, XValueMode.time, NaN)).toBe(
|
|
91
|
+
new Date(ts).toLocaleDateString()
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('formats number mode as plain string, even with a reference', () => {
|
|
96
|
+
expect(formatXValue(42.5, XValueMode.number, 1000)).toBe('42.5');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns empty string for non-finite values', () => {
|
|
100
|
+
expect(formatXValue(NaN, XValueMode.time)).toBe('');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X-value normalization and formatting for line/area charts.
|
|
3
|
+
*
|
|
4
|
+
* Charts accept x-values as epoch milliseconds, ISO-8601 strings, `Date`
|
|
5
|
+
* instances, or Temporal objects (`Instant`, `ZonedDateTime`,
|
|
6
|
+
* `PlainDateTime`, `PlainDate`). All are normalized to a single number
|
|
7
|
+
* (epoch milliseconds in `'time'` mode, the plain value in `'number'` mode)
|
|
8
|
+
* so the Chart.js linear scale positions points proportionally.
|
|
9
|
+
*
|
|
10
|
+
* Temporal support is structural (duck-typed): there is no dependency on the
|
|
11
|
+
* Temporal API or a polyfill, and cross-realm/polyfilled objects work.
|
|
12
|
+
* Plain* types (which carry no time zone) are interpreted in the system
|
|
13
|
+
* time zone.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* normalizeXValue('2026-07-06T10:00:00Z', XValueMode.time); // epoch ms
|
|
18
|
+
* normalizeXValue(new Date(0), XValueMode.time); // 0
|
|
19
|
+
* normalizeXValue({epochMilliseconds: 42}, XValueMode.time); // 42 (Temporal)
|
|
20
|
+
* normalizeXValue('2.5', XValueMode.number); // 2.5
|
|
21
|
+
*
|
|
22
|
+
* formatXValue(ms, XValueMode.time, referenceMs); // '5min' (relative)
|
|
23
|
+
* formatXValue(ms, XValueMode.time); // locale date string
|
|
24
|
+
* formatXValue(2.5, XValueMode.number); // '2.5'
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type TemporalEpochLike = {
|
|
29
|
+
epochMilliseconds: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type TemporalPlainLike = {
|
|
33
|
+
year: number;
|
|
34
|
+
month: number;
|
|
35
|
+
day: number;
|
|
36
|
+
hour?: number;
|
|
37
|
+
minute?: number;
|
|
38
|
+
second?: number;
|
|
39
|
+
millisecond?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Structural type covering the supported Temporal object shapes. */
|
|
43
|
+
export type TemporalLike = TemporalEpochLike | TemporalPlainLike;
|
|
44
|
+
|
|
45
|
+
/** Every value accepted as a chart x-coordinate. */
|
|
46
|
+
export type ChartXValue = number | string | Date | TemporalLike;
|
|
47
|
+
|
|
48
|
+
/** X-axis interpretation used for normalization and formatting. */
|
|
49
|
+
export enum XValueMode {
|
|
50
|
+
time = 'time',
|
|
51
|
+
number = 'number',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isTemporalEpochLike(v: object): v is TemporalEpochLike {
|
|
55
|
+
return typeof (v as TemporalEpochLike).epochMilliseconds === 'number';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTemporalPlainLike(v: object): v is TemporalPlainLike {
|
|
59
|
+
const t = v as TemporalPlainLike;
|
|
60
|
+
return (
|
|
61
|
+
typeof t.year === 'number' &&
|
|
62
|
+
typeof t.month === 'number' &&
|
|
63
|
+
typeof t.day === 'number'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Normalize an x-value to a finite number, or NaN when unusable.
|
|
69
|
+
*
|
|
70
|
+
* Rules, in order:
|
|
71
|
+
* 1. `number` → as-is (epoch ms in time mode, plain value in number mode)
|
|
72
|
+
* 2. `Date` → `getTime()`
|
|
73
|
+
* 3. `{epochMilliseconds}` (Temporal Instant / ZonedDateTime) → epoch ms
|
|
74
|
+
* 4. `{year, month, day, …}` (Temporal PlainDateTime / PlainDate) →
|
|
75
|
+
* epoch ms in the system time zone
|
|
76
|
+
* 5. `string` → time mode: `Date.parse()` first, `Number()` fallback;
|
|
77
|
+
* number mode: `Number()` only
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeXValue(v: ChartXValue, mode: XValueMode): number {
|
|
80
|
+
if (typeof v === 'number') {
|
|
81
|
+
return Number.isFinite(v) ? v : NaN;
|
|
82
|
+
}
|
|
83
|
+
if (v instanceof Date) {
|
|
84
|
+
return v.getTime();
|
|
85
|
+
}
|
|
86
|
+
if (typeof v === 'object' && v !== null) {
|
|
87
|
+
if (isTemporalEpochLike(v)) return v.epochMilliseconds;
|
|
88
|
+
if (isTemporalPlainLike(v)) {
|
|
89
|
+
return new Date(
|
|
90
|
+
v.year,
|
|
91
|
+
v.month - 1,
|
|
92
|
+
v.day,
|
|
93
|
+
v.hour ?? 0,
|
|
94
|
+
v.minute ?? 0,
|
|
95
|
+
v.second ?? 0,
|
|
96
|
+
v.millisecond ?? 0
|
|
97
|
+
).getTime();
|
|
98
|
+
}
|
|
99
|
+
return NaN;
|
|
100
|
+
}
|
|
101
|
+
if (typeof v === 'string') {
|
|
102
|
+
if (v.trim() === '') return NaN;
|
|
103
|
+
if (mode === XValueMode.time) {
|
|
104
|
+
const parsed = Date.parse(v);
|
|
105
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
106
|
+
}
|
|
107
|
+
const numeric = Number(v);
|
|
108
|
+
return Number.isFinite(numeric) ? numeric : NaN;
|
|
109
|
+
}
|
|
110
|
+
return NaN;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Format a normalized x-value for axis tick labels and tooltips.
|
|
115
|
+
*
|
|
116
|
+
* Mirrors the historical chart-line tick formatting exactly: with a finite
|
|
117
|
+
* `relativeToMs` (time mode) renders `<n>min` relative to it; without one
|
|
118
|
+
* renders `toLocaleDateString()`. Number mode renders the plain value.
|
|
119
|
+
* Callers express the `TimeDisplay` choice by passing or omitting
|
|
120
|
+
* `relativeToMs`.
|
|
121
|
+
*/
|
|
122
|
+
export function formatXValue(
|
|
123
|
+
value: number,
|
|
124
|
+
mode: XValueMode,
|
|
125
|
+
relativeToMs?: number
|
|
126
|
+
): string {
|
|
127
|
+
if (!Number.isFinite(value)) return '';
|
|
128
|
+
if (mode === XValueMode.number) return String(value);
|
|
129
|
+
if (relativeToMs !== undefined && Number.isFinite(relativeToMs)) {
|
|
130
|
+
const minutes = Math.round((value - relativeToMs) / 60000);
|
|
131
|
+
return `${minutes}min`;
|
|
132
|
+
}
|
|
133
|
+
return new Date(value).toLocaleDateString();
|
|
134
|
+
}
|
|
@@ -43,6 +43,19 @@ const SAMPLE_DATA = [
|
|
|
43
43
|
{label: '30', value: 50},
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
+
const UNEVEN_TIME_DATA = [
|
|
47
|
+
{x: '2026-07-06T10:00:00Z', value: 45},
|
|
48
|
+
{x: '2026-07-06T10:02:00Z', value: 52},
|
|
49
|
+
{x: '2026-07-06T10:03:00Z', value: 48},
|
|
50
|
+
{x: '2026-07-06T10:10:00Z', value: 55},
|
|
51
|
+
{x: '2026-07-06T10:11:00Z', value: 62},
|
|
52
|
+
{x: '2026-07-06T10:25:00Z', value: 58},
|
|
53
|
+
{x: '2026-07-06T10:26:00Z', value: 52},
|
|
54
|
+
{x: '2026-07-06T10:40:00Z', value: 40},
|
|
55
|
+
{x: '2026-07-06T11:20:00Z', value: 35},
|
|
56
|
+
{x: '2026-07-06T11:21:00Z', value: 50},
|
|
57
|
+
];
|
|
58
|
+
|
|
46
59
|
const meta: Meta = {
|
|
47
60
|
title: 'Instruments/Gauge Trend',
|
|
48
61
|
tags: ['autodocs', '6.0'],
|
|
@@ -253,6 +266,35 @@ export const GaugeTrend: Story = {
|
|
|
253
266
|
`,
|
|
254
267
|
};
|
|
255
268
|
|
|
269
|
+
export const UnevenTimeIntervals: Story = {
|
|
270
|
+
name: 'Uneven Time Intervals (Auto Time Axis)',
|
|
271
|
+
play: async () => {
|
|
272
|
+
// Wait for rendering to complete before snapshot
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
274
|
+
},
|
|
275
|
+
render: (_args) => html`
|
|
276
|
+
<obc-gauge-trend
|
|
277
|
+
.data=${UNEVEN_TIME_DATA}
|
|
278
|
+
.width=${_args.width}
|
|
279
|
+
.height=${_args.height}
|
|
280
|
+
.priority=${_args.priority}
|
|
281
|
+
.chartFill=${_args.chartFill}
|
|
282
|
+
.minValue=${_args.minValue ?? 0}
|
|
283
|
+
.maxValue=${_args.maxValue ?? 100}
|
|
284
|
+
.value=${_args.value}
|
|
285
|
+
.setpoint=${_args.setpoint}
|
|
286
|
+
.touching=${_args.touching}
|
|
287
|
+
.hasBar=${_args.hasBar}
|
|
288
|
+
.hasScale=${_args.hasScale}
|
|
289
|
+
.fillMode=${_args.fillMode}
|
|
290
|
+
.fillMin=${_args.fillMin}
|
|
291
|
+
.primaryTickmarkInterval=${10}
|
|
292
|
+
.secondaryTickmarkInterval=${5}
|
|
293
|
+
>
|
|
294
|
+
</obc-gauge-trend>
|
|
295
|
+
`,
|
|
296
|
+
};
|
|
297
|
+
|
|
256
298
|
export const GaugeTrendScaleReferenceSize: Story = {
|
|
257
299
|
name: 'Using ScaleReferenceSize=240',
|
|
258
300
|
play: async () => {
|
|
@@ -40,7 +40,9 @@ export {FillMode, ScaleType};
|
|
|
40
40
|
* ## Locked Configuration (not user-configurable)
|
|
41
41
|
* - Fixed aspect ratio scaling: always enabled
|
|
42
42
|
* - Instrument mode: always enabled (8px border radius)
|
|
43
|
-
* - X-axis type:
|
|
43
|
+
* - X-axis type: auto-detected — 'category' for `{label, value}` data,
|
|
44
|
+
* 'time' for `{x, value}` data (uneven intervals position proportionally).
|
|
45
|
+
* Assigning `xAxisType` explicitly disables auto-detection.
|
|
44
46
|
* - Line mode: always 'smooth'
|
|
45
47
|
* - Grid, tick marks, points, legend: always hidden
|
|
46
48
|
* - Scale advice position: always 'inner'
|
|
@@ -102,6 +104,17 @@ export {FillMode, ScaleType};
|
|
|
102
104
|
* ></obc-gauge-trend>
|
|
103
105
|
* ```
|
|
104
106
|
*
|
|
107
|
+
* ### Time-based data with uneven intervals
|
|
108
|
+
* ```html
|
|
109
|
+
* <obc-gauge-trend
|
|
110
|
+
* .data=${[
|
|
111
|
+
* {x: '2026-07-06T10:00:00Z', value: 3.5},
|
|
112
|
+
* {x: '2026-07-06T10:03:00Z', value: 4.2},
|
|
113
|
+
* {x: '2026-07-06T10:15:00Z', value: 5.0}
|
|
114
|
+
* ]}
|
|
115
|
+
* ></obc-gauge-trend>
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
105
118
|
* @property {number} width - Chart width in pixels (defines aspect ratio)
|
|
106
119
|
* @property {number} height - Chart height in pixels (defines aspect ratio)
|
|
107
120
|
* @property {boolean} enhanced - Use enhanced color palette for chart and scales
|
|
@@ -119,6 +132,8 @@ export {FillMode, ScaleType};
|
|
|
119
132
|
export class ObcGaugeTrend extends SetpointMixin(ObcChartLineBase) {
|
|
120
133
|
private _barVerticalElement?: HTMLElement;
|
|
121
134
|
private _isFirstUpdate = false;
|
|
135
|
+
private _explicitXAxisType = false;
|
|
136
|
+
private _autoAppliedXAxisType?: XAxisType;
|
|
122
137
|
|
|
123
138
|
constructor() {
|
|
124
139
|
super();
|
|
@@ -456,6 +471,28 @@ export class ObcGaugeTrend extends SetpointMixin(ObcChartLineBase) {
|
|
|
456
471
|
}
|
|
457
472
|
|
|
458
473
|
override willUpdate(changed: Map<PropertyKey, unknown>) {
|
|
474
|
+
// Auto axis detection: {x, value} data switches to time spacing, {label,
|
|
475
|
+
// value} stays category. An explicit xAxisType assignment (anything we
|
|
476
|
+
// did not auto-apply) permanently disables auto-detection. The
|
|
477
|
+
// constructor's 'category' default lands in the first changed-map but is
|
|
478
|
+
// not treated as explicit (hasUpdated is false and it equals the default).
|
|
479
|
+
if (
|
|
480
|
+
changed.has('xAxisType') &&
|
|
481
|
+
this.xAxisType !== this._autoAppliedXAxisType &&
|
|
482
|
+
(this.hasUpdated || this.xAxisType !== XAxisType.category)
|
|
483
|
+
) {
|
|
484
|
+
this._explicitXAxisType = true;
|
|
485
|
+
}
|
|
486
|
+
if (!this._explicitXAxisType && changed.has('data')) {
|
|
487
|
+
const allHaveX =
|
|
488
|
+
(this.data?.length ?? 0) > 0 && this.data.every((d) => d.x != null);
|
|
489
|
+
const target = allHaveX ? XAxisType.time : XAxisType.category;
|
|
490
|
+
if (this.xAxisType !== target) {
|
|
491
|
+
this._autoAppliedXAxisType = target;
|
|
492
|
+
this.xAxisType = target;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
459
496
|
super.willUpdate(changed);
|
|
460
497
|
|
|
461
498
|
// Update y-axis range when chart or scale min/max changes
|