@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.151 → 2.0.0-next.152
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/.storybook/vitest.setup.ts +12 -1
- package/bundle/openbridge-webcomponents.bundle.js +989 -319
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +1514 -77
- package/dist/alert-severity.d.ts +12 -9
- package/dist/alert-severity.d.ts.map +1 -1
- package/dist/alert-severity.js +30 -38
- package/dist/alert-severity.js.map +1 -1
- package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts +6 -0
- package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts.map +1 -1
- package/dist/building-blocks/bar-horizontal/bar-horizontal.js +6 -1
- package/dist/building-blocks/bar-horizontal/bar-horizontal.js.map +1 -1
- package/dist/building-blocks/bar-vertical/bar-vertical.d.ts +6 -0
- package/dist/building-blocks/bar-vertical/bar-vertical.d.ts.map +1 -1
- package/dist/building-blocks/bar-vertical/bar-vertical.js +6 -1
- package/dist/building-blocks/bar-vertical/bar-vertical.js.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.d.ts +129 -0
- package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.js +464 -79
- package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
- package/dist/building-blocks/external-scale/external-scale.d.ts +11 -0
- package/dist/building-blocks/external-scale/external-scale.d.ts.map +1 -1
- package/dist/building-blocks/external-scale/external-scale.js +45 -12
- package/dist/building-blocks/external-scale/external-scale.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 +7 -1
- package/dist/charthelpers/index.js.map +1 -1
- package/dist/charthelpers/range-labels.d.ts +55 -0
- package/dist/charthelpers/range-labels.d.ts.map +1 -0
- package/dist/charthelpers/range-labels.js +36 -0
- package/dist/charthelpers/range-labels.js.map +1 -0
- package/dist/components/alert-button/alert-button.css.js +13 -21
- package/dist/components/alert-button/alert-button.css.js.map +1 -1
- package/dist/components/alert-button/alert-button.d.ts +9 -4
- package/dist/components/alert-button/alert-button.d.ts.map +1 -1
- package/dist/components/alert-button/alert-button.js +24 -15
- package/dist/components/alert-button/alert-button.js.map +1 -1
- package/dist/components/alert-frame/alert-frame.css.js +109 -33
- package/dist/components/alert-frame/alert-frame.css.js.map +1 -1
- package/dist/components/alert-frame/alert-frame.d.ts +34 -7
- package/dist/components/alert-frame/alert-frame.d.ts.map +1 -1
- package/dist/components/alert-frame/alert-frame.js +134 -30
- package/dist/components/alert-frame/alert-frame.js.map +1 -1
- package/dist/components/alert-icon/alert-icon.d.ts +10 -7
- package/dist/components/alert-icon/alert-icon.d.ts.map +1 -1
- package/dist/components/alert-icon/alert-icon.js +37 -60
- package/dist/components/alert-icon/alert-icon.js.map +1 -1
- package/dist/components/alert-menu-item/alert-menu-item.css.js +1 -4
- package/dist/components/alert-menu-item/alert-menu-item.css.js.map +1 -1
- package/dist/components/date-item/date-item.css.js +3 -3
- package/dist/components/keyboard-full/keyboard-full.css.js +4 -4
- package/dist/components/message-menu-item/message-menu-item.css.js +2 -6
- package/dist/components/message-menu-item/message-menu-item.css.js.map +1 -1
- package/dist/components/sequence-toolbar/sequence-toolbar.css.js +1 -2
- package/dist/components/sequence-toolbar/sequence-toolbar.css.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts +6 -0
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.js +8 -0
- package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
- package/dist/navigation-instruments/instrument-field/instrument-field.css.js +2 -2
- package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
- package/dist/navigation-instruments/readout/readout.js +2 -1
- package/dist/navigation-instruments/readout/readout.js.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js +2 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
- package/dist/openbridge.css +19 -4
- package/dist/palettes/blinking.d.ts +14 -4
- package/dist/palettes/blinking.d.ts.map +1 -1
- package/dist/palettes/blinking.js +41 -35
- package/dist/palettes/blinking.js.map +1 -1
- package/dist/palettes/flashing-controller.d.ts +21 -0
- package/dist/palettes/flashing-controller.d.ts.map +1 -0
- package/dist/palettes/flashing-controller.js +45 -0
- package/dist/palettes/flashing-controller.js.map +1 -0
- package/dist/svghelpers/index.d.ts +1 -0
- package/dist/svghelpers/index.d.ts.map +1 -1
- package/dist/svghelpers/index.js +2 -0
- package/dist/svghelpers/index.js.map +1 -1
- package/dist/svghelpers/rounded-rect.d.ts +15 -0
- package/dist/svghelpers/rounded-rect.d.ts.map +1 -0
- package/dist/svghelpers/rounded-rect.js +29 -0
- package/dist/svghelpers/rounded-rect.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/script/check-css-variables.ts +2 -15
- package/src/alert-severity.spec.ts +56 -0
- package/src/alert-severity.ts +45 -43
- package/src/bars-graphs/area-graph/area-graph.stories.ts +235 -1
- package/src/bars-graphs/line-graph/line-graph.stories.ts +24 -0
- package/src/building-blocks/bar-horizontal/bar-horizontal.ts +7 -0
- package/src/building-blocks/bar-vertical/bar-vertical.ts +7 -0
- package/src/building-blocks/chart-line/chart-line-base.spec.ts +767 -0
- package/src/building-blocks/chart-line/chart-line-base.stories.ts +124 -0
- package/src/building-blocks/chart-line/chart-line-base.ts +671 -97
- package/src/building-blocks/chart-line/chart-sizing-battleground.stories.ts +17 -0
- package/src/building-blocks/external-scale/external-scale.spec.ts +154 -0
- package/src/building-blocks/external-scale/external-scale.ts +78 -20
- package/src/charthelpers/index.ts +1 -0
- package/src/charthelpers/range-labels.spec.ts +46 -0
- package/src/charthelpers/range-labels.ts +93 -0
- package/src/components/alert-button/alert-button.css +10 -23
- package/src/components/alert-button/alert-button.spec.ts +56 -15
- package/src/components/alert-button/alert-button.stories.ts +5 -1
- package/src/components/alert-button/alert-button.ts +33 -24
- package/src/components/alert-frame/alert-flash-scenario.stories.ts +669 -0
- package/src/components/alert-frame/alert-frame.css +106 -33
- package/src/components/alert-frame/alert-frame.spec.ts +176 -17
- package/src/components/alert-frame/alert-frame.stories.ts +61 -0
- package/src/components/alert-frame/alert-frame.ts +179 -41
- package/src/components/alert-icon/alert-icon.spec.ts +82 -27
- package/src/components/alert-icon/alert-icon.stories.ts +7 -1
- package/src/components/alert-icon/alert-icon.ts +48 -74
- package/src/components/alert-menu-item/alert-menu-item.css +1 -4
- package/src/components/date-item/date-item.css +3 -3
- package/src/components/keyboard-full/keyboard-full.css +4 -4
- package/src/components/message-menu-item/message-menu-item.css +2 -6
- package/src/components/sequence-toolbar/sequence-toolbar.css +1 -2
- package/src/navigation-instruments/gauge-trend/gauge-trend.stories.ts +29 -0
- package/src/navigation-instruments/gauge-trend/gauge-trend.ts +11 -0
- package/src/navigation-instruments/instrument-field/instrument-field.css +2 -2
- package/src/navigation-instruments/readout/readout.ts +2 -1
- package/src/navigation-instruments/readout-list-item/readout-list-item.ts +2 -1
- package/src/palettes/blinking.spec.ts +83 -0
- package/src/palettes/blinking.ts +47 -35
- package/src/palettes/flashing-controller.ts +62 -0
- package/src/palettes/manual.css +18 -4
- package/src/svghelpers/index.ts +1 -0
- package/src/svghelpers/rounded-rect.spec.ts +47 -0
- package/src/svghelpers/rounded-rect.ts +42 -0
- package/src/types.ts +20 -0
|
@@ -42,7 +42,13 @@ import {
|
|
|
42
42
|
formatXValue,
|
|
43
43
|
XValueMode,
|
|
44
44
|
observeLabelThreshold,
|
|
45
|
+
measureRangeLabelGutters,
|
|
46
|
+
rangeLabelFontString,
|
|
47
|
+
rangeLabelLineHeight,
|
|
48
|
+
formatRangeValue,
|
|
49
|
+
yRangeLabelValues,
|
|
45
50
|
} from '../../charthelpers/index.js';
|
|
51
|
+
import type {RangeLabelFont} from '../../charthelpers/index.js';
|
|
46
52
|
import type {ChartXValue} from '../../charthelpers/x-value.js';
|
|
47
53
|
import {
|
|
48
54
|
EXTERNAL_SCALE_BORDER_RADIUS_CSS_VAR,
|
|
@@ -91,6 +97,8 @@ interface ExternalScaleElement extends HTMLElement {
|
|
|
91
97
|
paddingEnd?: number;
|
|
92
98
|
primaryTickmarkInterval?: number;
|
|
93
99
|
showLabels?: boolean;
|
|
100
|
+
showMainTickmarkLabels?: boolean;
|
|
101
|
+
labelThickness?: number;
|
|
94
102
|
fixedAspectRatio?: boolean;
|
|
95
103
|
scaleReferenceSize?: number;
|
|
96
104
|
state?: InstrumentState;
|
|
@@ -123,6 +131,14 @@ export enum TimeDisplay {
|
|
|
123
131
|
date = 'date',
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
/** Which axes keep min / max labels below the label threshold. */
|
|
135
|
+
export enum RangeLabels {
|
|
136
|
+
none = 'none',
|
|
137
|
+
y = 'y',
|
|
138
|
+
x = 'x',
|
|
139
|
+
xy = 'xy',
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
export type ChartLinePoint = number | {x: ChartXValue; y: number};
|
|
127
143
|
|
|
128
144
|
export type ChartLineDataItem = {
|
|
@@ -145,6 +161,11 @@ export type ChartLineYAxisConfig = {
|
|
|
145
161
|
grid?: boolean;
|
|
146
162
|
};
|
|
147
163
|
|
|
164
|
+
export type ChartLineXAxisConfig = {
|
|
165
|
+
min?: number;
|
|
166
|
+
max?: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
148
169
|
const LINE_GRAPH_WATCHED_PROP_NAMES = [
|
|
149
170
|
'data',
|
|
150
171
|
'datasets',
|
|
@@ -153,6 +174,7 @@ const LINE_GRAPH_WATCHED_PROP_NAMES = [
|
|
|
153
174
|
'xAxisType',
|
|
154
175
|
'yAxisPosition',
|
|
155
176
|
'yAxes',
|
|
177
|
+
'xAxis',
|
|
156
178
|
'showGrid',
|
|
157
179
|
'showGridX',
|
|
158
180
|
'showGridY',
|
|
@@ -173,6 +195,7 @@ const LINE_GRAPH_WATCHED_PROP_NAMES = [
|
|
|
173
195
|
'height',
|
|
174
196
|
'fixedAspectRatioScaling', // Triggers responsive mode change
|
|
175
197
|
'hasLabelPadding', // Toggles edge-to-edge rendering and label visibility
|
|
198
|
+
'rangeLabels',
|
|
176
199
|
] as const;
|
|
177
200
|
|
|
178
201
|
const LINE_GRAPH_RECREATE_PROP_NAMES = [
|
|
@@ -180,6 +203,7 @@ const LINE_GRAPH_RECREATE_PROP_NAMES = [
|
|
|
180
203
|
'width',
|
|
181
204
|
'height',
|
|
182
205
|
'fixedAspectRatioScaling',
|
|
206
|
+
'rangeLabels', // Adds or removes the range-labels plugin
|
|
183
207
|
] as const;
|
|
184
208
|
|
|
185
209
|
/**
|
|
@@ -347,12 +371,21 @@ const LINE_GRAPH_DIMENSION_PROP_NAMES = [
|
|
|
347
371
|
* numeric x-values.
|
|
348
372
|
* @property yAxisPosition - Single y-axis position ('left' or 'right'). For multiple y-axes, use yAxes instead.
|
|
349
373
|
* @property yAxes - Multiple y-axis definitions for complex multi-axis charts.
|
|
374
|
+
* @property xAxis - Pinned x range (`min`/`max`) for time and number axes. Without it the
|
|
375
|
+
* axis spans exactly the data, so a window that is still filling stretches across the
|
|
376
|
+
* full width. In `minutes` display `max` is the `0min` reference.
|
|
377
|
+
* @availableWhen xAxis xAxisType!=category
|
|
350
378
|
* @property showGrid - Show grid lines.
|
|
351
379
|
* @property showGridX - Show vertical grid lines (x-axis). Default: false.
|
|
352
380
|
* @availableWhen showGridX showGrid==true
|
|
353
381
|
* @property showGridY - Show horizontal grid lines (y-axis). Default: false.
|
|
354
382
|
* @availableWhen showGridY showGrid==true
|
|
355
383
|
* @property showTickMarks - Show axis tick marks and labels.
|
|
384
|
+
* @property rangeLabels - Labels kept below the 192px threshold, where the axis labels are
|
|
385
|
+
* otherwise hidden: 'y' draws min, max and (inside the range) 0 in a gutter on the
|
|
386
|
+
* y-axis side; 'x' draws the first and last x value in a bottom gutter; 'xy' both.
|
|
387
|
+
* Ignored when `hasLabelPadding` is false.
|
|
388
|
+
* @availableWhen rangeLabels hasLabelPadding==true
|
|
356
389
|
* @property showPoints - Show point markers on data points. Default: false.
|
|
357
390
|
* @property lineMode - Line drawing style: 'smooth' (curved), 'straight', or 'stepped'.
|
|
358
391
|
* @property unit - Unit label displayed in tooltips (e.g., 'kW', 'kg', '%').
|
|
@@ -417,6 +450,9 @@ export class ObcChartLineBase extends LitElement {
|
|
|
417
450
|
@property({type: Array, attribute: false})
|
|
418
451
|
yAxes?: ChartLineYAxisConfig[] = undefined;
|
|
419
452
|
|
|
453
|
+
@property({type: Object, attribute: false})
|
|
454
|
+
xAxis?: ChartLineXAxisConfig = undefined;
|
|
455
|
+
|
|
420
456
|
@property({type: Boolean})
|
|
421
457
|
showGrid = false;
|
|
422
458
|
|
|
@@ -449,6 +485,27 @@ export class ObcChartLineBase extends LitElement {
|
|
|
449
485
|
@property({type: Boolean, attribute: false})
|
|
450
486
|
hasLabelPadding = true;
|
|
451
487
|
|
|
488
|
+
@property({type: String})
|
|
489
|
+
rangeLabels: RangeLabels = RangeLabels.none;
|
|
490
|
+
|
|
491
|
+
/** @internal - The y side wants range labels; an edge-to-edge chart never does. */
|
|
492
|
+
protected get rangeLabelsY(): boolean {
|
|
493
|
+
return (
|
|
494
|
+
this.hasLabelPadding &&
|
|
495
|
+
(this.rangeLabels === RangeLabels.y ||
|
|
496
|
+
this.rangeLabels === RangeLabels.xy)
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** @internal - The x side wants range labels; an edge-to-edge chart never does. */
|
|
501
|
+
protected get rangeLabelsX(): boolean {
|
|
502
|
+
return (
|
|
503
|
+
this.hasLabelPadding &&
|
|
504
|
+
(this.rangeLabels === RangeLabels.x ||
|
|
505
|
+
this.rangeLabels === RangeLabels.xy)
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
452
509
|
/** @internal - True when the x-axis positions points by numeric value. */
|
|
453
510
|
protected get isNumericXAxis(): boolean {
|
|
454
511
|
return (
|
|
@@ -456,6 +513,14 @@ export class ObcChartLineBase extends LitElement {
|
|
|
456
513
|
);
|
|
457
514
|
}
|
|
458
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Id of the y scale a dataset lands on when it names none. Chart.js would
|
|
518
|
+
* otherwise add a default `y` scale next to the configured ones and draw it.
|
|
519
|
+
*/
|
|
520
|
+
private get primaryYAxisId(): string {
|
|
521
|
+
return this.yAxes?.length ? (this.yAxes[0].id ?? 'y0') : 'y';
|
|
522
|
+
}
|
|
523
|
+
|
|
459
524
|
/** @internal - Normalization mode for the current x-axis type. */
|
|
460
525
|
protected get xValueMode(): XValueMode {
|
|
461
526
|
return this.xAxisType === XAxisType.number
|
|
@@ -466,6 +531,9 @@ export class ObcChartLineBase extends LitElement {
|
|
|
466
531
|
/** @internal - Last data/datasets reference already warned about. */
|
|
467
532
|
private lastWarnedXSource?: unknown;
|
|
468
533
|
|
|
534
|
+
/** @internal - Whether the `date`-display scale warning has been issued. */
|
|
535
|
+
private warnedDateDisplayScale = false;
|
|
536
|
+
|
|
469
537
|
/** @internal - Warn once per data assignment about unparseable x-values. */
|
|
470
538
|
private warnOnInvalidX(xValues: number[], sourceRef: unknown) {
|
|
471
539
|
const invalid = xValues.filter((x) => !Number.isFinite(x)).length;
|
|
@@ -621,18 +689,303 @@ export class ObcChartLineBase extends LitElement {
|
|
|
621
689
|
*/
|
|
622
690
|
|
|
623
691
|
/**
|
|
624
|
-
*
|
|
625
|
-
*
|
|
692
|
+
* @internal - Label band each slotted scale had before the chart narrowed
|
|
693
|
+
* it for compact labels, so it can be given back on the way out.
|
|
626
694
|
*/
|
|
627
|
-
private
|
|
628
|
-
|
|
695
|
+
private savedLabelThickness = new WeakMap<ExternalScaleElement, number>();
|
|
696
|
+
private savedMainTickmarkLabels = new WeakMap<
|
|
697
|
+
ExternalScaleElement,
|
|
698
|
+
boolean
|
|
699
|
+
>();
|
|
700
|
+
/** Last known threshold side, to notice a crossing in `updated()`. */
|
|
701
|
+
private wasBelowThreshold?: boolean;
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Label band a slotted scale needs for its compact labels, in the scale's
|
|
705
|
+
* own viewBox units. The scale prints the raw values, so those are what is
|
|
706
|
+
* measured; the x band is one text line.
|
|
707
|
+
*/
|
|
708
|
+
private compactLabelThickness(axis: 'x' | 'y'): number | undefined {
|
|
709
|
+
const context = this.canvasEl?.getContext('2d');
|
|
710
|
+
if (!context) return undefined;
|
|
711
|
+
const font = this.rangeLabelFont();
|
|
712
|
+
let visual: number;
|
|
713
|
+
if (axis === 'y') {
|
|
714
|
+
const bounds = this.rangeLabelCache.bounds;
|
|
715
|
+
if (!bounds) return undefined;
|
|
716
|
+
const texts = yRangeLabelValues(bounds.min, bounds.max).map(String);
|
|
717
|
+
visual = measureRangeLabelGutters(context, font, texts, []).side + 4;
|
|
718
|
+
} else {
|
|
719
|
+
visual = measureRangeLabelGutters(context, font, [], ['x']).bottom + 8;
|
|
720
|
+
}
|
|
721
|
+
if (!this.fixedAspectRatioScaling) return Math.ceil(visual);
|
|
722
|
+
const effectiveLength =
|
|
723
|
+
axis === 'y' ? this.getEffectiveHeight() : this.getEffectiveWidth();
|
|
724
|
+
return Math.ceil((visual * this.scaleReferenceSize) / effectiveLength);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** @internal - Labels the range-labels plugin drew last, for tests. */
|
|
728
|
+
lastRangeLabels: {axis: 'x' | 'y'; text: string; x: number; y: number}[] = [];
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Range labels for the current data and size, refreshed with every
|
|
732
|
+
* `getChartOptions()` so the plugin never rebuilds the datasets on a draw.
|
|
733
|
+
*/
|
|
734
|
+
private rangeLabelCache: {
|
|
735
|
+
y: string[];
|
|
736
|
+
x: string[];
|
|
737
|
+
bounds?: {min: number; max: number};
|
|
738
|
+
} = {y: [], x: []};
|
|
739
|
+
|
|
740
|
+
private slotFor(
|
|
741
|
+
side: 'left' | 'right' | 'top' | 'bottom'
|
|
742
|
+
): HTMLSlotElement | undefined {
|
|
743
|
+
return {
|
|
629
744
|
left: this.leftScaleSlot,
|
|
630
745
|
right: this.rightScaleSlot,
|
|
631
746
|
top: this.topScaleSlot,
|
|
632
747
|
bottom: this.bottomScaleSlot,
|
|
748
|
+
}[side];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private hasSlottedScale(side: 'left' | 'right' | 'top' | 'bottom'): boolean {
|
|
752
|
+
return (this.slotFor(side)?.assignedElements().length ?? 0) > 0;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** @internal - Below the label threshold on either dimension. */
|
|
756
|
+
private isBelowThreshold(): boolean {
|
|
757
|
+
return (
|
|
758
|
+
this.getEffectiveWidth() <
|
|
759
|
+
RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS ||
|
|
760
|
+
this.getEffectiveHeight() <
|
|
761
|
+
RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private get ySide(): 'left' | 'right' {
|
|
766
|
+
const first = this.yAxes?.[0]?.position;
|
|
767
|
+
return (first ?? this.yAxisPosition) === 'right' ? 'right' : 'left';
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private rangeLabelFont(): RangeLabelFont {
|
|
771
|
+
const sizePx = Number.parseFloat(
|
|
772
|
+
getCssVariableValue(this, LINE_GRAPH_LABEL_CONFIG.fontSizeVar)
|
|
773
|
+
);
|
|
774
|
+
return {
|
|
775
|
+
family: getCssVariableValue(this, LINE_GRAPH_LABEL_CONFIG.fontFamily),
|
|
776
|
+
sizePx: Number.isFinite(sizePx) ? sizePx : 12,
|
|
777
|
+
weight: getCssVariableValue(this, LINE_GRAPH_LABEL_CONFIG.fontWeightVar),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Extent of the prepared data: one for x, one per y scale id. Each y axis
|
|
783
|
+
* ranges from its own datasets, so a merged extent would label one axis
|
|
784
|
+
* with another's values.
|
|
785
|
+
*/
|
|
786
|
+
private dataExtents(
|
|
787
|
+
prepared: ReturnType<ObcChartLineBase['prepareChartDataAndLabels']>
|
|
788
|
+
): {
|
|
789
|
+
x?: {min: number; max: number};
|
|
790
|
+
y: Map<string, {min: number; max: number}>;
|
|
791
|
+
} {
|
|
792
|
+
const x = {min: Infinity, max: -Infinity};
|
|
793
|
+
const y = new Map<string, {min: number; max: number}>();
|
|
794
|
+
const take = (extent: {min: number; max: number}, v: number) => {
|
|
795
|
+
if (!Number.isFinite(v)) return;
|
|
796
|
+
extent.min = Math.min(extent.min, v);
|
|
797
|
+
extent.max = Math.max(extent.max, v);
|
|
798
|
+
};
|
|
799
|
+
prepared.datasets.forEach((dataset) => {
|
|
800
|
+
const id = dataset.yAxisID ?? this.primaryYAxisId;
|
|
801
|
+
const extent = y.get(id) ?? {min: Infinity, max: -Infinity};
|
|
802
|
+
y.set(id, extent);
|
|
803
|
+
(dataset.data as ChartLinePoint[]).forEach((point) => {
|
|
804
|
+
if (typeof point === 'number') {
|
|
805
|
+
take(extent, point);
|
|
806
|
+
} else if (point) {
|
|
807
|
+
take(x, point.x as number);
|
|
808
|
+
take(extent, point.y);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
y.forEach((extent, id) => {
|
|
813
|
+
if (!(extent.min <= extent.max)) y.delete(id);
|
|
814
|
+
});
|
|
815
|
+
return {x: x.min <= x.max ? x : undefined, y};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Range the y labels describe. Below the threshold the axis runs on data
|
|
820
|
+
* bounds, so its datasets' extent is exact and needs no chart to read from.
|
|
821
|
+
*/
|
|
822
|
+
private yRangeBounds(
|
|
823
|
+
extents: Map<string, {min: number; max: number}>
|
|
824
|
+
): {min: number; max: number} | undefined {
|
|
825
|
+
let axis: ChartLineYAxisConfig | undefined;
|
|
826
|
+
let id = 'y';
|
|
827
|
+
if (this.yAxes?.length) {
|
|
828
|
+
const match = this.yAxes.findIndex(
|
|
829
|
+
(a) => (a.position ?? 'left') === this.ySide
|
|
830
|
+
);
|
|
831
|
+
const index = match === -1 ? 0 : match;
|
|
832
|
+
axis = this.yAxes[index];
|
|
833
|
+
id = axis.id ?? `y${index}`;
|
|
834
|
+
}
|
|
835
|
+
const extent = extents.get(id);
|
|
836
|
+
// Stacking makes the axis span the accumulated series, which a per-dataset
|
|
837
|
+
// extent cannot describe, so the laid-out scale is the only source for it.
|
|
838
|
+
const stackedScale = this.shouldStack()
|
|
839
|
+
? this.chart?.scales[id]
|
|
840
|
+
: undefined;
|
|
841
|
+
const min = axis?.min ?? stackedScale?.min ?? extent?.min;
|
|
842
|
+
const max = axis?.max ?? stackedScale?.max ?? extent?.max;
|
|
843
|
+
return min !== undefined && max !== undefined ? {min, max} : undefined;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** First and last x label, or nothing when there is no span to label. */
|
|
847
|
+
private xEdgeLabelTexts(
|
|
848
|
+
prepared: ReturnType<ObcChartLineBase['prepareChartDataAndLabels']>,
|
|
849
|
+
extent: {min: number; max: number} | undefined
|
|
850
|
+
): string[] {
|
|
851
|
+
if (!this.isNumericXAxis) {
|
|
852
|
+
const labels = prepared.labels.map(String);
|
|
853
|
+
const first = labels[0];
|
|
854
|
+
const last = labels[labels.length - 1];
|
|
855
|
+
return labels.length > 1 && first !== last ? [first, last] : [];
|
|
856
|
+
}
|
|
857
|
+
const min = this.xAxis?.min ?? extent?.min;
|
|
858
|
+
const max = this.xAxis?.max ?? extent?.max;
|
|
859
|
+
if (min === undefined || max === undefined || min === max) return [];
|
|
860
|
+
const reference =
|
|
861
|
+
this.timeDisplay === TimeDisplay.minutes
|
|
862
|
+
? this.computeTimeReference()
|
|
863
|
+
: undefined;
|
|
864
|
+
return [
|
|
865
|
+
formatXValue(min, this.xValueMode, reference),
|
|
866
|
+
formatXValue(max, this.xValueMode, reference),
|
|
867
|
+
];
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Recompute the range labels. A side with a slotted scale is labelled by
|
|
872
|
+
* the scale, so it gets no text here; the y bounds are kept regardless,
|
|
873
|
+
* since the scale's compact band is measured from them.
|
|
874
|
+
*/
|
|
875
|
+
private refreshRangeLabels(): typeof this.rangeLabelCache {
|
|
876
|
+
const cache: typeof this.rangeLabelCache = {y: [], x: []};
|
|
877
|
+
if (this.isBelowThreshold() && (this.rangeLabelsY || this.rangeLabelsX)) {
|
|
878
|
+
// Prepared once: it restyles every dataset and reads CSS variables.
|
|
879
|
+
const prepared = this.prepareChartDataAndLabels();
|
|
880
|
+
const extents = this.dataExtents(prepared);
|
|
881
|
+
if (this.rangeLabelsY) {
|
|
882
|
+
cache.bounds = this.yRangeBounds(extents.y);
|
|
883
|
+
if (
|
|
884
|
+
cache.bounds &&
|
|
885
|
+
!this.hasSlottedScale('left') &&
|
|
886
|
+
!this.hasSlottedScale('right')
|
|
887
|
+
) {
|
|
888
|
+
const {min, max} = cache.bounds;
|
|
889
|
+
cache.y = yRangeLabelValues(min, max).map((v) =>
|
|
890
|
+
formatRangeValue(v, min, max)
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (
|
|
895
|
+
this.rangeLabelsX &&
|
|
896
|
+
!this.hasSlottedScale('top') &&
|
|
897
|
+
!this.hasSlottedScale('bottom')
|
|
898
|
+
) {
|
|
899
|
+
cache.x = this.xEdgeLabelTexts(prepared, extents.x);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
this.rangeLabelCache = cache;
|
|
903
|
+
return cache;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Paints min / 0 / max and first / last once Chart.js has drawn, inside the
|
|
908
|
+
* gutters `getChartOptions()` reserved. The end labels sit flush with the
|
|
909
|
+
* plot edges so no vertical padding is needed for them.
|
|
910
|
+
*/
|
|
911
|
+
private createRangeLabelsPlugin() {
|
|
912
|
+
return {
|
|
913
|
+
id: 'rangeLabels',
|
|
914
|
+
afterDraw: (chart: Chart) => {
|
|
915
|
+
this.lastRangeLabels = [];
|
|
916
|
+
const {y: yTexts, x: xTexts, bounds} = this.rangeLabelCache;
|
|
917
|
+
if (!yTexts.length && !xTexts.length) return;
|
|
918
|
+
const area = chart.chartArea;
|
|
919
|
+
const ctx = chart.ctx;
|
|
920
|
+
const font = this.rangeLabelFont();
|
|
921
|
+
const lineHeight = rangeLabelLineHeight(font);
|
|
922
|
+
ctx.save();
|
|
923
|
+
ctx.font = rangeLabelFontString(font);
|
|
924
|
+
ctx.fillStyle = getCssVariableValue(
|
|
925
|
+
this,
|
|
926
|
+
LINE_GRAPH_LABEL_CONFIG.fontColorVar
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
const scale = chart.scales[this.primaryYAxisId];
|
|
930
|
+
if (yTexts.length && bounds && scale) {
|
|
931
|
+
const onRight = this.ySide === 'right';
|
|
932
|
+
const x = onRight ? area.right + 8 : area.left - 8;
|
|
933
|
+
ctx.textAlign = onRight ? 'left' : 'right';
|
|
934
|
+
const values = yRangeLabelValues(bounds.min, bounds.max);
|
|
935
|
+
// Top-down, so the recorded order reads max, 0, min.
|
|
936
|
+
for (let i = values.length - 1; i >= 0; i--) {
|
|
937
|
+
const isMax = i === values.length - 1;
|
|
938
|
+
const isMin = i === 0;
|
|
939
|
+
const y = isMax
|
|
940
|
+
? area.top
|
|
941
|
+
: isMin
|
|
942
|
+
? area.bottom
|
|
943
|
+
: scale.getPixelForValue(values[i]);
|
|
944
|
+
// A 0 within a line of either end would collide with it.
|
|
945
|
+
if (
|
|
946
|
+
!isMax &&
|
|
947
|
+
!isMin &&
|
|
948
|
+
(y - area.top < lineHeight || area.bottom - y < lineHeight)
|
|
949
|
+
) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
ctx.textBaseline = isMax ? 'top' : isMin ? 'bottom' : 'middle';
|
|
953
|
+
ctx.fillText(yTexts[i], x, y);
|
|
954
|
+
this.lastRangeLabels.push({axis: 'y', text: yTexts[i], x, y});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (xTexts.length === 2) {
|
|
959
|
+
const y = area.bottom + 4;
|
|
960
|
+
ctx.textBaseline = 'top';
|
|
961
|
+
ctx.textAlign = 'left';
|
|
962
|
+
ctx.fillText(xTexts[0], area.left, y);
|
|
963
|
+
this.lastRangeLabels.push({
|
|
964
|
+
axis: 'x',
|
|
965
|
+
text: xTexts[0],
|
|
966
|
+
x: area.left,
|
|
967
|
+
y,
|
|
968
|
+
});
|
|
969
|
+
ctx.textAlign = 'right';
|
|
970
|
+
ctx.fillText(xTexts[1], area.right, y);
|
|
971
|
+
this.lastRangeLabels.push({
|
|
972
|
+
axis: 'x',
|
|
973
|
+
text: xTexts[1],
|
|
974
|
+
x: area.right,
|
|
975
|
+
y,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
ctx.restore();
|
|
979
|
+
},
|
|
633
980
|
};
|
|
981
|
+
}
|
|
634
982
|
|
|
635
|
-
|
|
983
|
+
/**
|
|
984
|
+
* Check if a slotted scale element is actually visible (renders content).
|
|
985
|
+
* For bar-vertical/bar-horizontal elements, checks hasBar and hasScale properties.
|
|
986
|
+
*/
|
|
987
|
+
private hasVisibleScale(side: 'left' | 'right' | 'top' | 'bottom'): boolean {
|
|
988
|
+
const slot = this.slotFor(side);
|
|
636
989
|
const elements = slot?.assignedElements() ?? [];
|
|
637
990
|
|
|
638
991
|
return elements.some((el: Element) => {
|
|
@@ -1231,18 +1584,23 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1231
1584
|
this.isUpdatingScales = true;
|
|
1232
1585
|
|
|
1233
1586
|
try {
|
|
1234
|
-
// Step 1:
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1587
|
+
// Step 1: The padding the chart itself will lay out with, so a slotted
|
|
1588
|
+
// scale's drawing area lines up with the chart area on every side.
|
|
1589
|
+
const padding = this.computeChartPadding();
|
|
1590
|
+
|
|
1591
|
+
// Step 2: The chart area left once the (visual) padding is taken from
|
|
1592
|
+
// the visual size — the reference `width` / `height` would go negative
|
|
1593
|
+
// in a container larger than the reference.
|
|
1594
|
+
const effectiveWidth =
|
|
1595
|
+
this.getEffectiveWidth() - padding.left - padding.right;
|
|
1596
|
+
const effectiveHeight =
|
|
1597
|
+
this.getEffectiveHeight() - padding.top - padding.bottom;
|
|
1240
1598
|
|
|
1241
1599
|
// Guard against invalid dimensions
|
|
1242
1600
|
if (effectiveWidth <= 0 || effectiveHeight <= 0) {
|
|
1243
1601
|
console.warn('[chart-line-base] Invalid effective dimensions', {
|
|
1244
|
-
width: this.
|
|
1245
|
-
height: this.
|
|
1602
|
+
width: this.getEffectiveWidth(),
|
|
1603
|
+
height: this.getEffectiveHeight(),
|
|
1246
1604
|
padding,
|
|
1247
1605
|
effectiveWidth,
|
|
1248
1606
|
effectiveHeight,
|
|
@@ -1289,6 +1647,75 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1289
1647
|
return padding;
|
|
1290
1648
|
}
|
|
1291
1649
|
|
|
1650
|
+
/**
|
|
1651
|
+
* Range a slotted scale on the given side has to cover.
|
|
1652
|
+
*
|
|
1653
|
+
* A range pinned in the axis configuration is used as is: it is the source
|
|
1654
|
+
* of truth, and on the first sync there is no chart to read yet. Otherwise
|
|
1655
|
+
* the live Chart.js scale is read under the id `buildScalesConfig()` gives
|
|
1656
|
+
* it. A side with no axis of its own follows the first configured one.
|
|
1657
|
+
*/
|
|
1658
|
+
private resolveAxisRange(side: 'left' | 'right' | 'x'): {
|
|
1659
|
+
min: number;
|
|
1660
|
+
max: number;
|
|
1661
|
+
} {
|
|
1662
|
+
let id = side === 'x' ? 'x' : 'y';
|
|
1663
|
+
let configured: {min?: number; max?: number} | undefined;
|
|
1664
|
+
|
|
1665
|
+
if (side === 'x') {
|
|
1666
|
+
configured = this.isNumericXAxis ? this.xAxis : undefined;
|
|
1667
|
+
} else if (this.yAxes?.length) {
|
|
1668
|
+
const match = this.yAxes.findIndex(
|
|
1669
|
+
(axis) => (axis.position ?? 'left') === side
|
|
1670
|
+
);
|
|
1671
|
+
const index = match === -1 ? 0 : match;
|
|
1672
|
+
const axis = this.yAxes[index];
|
|
1673
|
+
id = axis.id ?? `y${index}`;
|
|
1674
|
+
configured = axis;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (configured?.min !== undefined && configured.max !== undefined) {
|
|
1678
|
+
return {min: configured.min, max: configured.max};
|
|
1679
|
+
}
|
|
1680
|
+
const scale = this.chart?.scales[id];
|
|
1681
|
+
return {
|
|
1682
|
+
min: scale?.min ?? configured?.min ?? 0,
|
|
1683
|
+
max: scale?.max ?? configured?.max ?? 100,
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Range pushed to the top and bottom scales, or `undefined` to leave them
|
|
1689
|
+
* alone. A scale prints its values verbatim, and on a `time` axis the raw
|
|
1690
|
+
* range is epoch milliseconds. In `minutes` display the range is converted
|
|
1691
|
+
* to the minutes the chart's own labels show; `date` display has no numeric
|
|
1692
|
+
* equivalent, so the scale keeps its own range.
|
|
1693
|
+
*/
|
|
1694
|
+
private resolveSlottedXRange(): {min: number; max: number} | undefined {
|
|
1695
|
+
const range = this.resolveAxisRange('x');
|
|
1696
|
+
if (this.xAxisType !== XAxisType.time) return range;
|
|
1697
|
+
|
|
1698
|
+
if (this.timeDisplay !== TimeDisplay.minutes) {
|
|
1699
|
+
if (!this.warnedDateDisplayScale) {
|
|
1700
|
+
this.warnedDateDisplayScale = true;
|
|
1701
|
+
console.warn(
|
|
1702
|
+
"[obc-chart] a slotted horizontal scale cannot label a time axis in 'date' display and keeps its own range; use timeDisplay='minutes' or a number axis."
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
return undefined;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const pinned =
|
|
1709
|
+
this.xAxis?.min !== undefined && this.xAxis?.max !== undefined;
|
|
1710
|
+
const reference = this.computeTimeReference();
|
|
1711
|
+
if (reference === undefined || (!this.chart && !pinned)) return undefined;
|
|
1712
|
+
const minute = 60_000;
|
|
1713
|
+
return {
|
|
1714
|
+
min: (range.min - reference) / minute,
|
|
1715
|
+
max: (range.max - reference) / minute,
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1292
1719
|
/**
|
|
1293
1720
|
* Update properties on slotted scale elements
|
|
1294
1721
|
*/
|
|
@@ -1298,11 +1725,9 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1298
1725
|
bottom: number;
|
|
1299
1726
|
left: number;
|
|
1300
1727
|
}) {
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1303
|
-
const
|
|
1304
|
-
const xMin = this.chart?.scales['x']?.min ?? 0;
|
|
1305
|
-
const xMax = this.chart?.scales['x']?.max ?? 100;
|
|
1728
|
+
const leftRange = this.resolveAxisRange('left');
|
|
1729
|
+
const rightRange = this.resolveAxisRange('right');
|
|
1730
|
+
const xRange = this.resolveSlottedXRange();
|
|
1306
1731
|
|
|
1307
1732
|
// Use effective dimensions for threshold checks
|
|
1308
1733
|
const effectiveWidth = this.getEffectiveWidth();
|
|
@@ -1314,23 +1739,75 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1314
1739
|
// `labelThickness` band stays in the reported thickness and the chart
|
|
1315
1740
|
// gets re-padded inward (leaving whitespace on the scale's side), and any
|
|
1316
1741
|
// visible labels would be clipped against the canvas edge.
|
|
1317
|
-
const
|
|
1318
|
-
this.hasLabelPadding &&
|
|
1742
|
+
const aboveThreshold =
|
|
1319
1743
|
effectiveWidth >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS &&
|
|
1320
1744
|
effectiveHeight >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS;
|
|
1745
|
+
const showLabels = this.hasLabelPadding && aboveThreshold;
|
|
1746
|
+
// Below the threshold a ladder has no room, but min / 0 / max still has.
|
|
1747
|
+
const compactY = !aboveThreshold && this.rangeLabelsY;
|
|
1748
|
+
const compactX = !aboveThreshold && this.rangeLabelsX;
|
|
1749
|
+
const compactYThickness = compactY
|
|
1750
|
+
? this.compactLabelThickness('y')
|
|
1751
|
+
: undefined;
|
|
1752
|
+
const compactXThickness = compactX
|
|
1753
|
+
? this.compactLabelThickness('x')
|
|
1754
|
+
: undefined;
|
|
1755
|
+
|
|
1756
|
+
// Compact mode labels a slotted scale's main tickmarks; a scale the chart
|
|
1757
|
+
// never compacted keeps whatever the consumer set.
|
|
1758
|
+
const mainLabelsFor = (
|
|
1759
|
+
scale: ExternalScaleElement,
|
|
1760
|
+
compact: boolean
|
|
1761
|
+
): boolean | undefined => {
|
|
1762
|
+
if (compact) {
|
|
1763
|
+
if (!this.savedMainTickmarkLabels.has(scale)) {
|
|
1764
|
+
this.savedMainTickmarkLabels.set(
|
|
1765
|
+
scale,
|
|
1766
|
+
scale.showMainTickmarkLabels ?? false
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
if (!this.savedMainTickmarkLabels.has(scale)) return undefined;
|
|
1772
|
+
const saved = this.savedMainTickmarkLabels.get(scale);
|
|
1773
|
+
this.savedMainTickmarkLabels.delete(scale);
|
|
1774
|
+
return saved;
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
// Narrow a scale's label band to its compact labels, and give the scale
|
|
1778
|
+
// its own band back once the chart leaves compact mode.
|
|
1779
|
+
const bandFor = (
|
|
1780
|
+
scale: ExternalScaleElement,
|
|
1781
|
+
compact: boolean,
|
|
1782
|
+
thickness: number | undefined
|
|
1783
|
+
): number | undefined => {
|
|
1784
|
+
if (compact) {
|
|
1785
|
+
if (
|
|
1786
|
+
thickness === undefined ||
|
|
1787
|
+
typeof scale.labelThickness !== 'number'
|
|
1788
|
+
) {
|
|
1789
|
+
return undefined;
|
|
1790
|
+
}
|
|
1791
|
+
if (!this.savedLabelThickness.has(scale)) {
|
|
1792
|
+
this.savedLabelThickness.set(scale, scale.labelThickness);
|
|
1793
|
+
}
|
|
1794
|
+
return thickness;
|
|
1795
|
+
}
|
|
1796
|
+
const saved = this.savedLabelThickness.get(scale);
|
|
1797
|
+
this.savedLabelThickness.delete(scale);
|
|
1798
|
+
return saved;
|
|
1799
|
+
};
|
|
1321
1800
|
|
|
1322
|
-
//
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
// viewBox padding needs to be: basePadding * scaleReferenceSize / referenceSize
|
|
1326
|
-
// This ensures the visual padding matches when the SVG scales to fill the container.
|
|
1801
|
+
// Visual pixels into a viewBox stretched to the *effective* length; the
|
|
1802
|
+
// reference `width` / `height` only matches in a reference-sized
|
|
1803
|
+
// container, and every other one drifts.
|
|
1327
1804
|
const verticalViewBoxPadding = this.fixedAspectRatioScaling
|
|
1328
1805
|
? {
|
|
1329
1806
|
top: Math.round(
|
|
1330
|
-
(padding.top * this.scaleReferenceSize) /
|
|
1807
|
+
(padding.top * this.scaleReferenceSize) / effectiveHeight
|
|
1331
1808
|
),
|
|
1332
1809
|
bottom: Math.round(
|
|
1333
|
-
(padding.bottom * this.scaleReferenceSize) /
|
|
1810
|
+
(padding.bottom * this.scaleReferenceSize) / effectiveHeight
|
|
1334
1811
|
),
|
|
1335
1812
|
}
|
|
1336
1813
|
: {top: padding.top, bottom: padding.bottom};
|
|
@@ -1338,10 +1815,10 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1338
1815
|
const horizontalViewBoxPadding = this.fixedAspectRatioScaling
|
|
1339
1816
|
? {
|
|
1340
1817
|
left: Math.round(
|
|
1341
|
-
(padding.left * this.scaleReferenceSize) /
|
|
1818
|
+
(padding.left * this.scaleReferenceSize) / effectiveWidth
|
|
1342
1819
|
),
|
|
1343
1820
|
right: Math.round(
|
|
1344
|
-
(padding.right * this.scaleReferenceSize) /
|
|
1821
|
+
(padding.right * this.scaleReferenceSize) / effectiveWidth
|
|
1345
1822
|
),
|
|
1346
1823
|
}
|
|
1347
1824
|
: {left: padding.left, right: padding.right};
|
|
@@ -1355,20 +1832,28 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1355
1832
|
]
|
|
1356
1833
|
> = [];
|
|
1357
1834
|
|
|
1835
|
+
/** Empty on a side whose slotted scale carries a range of its own. */
|
|
1836
|
+
const rangeProps = (
|
|
1837
|
+
side: 'left' | 'right' | 'top' | 'bottom',
|
|
1838
|
+
range: {min: number; max: number}
|
|
1839
|
+
): Partial<ExternalScaleElement> =>
|
|
1840
|
+
this.ownsSlottedScaleRange(side)
|
|
1841
|
+
? {minValue: range.min, maxValue: range.max}
|
|
1842
|
+
: {};
|
|
1843
|
+
|
|
1358
1844
|
// Left scale
|
|
1359
1845
|
if (this.leftScaleSlot) {
|
|
1360
1846
|
const scales =
|
|
1361
1847
|
this.leftScaleSlot.assignedElements() as ExternalScaleElement[];
|
|
1362
1848
|
scales.forEach((scale) => {
|
|
1363
1849
|
const props: Partial<ExternalScaleElement> = {
|
|
1364
|
-
|
|
1365
|
-
maxValue: yMax,
|
|
1850
|
+
...rangeProps('left', leftRange),
|
|
1366
1851
|
height: effectiveHeight, // Use effective height for proper sizing
|
|
1367
1852
|
paddingTop: verticalViewBoxPadding.top,
|
|
1368
1853
|
paddingBottom: verticalViewBoxPadding.bottom,
|
|
1369
1854
|
paddingStart: verticalViewBoxPadding.top,
|
|
1370
1855
|
paddingEnd: verticalViewBoxPadding.bottom,
|
|
1371
|
-
showLabels,
|
|
1856
|
+
showLabels: showLabels || compactY,
|
|
1372
1857
|
fixedAspectRatio: this.fixedAspectRatioScaling,
|
|
1373
1858
|
// Use chart's scaleReferenceSize property for proportional scaling
|
|
1374
1859
|
scaleReferenceSize: this.scaleReferenceSize,
|
|
@@ -1381,6 +1866,10 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1381
1866
|
if (this.yStepSize !== undefined) {
|
|
1382
1867
|
props.primaryTickmarkInterval = this.yStepSize;
|
|
1383
1868
|
}
|
|
1869
|
+
const band = bandFor(scale, compactY, compactYThickness);
|
|
1870
|
+
const mainLabels = mainLabelsFor(scale, compactY);
|
|
1871
|
+
if (mainLabels !== undefined) props.showMainTickmarkLabels = mainLabels;
|
|
1872
|
+
if (band !== undefined) props.labelThickness = band;
|
|
1384
1873
|
updates.push([this.leftScaleSlot, scale, props]);
|
|
1385
1874
|
});
|
|
1386
1875
|
}
|
|
@@ -1391,14 +1880,13 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1391
1880
|
this.rightScaleSlot.assignedElements() as ExternalScaleElement[];
|
|
1392
1881
|
scales.forEach((scale) => {
|
|
1393
1882
|
const props: Partial<ExternalScaleElement> = {
|
|
1394
|
-
|
|
1395
|
-
maxValue: yMax,
|
|
1883
|
+
...rangeProps('right', rightRange),
|
|
1396
1884
|
height: effectiveHeight, // Use effective height for proper sizing
|
|
1397
1885
|
paddingTop: verticalViewBoxPadding.top,
|
|
1398
1886
|
paddingBottom: verticalViewBoxPadding.bottom,
|
|
1399
1887
|
paddingStart: verticalViewBoxPadding.top,
|
|
1400
1888
|
paddingEnd: verticalViewBoxPadding.bottom,
|
|
1401
|
-
showLabels,
|
|
1889
|
+
showLabels: showLabels || compactY,
|
|
1402
1890
|
fixedAspectRatio: this.fixedAspectRatioScaling,
|
|
1403
1891
|
// Use chart's scaleReferenceSize property for proportional scaling
|
|
1404
1892
|
scaleReferenceSize: this.scaleReferenceSize,
|
|
@@ -1411,6 +1899,10 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1411
1899
|
if (this.yStepSize !== undefined) {
|
|
1412
1900
|
props.primaryTickmarkInterval = this.yStepSize;
|
|
1413
1901
|
}
|
|
1902
|
+
const band = bandFor(scale, compactY, compactYThickness);
|
|
1903
|
+
const mainLabels = mainLabelsFor(scale, compactY);
|
|
1904
|
+
if (mainLabels !== undefined) props.showMainTickmarkLabels = mainLabels;
|
|
1905
|
+
if (band !== undefined) props.labelThickness = band;
|
|
1414
1906
|
updates.push([this.rightScaleSlot, scale, props]);
|
|
1415
1907
|
});
|
|
1416
1908
|
}
|
|
@@ -1421,14 +1913,13 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1421
1913
|
this.topScaleSlot.assignedElements() as ExternalScaleElement[];
|
|
1422
1914
|
scales.forEach((scale) => {
|
|
1423
1915
|
const props: Partial<ExternalScaleElement> = {
|
|
1424
|
-
|
|
1425
|
-
maxValue: xMax,
|
|
1916
|
+
...xRange,
|
|
1426
1917
|
width: effectiveWidth, // Use effective width for proper sizing
|
|
1427
1918
|
paddingLeft: horizontalViewBoxPadding.left,
|
|
1428
1919
|
paddingRight: horizontalViewBoxPadding.right,
|
|
1429
1920
|
paddingStart: horizontalViewBoxPadding.left,
|
|
1430
1921
|
paddingEnd: horizontalViewBoxPadding.right,
|
|
1431
|
-
showLabels,
|
|
1922
|
+
showLabels: showLabels || compactX,
|
|
1432
1923
|
fixedAspectRatio: this.fixedAspectRatioScaling,
|
|
1433
1924
|
// Use chart's scaleReferenceSize property for proportional scaling
|
|
1434
1925
|
scaleReferenceSize: this.scaleReferenceSize,
|
|
@@ -1441,6 +1932,10 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1441
1932
|
if (this.xStepSize !== undefined) {
|
|
1442
1933
|
props.primaryTickmarkInterval = this.xStepSize;
|
|
1443
1934
|
}
|
|
1935
|
+
const band = bandFor(scale, compactX, compactXThickness);
|
|
1936
|
+
const mainLabels = mainLabelsFor(scale, compactX);
|
|
1937
|
+
if (mainLabels !== undefined) props.showMainTickmarkLabels = mainLabels;
|
|
1938
|
+
if (band !== undefined) props.labelThickness = band;
|
|
1444
1939
|
updates.push([this.topScaleSlot, scale, props]);
|
|
1445
1940
|
});
|
|
1446
1941
|
}
|
|
@@ -1451,14 +1946,13 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1451
1946
|
this.bottomScaleSlot.assignedElements() as ExternalScaleElement[];
|
|
1452
1947
|
scales.forEach((scale) => {
|
|
1453
1948
|
const props: Partial<ExternalScaleElement> = {
|
|
1454
|
-
|
|
1455
|
-
maxValue: xMax,
|
|
1949
|
+
...xRange,
|
|
1456
1950
|
width: effectiveWidth, // Use effective width for proper sizing
|
|
1457
1951
|
paddingLeft: horizontalViewBoxPadding.left,
|
|
1458
1952
|
paddingRight: horizontalViewBoxPadding.right,
|
|
1459
1953
|
paddingStart: horizontalViewBoxPadding.left,
|
|
1460
1954
|
paddingEnd: horizontalViewBoxPadding.right,
|
|
1461
|
-
showLabels,
|
|
1955
|
+
showLabels: showLabels || compactX,
|
|
1462
1956
|
fixedAspectRatio: this.fixedAspectRatioScaling,
|
|
1463
1957
|
// Use chart's scaleReferenceSize property for proportional scaling
|
|
1464
1958
|
scaleReferenceSize: this.scaleReferenceSize,
|
|
@@ -1471,6 +1965,10 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1471
1965
|
if (this.xStepSize !== undefined) {
|
|
1472
1966
|
props.primaryTickmarkInterval = this.xStepSize;
|
|
1473
1967
|
}
|
|
1968
|
+
const band = bandFor(scale, compactX, compactXThickness);
|
|
1969
|
+
const mainLabels = mainLabelsFor(scale, compactX);
|
|
1970
|
+
if (mainLabels !== undefined) props.showMainTickmarkLabels = mainLabels;
|
|
1971
|
+
if (band !== undefined) props.labelThickness = band;
|
|
1474
1972
|
updates.push([this.bottomScaleSlot, scale, props]);
|
|
1475
1973
|
});
|
|
1476
1974
|
}
|
|
@@ -1481,6 +1979,42 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1481
1979
|
});
|
|
1482
1980
|
}
|
|
1483
1981
|
|
|
1982
|
+
/**
|
|
1983
|
+
* Whether the chart's axis range is what a slotted scale on this side should
|
|
1984
|
+
* describe. A subclass that slots a scale of its own and gives it a separate
|
|
1985
|
+
* range returns false for that side, otherwise the sync below overwrites it.
|
|
1986
|
+
*/
|
|
1987
|
+
protected ownsSlottedScaleRange(
|
|
1988
|
+
_side: 'left' | 'right' | 'top' | 'bottom'
|
|
1989
|
+
): boolean {
|
|
1990
|
+
return true;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Push the current axis ranges to the slotted scales. The full
|
|
1995
|
+
* `updateScaleProperties()` cascade runs before the chart is (re)built and
|
|
1996
|
+
* so reads the previous chart's ranges; this closes that gap once the new
|
|
1997
|
+
* chart exists, and keeps auto-ranged scales in step with the data.
|
|
1998
|
+
*/
|
|
1999
|
+
private syncSlottedScaleRanges() {
|
|
2000
|
+
const apply = (
|
|
2001
|
+
side: 'left' | 'right' | 'top' | 'bottom',
|
|
2002
|
+
slot: HTMLSlotElement | undefined,
|
|
2003
|
+
range: {min: number; max: number} | undefined
|
|
2004
|
+
) => {
|
|
2005
|
+
if (!slot || !range || !this.ownsSlottedScaleRange(side)) return;
|
|
2006
|
+
(slot.assignedElements() as ExternalScaleElement[]).forEach((scale) => {
|
|
2007
|
+
scale.minValue = range.min;
|
|
2008
|
+
scale.maxValue = range.max;
|
|
2009
|
+
});
|
|
2010
|
+
};
|
|
2011
|
+
apply('left', this.leftScaleSlot, this.resolveAxisRange('left'));
|
|
2012
|
+
apply('right', this.rightScaleSlot, this.resolveAxisRange('right'));
|
|
2013
|
+
const xRange = this.resolveSlottedXRange();
|
|
2014
|
+
apply('top', this.topScaleSlot, xRange);
|
|
2015
|
+
apply('bottom', this.bottomScaleSlot, xRange);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1484
2018
|
private hasAnyChanged(
|
|
1485
2019
|
changed: PropertyValues,
|
|
1486
2020
|
props: readonly (keyof ObcChartLineBase)[]
|
|
@@ -1611,13 +2145,21 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1611
2145
|
if (this.updateComputedDimensions()) return;
|
|
1612
2146
|
}
|
|
1613
2147
|
|
|
1614
|
-
// `
|
|
1615
|
-
//
|
|
1616
|
-
//
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2148
|
+
// Only `syncScalesAndChart()` re-cascades to slotted scales, and these are
|
|
2149
|
+
// the changes that need it: the two flags decide what a scale renders, and
|
|
2150
|
+
// a threshold crossing reaches a plain rebuild in pixel mode.
|
|
2151
|
+
const belowThreshold = this.isBelowThreshold();
|
|
2152
|
+
const crossedThreshold =
|
|
2153
|
+
this.wasBelowThreshold !== undefined &&
|
|
2154
|
+
this.wasBelowThreshold !== belowThreshold;
|
|
2155
|
+
this.wasBelowThreshold = belowThreshold;
|
|
2156
|
+
|
|
2157
|
+
if (
|
|
2158
|
+
(changed.has('hasLabelPadding') ||
|
|
2159
|
+
changed.has('rangeLabels') ||
|
|
2160
|
+
crossedThreshold) &&
|
|
2161
|
+
this.hasExternalScales()
|
|
2162
|
+
) {
|
|
1621
2163
|
this.syncScalesAndChart();
|
|
1622
2164
|
return;
|
|
1623
2165
|
}
|
|
@@ -1689,7 +2231,9 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1689
2231
|
this.canvasEl,
|
|
1690
2232
|
() =>
|
|
1691
2233
|
(this.canvasEl?.clientHeight ?? 0) >=
|
|
1692
|
-
|
|
2234
|
+
RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS &&
|
|
2235
|
+
(this.canvasEl?.clientWidth ?? 0) >=
|
|
2236
|
+
RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS,
|
|
1693
2237
|
{
|
|
1694
2238
|
rebuild: () => {
|
|
1695
2239
|
this.chart?.destroy();
|
|
@@ -1865,6 +2409,7 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1865
2409
|
const result = {
|
|
1866
2410
|
...(existingDataset || {}),
|
|
1867
2411
|
data: existingDataset?.data ?? values!,
|
|
2412
|
+
yAxisID: existingDataset?.yAxisID ?? this.primaryYAxisId,
|
|
1868
2413
|
borderColor,
|
|
1869
2414
|
backgroundColor,
|
|
1870
2415
|
borderWidth: existingDataset?.borderWidth ?? 2,
|
|
@@ -1922,6 +2467,7 @@ export class ObcChartLineBase extends LitElement {
|
|
|
1922
2467
|
const baselineDataset: ChartDataset<'line', ChartLinePoint[]> = {
|
|
1923
2468
|
label: 'threshold-baseline',
|
|
1924
2469
|
data: baselineData,
|
|
2470
|
+
yAxisID: this.primaryYAxisId,
|
|
1925
2471
|
borderColor: 'transparent',
|
|
1926
2472
|
backgroundColor: 'transparent',
|
|
1927
2473
|
borderWidth: 0,
|
|
@@ -2040,6 +2586,62 @@ export class ObcChartLineBase extends LitElement {
|
|
|
2040
2586
|
return {datasets, labels};
|
|
2041
2587
|
}
|
|
2042
2588
|
|
|
2589
|
+
/**
|
|
2590
|
+
* One padding source for the Chart.js layout and the slotted scales: two
|
|
2591
|
+
* sources painted the canvas over a scale and inset a scale inside a chart.
|
|
2592
|
+
*/
|
|
2593
|
+
private computeChartPadding(): {
|
|
2594
|
+
top: number;
|
|
2595
|
+
right: number;
|
|
2596
|
+
bottom: number;
|
|
2597
|
+
left: number;
|
|
2598
|
+
} {
|
|
2599
|
+
const isTooSmall = this.isBelowThreshold();
|
|
2600
|
+
// Get scale factor for proportional scaling in fixed aspect ratio mode
|
|
2601
|
+
const scaleFactor = this.getScaleFactor();
|
|
2602
|
+
|
|
2603
|
+
const defaultPaddingScaled = !this.hasLabelPadding
|
|
2604
|
+
? 0
|
|
2605
|
+
: this.fixedAspectRatioScaling
|
|
2606
|
+
? Math.round(CHART_DIMENSIONS.CANVAS_PADDING * scaleFactor)
|
|
2607
|
+
: CHART_DIMENSIONS.CANVAS_PADDING;
|
|
2608
|
+
|
|
2609
|
+
const {y: yTexts, x: xTexts} = this.refreshRangeLabels();
|
|
2610
|
+
const context = this.canvasEl?.getContext('2d');
|
|
2611
|
+
const gutters =
|
|
2612
|
+
(yTexts.length || xTexts.length) && context
|
|
2613
|
+
? measureRangeLabelGutters(
|
|
2614
|
+
context,
|
|
2615
|
+
this.rangeLabelFont(),
|
|
2616
|
+
yTexts,
|
|
2617
|
+
xTexts
|
|
2618
|
+
)
|
|
2619
|
+
: {side: 0, bottom: 0};
|
|
2620
|
+
|
|
2621
|
+
const scalePadding = this.calculatePaddingFromScales();
|
|
2622
|
+
const freeSide = (side: 'top' | 'right' | 'bottom' | 'left'): number => {
|
|
2623
|
+
if (!isTooSmall) return defaultPaddingScaled;
|
|
2624
|
+
if (side === 'bottom') return gutters.bottom;
|
|
2625
|
+
return side === this.ySide ? gutters.side : 0;
|
|
2626
|
+
};
|
|
2627
|
+
const padding = {
|
|
2628
|
+
top: this.externalScaleDimensions.has('top')
|
|
2629
|
+
? scalePadding.top
|
|
2630
|
+
: freeSide('top'),
|
|
2631
|
+
right: this.externalScaleDimensions.has('right')
|
|
2632
|
+
? scalePadding.right
|
|
2633
|
+
: freeSide('right'),
|
|
2634
|
+
bottom: this.externalScaleDimensions.has('bottom')
|
|
2635
|
+
? scalePadding.bottom
|
|
2636
|
+
: freeSide('bottom'),
|
|
2637
|
+
left: this.externalScaleDimensions.has('left')
|
|
2638
|
+
? scalePadding.left
|
|
2639
|
+
: freeSide('left'),
|
|
2640
|
+
};
|
|
2641
|
+
|
|
2642
|
+
return padding;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2043
2645
|
/**
|
|
2044
2646
|
* Get Chart.js options with dynamic sizing and padding
|
|
2045
2647
|
*/
|
|
@@ -2053,53 +2655,7 @@ export class ObcChartLineBase extends LitElement {
|
|
|
2053
2655
|
effectiveWidth < RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS ||
|
|
2054
2656
|
effectiveHeight < RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS;
|
|
2055
2657
|
|
|
2056
|
-
|
|
2057
|
-
const scaleFactor = this.getScaleFactor();
|
|
2058
|
-
|
|
2059
|
-
// Calculate padding for the chart.
|
|
2060
|
-
// External scales report their actual visual (scaled) thickness when in fixedAspectRatio mode,
|
|
2061
|
-
// so we use those values directly without additional scaling.
|
|
2062
|
-
// For sides without external scales, we apply the chart's scaleFactor to default padding.
|
|
2063
|
-
let padding: {top: number; right: number; bottom: number; left: number};
|
|
2064
|
-
|
|
2065
|
-
// When hasLabelPadding=false the chart never reserves space for axis tick
|
|
2066
|
-
// labels on sides without an external scale (renders edge-to-edge there).
|
|
2067
|
-
// External-scale sides still receive their reported visual thickness so
|
|
2068
|
-
// slotted scales/bars remain fully visible.
|
|
2069
|
-
const defaultPaddingScaled = !this.hasLabelPadding
|
|
2070
|
-
? 0
|
|
2071
|
-
: this.fixedAspectRatioScaling
|
|
2072
|
-
? Math.round(CHART_DIMENSIONS.CANVAS_PADDING * scaleFactor)
|
|
2073
|
-
: CHART_DIMENSIONS.CANVAS_PADDING;
|
|
2074
|
-
|
|
2075
|
-
if (isTooSmall && this.hasLabelPadding) {
|
|
2076
|
-
padding = {top: 0, right: 0, bottom: 0, left: 0};
|
|
2077
|
-
} else if (this.hasExternalScales()) {
|
|
2078
|
-
// External scales report their visual dimensions (already scaled when fixedAspectRatio=true)
|
|
2079
|
-
const scalePadding = this.calculatePaddingFromScales();
|
|
2080
|
-
padding = {
|
|
2081
|
-
top: this.externalScaleDimensions.has('top')
|
|
2082
|
-
? scalePadding.top
|
|
2083
|
-
: defaultPaddingScaled,
|
|
2084
|
-
right: this.externalScaleDimensions.has('right')
|
|
2085
|
-
? scalePadding.right
|
|
2086
|
-
: defaultPaddingScaled,
|
|
2087
|
-
bottom: this.externalScaleDimensions.has('bottom')
|
|
2088
|
-
? scalePadding.bottom
|
|
2089
|
-
: defaultPaddingScaled,
|
|
2090
|
-
left: this.externalScaleDimensions.has('left')
|
|
2091
|
-
? scalePadding.left
|
|
2092
|
-
: defaultPaddingScaled,
|
|
2093
|
-
};
|
|
2094
|
-
} else {
|
|
2095
|
-
// No external scales - apply scaleFactor to all default padding
|
|
2096
|
-
padding = {
|
|
2097
|
-
top: defaultPaddingScaled,
|
|
2098
|
-
right: defaultPaddingScaled,
|
|
2099
|
-
bottom: defaultPaddingScaled,
|
|
2100
|
-
left: defaultPaddingScaled,
|
|
2101
|
-
};
|
|
2102
|
-
}
|
|
2658
|
+
const padding = this.computeChartPadding();
|
|
2103
2659
|
|
|
2104
2660
|
// Set CSS variables for wrapper and canvas sizing
|
|
2105
2661
|
if (this.fixedAspectRatioScaling) {
|
|
@@ -2169,10 +2725,18 @@ export class ObcChartLineBase extends LitElement {
|
|
|
2169
2725
|
/**
|
|
2170
2726
|
* Compute reference timestamp for time axis formatting.
|
|
2171
2727
|
* Returns earliest timestamp for 'date' mode, latest for 'minutes' mode.
|
|
2728
|
+
* A pinned x range supplies the reference instead, so the axis edge stays
|
|
2729
|
+
* the reference while the data has not reached it yet.
|
|
2172
2730
|
*/
|
|
2173
2731
|
private computeTimeReference(): number | undefined {
|
|
2174
2732
|
if (this.xAxisType !== XAxisType.time) return undefined;
|
|
2175
2733
|
|
|
2734
|
+
const pinned =
|
|
2735
|
+
this.timeDisplay === TimeDisplay.minutes
|
|
2736
|
+
? this.xAxis?.max
|
|
2737
|
+
: this.xAxis?.min;
|
|
2738
|
+
if (pinned !== undefined && Number.isFinite(pinned)) return pinned;
|
|
2739
|
+
|
|
2176
2740
|
const timestamps: number[] = [];
|
|
2177
2741
|
|
|
2178
2742
|
// Collect timestamps from datasets
|
|
@@ -2267,6 +2831,9 @@ export class ObcChartLineBase extends LitElement {
|
|
|
2267
2831
|
offset: false, // Always edge-to-edge (no padding on x-axis)
|
|
2268
2832
|
grace: 0, // No extra margin
|
|
2269
2833
|
bounds: 'data', // Use data bounds for edge-to-edge rendering
|
|
2834
|
+
// A category axis has no numeric range to pin.
|
|
2835
|
+
min: this.isNumericXAxis ? this.xAxis?.min : undefined,
|
|
2836
|
+
max: this.isNumericXAxis ? this.xAxis?.max : undefined,
|
|
2270
2837
|
grid: {
|
|
2271
2838
|
display: this.showGrid && this.showGridX,
|
|
2272
2839
|
color: gridColor,
|
|
@@ -2381,12 +2948,18 @@ export class ObcChartLineBase extends LitElement {
|
|
|
2381
2948
|
type: 'line',
|
|
2382
2949
|
data: {labels, datasets},
|
|
2383
2950
|
options: this.getChartOptions(),
|
|
2384
|
-
plugins:
|
|
2951
|
+
plugins: [
|
|
2952
|
+
...(this.borderRadiusPosition ? [this.createBorderPlugin()] : []),
|
|
2953
|
+
...(this.rangeLabels !== RangeLabels.none
|
|
2954
|
+
? [this.createRangeLabelsPlugin()]
|
|
2955
|
+
: []),
|
|
2956
|
+
],
|
|
2385
2957
|
} as ChartConfiguration<'line'>);
|
|
2386
2958
|
|
|
2387
2959
|
// Defer legend update to next tick to ensure Chart.js metadata is initialized
|
|
2388
2960
|
requestAnimationFrame(() => this.updateLegend());
|
|
2389
2961
|
this.applyFillModes();
|
|
2962
|
+
this.syncSlottedScaleRanges();
|
|
2390
2963
|
}
|
|
2391
2964
|
|
|
2392
2965
|
private updateChart() {
|
|
@@ -2403,6 +2976,7 @@ export class ObcChartLineBase extends LitElement {
|
|
|
2403
2976
|
|
|
2404
2977
|
this.applyFillModes();
|
|
2405
2978
|
this.chart.update();
|
|
2979
|
+
this.syncSlottedScaleRanges();
|
|
2406
2980
|
|
|
2407
2981
|
// Update legend after chart update completes to ensure metadata is ready
|
|
2408
2982
|
requestAnimationFrame(() => this.updateLegend());
|