@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.
Files changed (136) hide show
  1. package/.storybook/vitest.setup.ts +12 -1
  2. package/bundle/openbridge-webcomponents.bundle.js +989 -319
  3. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  4. package/custom-elements.json +1514 -77
  5. package/dist/alert-severity.d.ts +12 -9
  6. package/dist/alert-severity.d.ts.map +1 -1
  7. package/dist/alert-severity.js +30 -38
  8. package/dist/alert-severity.js.map +1 -1
  9. package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts +6 -0
  10. package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts.map +1 -1
  11. package/dist/building-blocks/bar-horizontal/bar-horizontal.js +6 -1
  12. package/dist/building-blocks/bar-horizontal/bar-horizontal.js.map +1 -1
  13. package/dist/building-blocks/bar-vertical/bar-vertical.d.ts +6 -0
  14. package/dist/building-blocks/bar-vertical/bar-vertical.d.ts.map +1 -1
  15. package/dist/building-blocks/bar-vertical/bar-vertical.js +6 -1
  16. package/dist/building-blocks/bar-vertical/bar-vertical.js.map +1 -1
  17. package/dist/building-blocks/chart-line/chart-line-base.d.ts +129 -0
  18. package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
  19. package/dist/building-blocks/chart-line/chart-line-base.js +464 -79
  20. package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
  21. package/dist/building-blocks/external-scale/external-scale.d.ts +11 -0
  22. package/dist/building-blocks/external-scale/external-scale.d.ts.map +1 -1
  23. package/dist/building-blocks/external-scale/external-scale.js +45 -12
  24. package/dist/building-blocks/external-scale/external-scale.js.map +1 -1
  25. package/dist/charthelpers/index.d.ts +1 -0
  26. package/dist/charthelpers/index.d.ts.map +1 -1
  27. package/dist/charthelpers/index.js +7 -1
  28. package/dist/charthelpers/index.js.map +1 -1
  29. package/dist/charthelpers/range-labels.d.ts +55 -0
  30. package/dist/charthelpers/range-labels.d.ts.map +1 -0
  31. package/dist/charthelpers/range-labels.js +36 -0
  32. package/dist/charthelpers/range-labels.js.map +1 -0
  33. package/dist/components/alert-button/alert-button.css.js +13 -21
  34. package/dist/components/alert-button/alert-button.css.js.map +1 -1
  35. package/dist/components/alert-button/alert-button.d.ts +9 -4
  36. package/dist/components/alert-button/alert-button.d.ts.map +1 -1
  37. package/dist/components/alert-button/alert-button.js +24 -15
  38. package/dist/components/alert-button/alert-button.js.map +1 -1
  39. package/dist/components/alert-frame/alert-frame.css.js +109 -33
  40. package/dist/components/alert-frame/alert-frame.css.js.map +1 -1
  41. package/dist/components/alert-frame/alert-frame.d.ts +34 -7
  42. package/dist/components/alert-frame/alert-frame.d.ts.map +1 -1
  43. package/dist/components/alert-frame/alert-frame.js +134 -30
  44. package/dist/components/alert-frame/alert-frame.js.map +1 -1
  45. package/dist/components/alert-icon/alert-icon.d.ts +10 -7
  46. package/dist/components/alert-icon/alert-icon.d.ts.map +1 -1
  47. package/dist/components/alert-icon/alert-icon.js +37 -60
  48. package/dist/components/alert-icon/alert-icon.js.map +1 -1
  49. package/dist/components/alert-menu-item/alert-menu-item.css.js +1 -4
  50. package/dist/components/alert-menu-item/alert-menu-item.css.js.map +1 -1
  51. package/dist/components/date-item/date-item.css.js +3 -3
  52. package/dist/components/keyboard-full/keyboard-full.css.js +4 -4
  53. package/dist/components/message-menu-item/message-menu-item.css.js +2 -6
  54. package/dist/components/message-menu-item/message-menu-item.css.js.map +1 -1
  55. package/dist/components/sequence-toolbar/sequence-toolbar.css.js +1 -2
  56. package/dist/components/sequence-toolbar/sequence-toolbar.css.js.map +1 -1
  57. package/dist/index.js +2 -1
  58. package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts +6 -0
  59. package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
  60. package/dist/navigation-instruments/gauge-trend/gauge-trend.js +8 -0
  61. package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
  62. package/dist/navigation-instruments/instrument-field/instrument-field.css.js +2 -2
  63. package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
  64. package/dist/navigation-instruments/readout/readout.js +2 -1
  65. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  66. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  67. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +2 -1
  68. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  69. package/dist/openbridge.css +19 -4
  70. package/dist/palettes/blinking.d.ts +14 -4
  71. package/dist/palettes/blinking.d.ts.map +1 -1
  72. package/dist/palettes/blinking.js +41 -35
  73. package/dist/palettes/blinking.js.map +1 -1
  74. package/dist/palettes/flashing-controller.d.ts +21 -0
  75. package/dist/palettes/flashing-controller.d.ts.map +1 -0
  76. package/dist/palettes/flashing-controller.js +45 -0
  77. package/dist/palettes/flashing-controller.js.map +1 -0
  78. package/dist/svghelpers/index.d.ts +1 -0
  79. package/dist/svghelpers/index.d.ts.map +1 -1
  80. package/dist/svghelpers/index.js +2 -0
  81. package/dist/svghelpers/index.js.map +1 -1
  82. package/dist/svghelpers/rounded-rect.d.ts +15 -0
  83. package/dist/svghelpers/rounded-rect.d.ts.map +1 -0
  84. package/dist/svghelpers/rounded-rect.js +29 -0
  85. package/dist/svghelpers/rounded-rect.js.map +1 -0
  86. package/dist/types.d.ts +14 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js +9 -0
  89. package/dist/types.js.map +1 -1
  90. package/package.json +1 -1
  91. package/script/check-css-variables.ts +2 -15
  92. package/src/alert-severity.spec.ts +56 -0
  93. package/src/alert-severity.ts +45 -43
  94. package/src/bars-graphs/area-graph/area-graph.stories.ts +235 -1
  95. package/src/bars-graphs/line-graph/line-graph.stories.ts +24 -0
  96. package/src/building-blocks/bar-horizontal/bar-horizontal.ts +7 -0
  97. package/src/building-blocks/bar-vertical/bar-vertical.ts +7 -0
  98. package/src/building-blocks/chart-line/chart-line-base.spec.ts +767 -0
  99. package/src/building-blocks/chart-line/chart-line-base.stories.ts +124 -0
  100. package/src/building-blocks/chart-line/chart-line-base.ts +671 -97
  101. package/src/building-blocks/chart-line/chart-sizing-battleground.stories.ts +17 -0
  102. package/src/building-blocks/external-scale/external-scale.spec.ts +154 -0
  103. package/src/building-blocks/external-scale/external-scale.ts +78 -20
  104. package/src/charthelpers/index.ts +1 -0
  105. package/src/charthelpers/range-labels.spec.ts +46 -0
  106. package/src/charthelpers/range-labels.ts +93 -0
  107. package/src/components/alert-button/alert-button.css +10 -23
  108. package/src/components/alert-button/alert-button.spec.ts +56 -15
  109. package/src/components/alert-button/alert-button.stories.ts +5 -1
  110. package/src/components/alert-button/alert-button.ts +33 -24
  111. package/src/components/alert-frame/alert-flash-scenario.stories.ts +669 -0
  112. package/src/components/alert-frame/alert-frame.css +106 -33
  113. package/src/components/alert-frame/alert-frame.spec.ts +176 -17
  114. package/src/components/alert-frame/alert-frame.stories.ts +61 -0
  115. package/src/components/alert-frame/alert-frame.ts +179 -41
  116. package/src/components/alert-icon/alert-icon.spec.ts +82 -27
  117. package/src/components/alert-icon/alert-icon.stories.ts +7 -1
  118. package/src/components/alert-icon/alert-icon.ts +48 -74
  119. package/src/components/alert-menu-item/alert-menu-item.css +1 -4
  120. package/src/components/date-item/date-item.css +3 -3
  121. package/src/components/keyboard-full/keyboard-full.css +4 -4
  122. package/src/components/message-menu-item/message-menu-item.css +2 -6
  123. package/src/components/sequence-toolbar/sequence-toolbar.css +1 -2
  124. package/src/navigation-instruments/gauge-trend/gauge-trend.stories.ts +29 -0
  125. package/src/navigation-instruments/gauge-trend/gauge-trend.ts +11 -0
  126. package/src/navigation-instruments/instrument-field/instrument-field.css +2 -2
  127. package/src/navigation-instruments/readout/readout.ts +2 -1
  128. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +2 -1
  129. package/src/palettes/blinking.spec.ts +83 -0
  130. package/src/palettes/blinking.ts +47 -35
  131. package/src/palettes/flashing-controller.ts +62 -0
  132. package/src/palettes/manual.css +18 -4
  133. package/src/svghelpers/index.ts +1 -0
  134. package/src/svghelpers/rounded-rect.spec.ts +47 -0
  135. package/src/svghelpers/rounded-rect.ts +42 -0
  136. package/src/types.ts +20 -0
@@ -6,14 +6,15 @@ import chartDebugStyle from "../../charthelpers/chart-debug.css.js";
6
6
  import chartLegendStyle from "../../charthelpers/chart-legend.css.js";
7
7
  import { Chart, LineController, LineElement, PointElement, ArcElement, CategoryScale, LinearScale, TimeScale, Filler, Tooltip } from "chart.js";
8
8
  import { InstrumentState, Priority, FrameStyle, BorderRadiusPosition } from "../../navigation-instruments/types.js";
9
- import { CHART_DIMENSIONS, RECTANGULAR_CHART_DIMENSIONS, CHART_SECTOR_ENHANCED_COLORS, CHART_SECTOR_DEFAULT_COLORS, CHART_AREA_BACKGROUND_COLOR_VAR, LINE_GRAPH_GRID_CONFIG, LINE_GRAPH_LABEL_CONFIG } from "../../charthelpers/constants.js";
9
+ import { RECTANGULAR_CHART_DIMENSIONS, CHART_DIMENSIONS, CHART_SECTOR_ENHANCED_COLORS, CHART_SECTOR_DEFAULT_COLORS, LINE_GRAPH_LABEL_CONFIG, CHART_AREA_BACKGROUND_COLOR_VAR, LINE_GRAPH_GRID_CONFIG } from "../../charthelpers/constants.js";
10
10
  import { getCssVariableValue, applyAlphaToColor, getChartColorsOrDefault } from "../../charthelpers/colors.js";
11
11
  import { observeThemeChanges } from "../../charthelpers/theme.js";
12
12
  import { formatChartNumber } from "../../charthelpers/canvas-layout.js";
13
13
  import { getChartTooltipOptions } from "../../charthelpers/tooltip.js";
14
14
  import { generateLegendHTML } from "../../charthelpers/legend.js";
15
- import { XValueMode, normalizeXValue, formatXValue } from "../../charthelpers/x-value.js";
15
+ import { XValueMode, formatXValue, normalizeXValue } from "../../charthelpers/x-value.js";
16
16
  import { observeLabelThreshold } from "../../charthelpers/label-threshold.js";
17
+ import { yRangeLabelValues, measureRangeLabelGutters, formatRangeValue, rangeLabelLineHeight, rangeLabelFontString } from "../../charthelpers/range-labels.js";
17
18
  import { readExternalScaleBorderRadiusPx, ScaleType, startExternalScaleBorderRadiusObserver, EXTERNAL_SCALE_BORDER_RADIUS_CSS_VAR } from "../external-scale/external-scale.js";
18
19
  var __defProp = Object.defineProperty;
19
20
  var __decorateClass = (decorators, target, key, kind) => {
@@ -57,6 +58,13 @@ var TimeDisplay = /* @__PURE__ */ ((TimeDisplay2) => {
57
58
  TimeDisplay2["date"] = "date";
58
59
  return TimeDisplay2;
59
60
  })(TimeDisplay || {});
61
+ var RangeLabels = /* @__PURE__ */ ((RangeLabels2) => {
62
+ RangeLabels2["none"] = "none";
63
+ RangeLabels2["y"] = "y";
64
+ RangeLabels2["x"] = "x";
65
+ RangeLabels2["xy"] = "xy";
66
+ return RangeLabels2;
67
+ })(RangeLabels || {});
60
68
  const LINE_GRAPH_WATCHED_PROP_NAMES = [
61
69
  "data",
62
70
  "datasets",
@@ -65,6 +73,7 @@ const LINE_GRAPH_WATCHED_PROP_NAMES = [
65
73
  "xAxisType",
66
74
  "yAxisPosition",
67
75
  "yAxes",
76
+ "xAxis",
68
77
  "showGrid",
69
78
  "showGridX",
70
79
  "showGridY",
@@ -89,14 +98,17 @@ const LINE_GRAPH_WATCHED_PROP_NAMES = [
89
98
  "height",
90
99
  "fixedAspectRatioScaling",
91
100
  // Triggers responsive mode change
92
- "hasLabelPadding"
101
+ "hasLabelPadding",
93
102
  // Toggles edge-to-edge rendering and label visibility
103
+ "rangeLabels"
94
104
  ];
95
105
  const LINE_GRAPH_RECREATE_PROP_NAMES = [
96
106
  "showDebugOverlay",
97
107
  "width",
98
108
  "height",
99
- "fixedAspectRatioScaling"
109
+ "fixedAspectRatioScaling",
110
+ "rangeLabels"
111
+ // Adds or removes the range-labels plugin
100
112
  ];
101
113
  const LINE_GRAPH_DIMENSION_PROP_NAMES = [
102
114
  "width",
@@ -119,11 +131,14 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
119
131
  this.xAxisType = "category";
120
132
  this.yAxisPosition = "left";
121
133
  this.yAxes = void 0;
134
+ this.xAxis = void 0;
122
135
  this.showGrid = false;
123
136
  this.showGridX = false;
124
137
  this.showGridY = false;
125
138
  this.showTickMarks = false;
126
139
  this.hasLabelPadding = true;
140
+ this.rangeLabels = "none";
141
+ this.warnedDateDisplayScale = false;
127
142
  this.DEFAULT_TENSION = 0.4;
128
143
  this.POINT_RADIUS = 3;
129
144
  this.showPoints = false;
@@ -146,6 +161,10 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
146
161
  this.currentBorderRadiusPx = 8;
147
162
  this.computedWidth = 480;
148
163
  this.computedHeight = 320;
164
+ this.savedLabelThickness = /* @__PURE__ */ new WeakMap();
165
+ this.savedMainTickmarkLabels = /* @__PURE__ */ new WeakMap();
166
+ this.lastRangeLabels = [];
167
+ this.rangeLabelCache = { y: [], x: [] };
149
168
  this.updateBorderRadius = () => {
150
169
  if (!this.canvasEl) return;
151
170
  let next;
@@ -185,10 +204,25 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
185
204
  }, 16);
186
205
  };
187
206
  }
207
+ /** @internal - The y side wants range labels; an edge-to-edge chart never does. */
208
+ get rangeLabelsY() {
209
+ return this.hasLabelPadding && (this.rangeLabels === "y" || this.rangeLabels === "xy");
210
+ }
211
+ /** @internal - The x side wants range labels; an edge-to-edge chart never does. */
212
+ get rangeLabelsX() {
213
+ return this.hasLabelPadding && (this.rangeLabels === "x" || this.rangeLabels === "xy");
214
+ }
188
215
  /** @internal - True when the x-axis positions points by numeric value. */
189
216
  get isNumericXAxis() {
190
217
  return this.xAxisType === "time" || this.xAxisType === "number";
191
218
  }
219
+ /**
220
+ * Id of the y scale a dataset lands on when it names none. Chart.js would
221
+ * otherwise add a default `y` scale next to the configured ones and draw it.
222
+ */
223
+ get primaryYAxisId() {
224
+ return this.yAxes?.length ? this.yAxes[0].id ?? "y0" : "y";
225
+ }
192
226
  /** @internal - Normalization mode for the current x-axis type. */
193
227
  get xValueMode() {
194
228
  return this.xAxisType === "number" ? XValueMode.number : XValueMode.time;
@@ -227,37 +261,221 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
227
261
  throw new Error("shouldStack() must be implemented in subclass");
228
262
  }
229
263
  /**
230
- * Compute which corners should be rounded based on borderRadiusPosition and
231
- * which external scales are present.
232
- *
233
- * Logic:
234
- * - Opposite scales (left+right OR top+bottom) → no rounding (middleChild behavior)
235
- * - Perpendicular scales (e.g., right+bottom):
236
- * - innerFirstChild → round corner adjacent to both scales
237
- * - outerLastChild → round free corner opposite to scales
238
- * - Single-side scales:
239
- * - When external scale is on RIGHT, chart is on LEFT
240
- * - innerFirstChild → round LEFT corners (top-left + bottom-left)
241
- * - outerLastChild → round RIGHT corners (top-right + bottom-right)
242
- * - When external scale is on LEFT, chart is on RIGHT (opposite)
243
- * - When external scale is on TOP, chart is on BOTTOM
244
- * - innerFirstChild → round BOTTOM corners (bottom-left + bottom-right)
245
- * - outerLastChild → round TOP corners (top-left + top-right)
246
- * - When external scale is on BOTTOM, chart is on TOP (opposite)
247
- * - middleChild → no rounding
248
- */
249
- /**
250
- * Check if a slotted scale element is actually visible (renders content).
251
- * For bar-vertical/bar-horizontal elements, checks hasBar and hasScale properties.
264
+ * Label band a slotted scale needs for its compact labels, in the scale's
265
+ * own viewBox units. The scale prints the raw values, so those are what is
266
+ * measured; the x band is one text line.
252
267
  */
253
- hasVisibleScale(side) {
254
- const slotMap = {
268
+ compactLabelThickness(axis) {
269
+ const context = this.canvasEl?.getContext("2d");
270
+ if (!context) return void 0;
271
+ const font = this.rangeLabelFont();
272
+ let visual;
273
+ if (axis === "y") {
274
+ const bounds = this.rangeLabelCache.bounds;
275
+ if (!bounds) return void 0;
276
+ const texts = yRangeLabelValues(bounds.min, bounds.max).map(String);
277
+ visual = measureRangeLabelGutters(context, font, texts, []).side + 4;
278
+ } else {
279
+ visual = measureRangeLabelGutters(context, font, [], ["x"]).bottom + 8;
280
+ }
281
+ if (!this.fixedAspectRatioScaling) return Math.ceil(visual);
282
+ const effectiveLength = axis === "y" ? this.getEffectiveHeight() : this.getEffectiveWidth();
283
+ return Math.ceil(visual * this.scaleReferenceSize / effectiveLength);
284
+ }
285
+ slotFor(side) {
286
+ return {
255
287
  left: this.leftScaleSlot,
256
288
  right: this.rightScaleSlot,
257
289
  top: this.topScaleSlot,
258
290
  bottom: this.bottomScaleSlot
291
+ }[side];
292
+ }
293
+ hasSlottedScale(side) {
294
+ return (this.slotFor(side)?.assignedElements().length ?? 0) > 0;
295
+ }
296
+ /** @internal - Below the label threshold on either dimension. */
297
+ isBelowThreshold() {
298
+ return this.getEffectiveWidth() < RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS || this.getEffectiveHeight() < RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS;
299
+ }
300
+ get ySide() {
301
+ const first = this.yAxes?.[0]?.position;
302
+ return (first ?? this.yAxisPosition) === "right" ? "right" : "left";
303
+ }
304
+ rangeLabelFont() {
305
+ const sizePx = Number.parseFloat(
306
+ getCssVariableValue(this, LINE_GRAPH_LABEL_CONFIG.fontSizeVar)
307
+ );
308
+ return {
309
+ family: getCssVariableValue(this, LINE_GRAPH_LABEL_CONFIG.fontFamily),
310
+ sizePx: Number.isFinite(sizePx) ? sizePx : 12,
311
+ weight: getCssVariableValue(this, LINE_GRAPH_LABEL_CONFIG.fontWeightVar)
259
312
  };
260
- const slot = slotMap[side];
313
+ }
314
+ /**
315
+ * Extent of the prepared data: one for x, one per y scale id. Each y axis
316
+ * ranges from its own datasets, so a merged extent would label one axis
317
+ * with another's values.
318
+ */
319
+ dataExtents(prepared) {
320
+ const x = { min: Infinity, max: -Infinity };
321
+ const y = /* @__PURE__ */ new Map();
322
+ const take = (extent, v) => {
323
+ if (!Number.isFinite(v)) return;
324
+ extent.min = Math.min(extent.min, v);
325
+ extent.max = Math.max(extent.max, v);
326
+ };
327
+ prepared.datasets.forEach((dataset) => {
328
+ const id = dataset.yAxisID ?? this.primaryYAxisId;
329
+ const extent = y.get(id) ?? { min: Infinity, max: -Infinity };
330
+ y.set(id, extent);
331
+ dataset.data.forEach((point) => {
332
+ if (typeof point === "number") {
333
+ take(extent, point);
334
+ } else if (point) {
335
+ take(x, point.x);
336
+ take(extent, point.y);
337
+ }
338
+ });
339
+ });
340
+ y.forEach((extent, id) => {
341
+ if (!(extent.min <= extent.max)) y.delete(id);
342
+ });
343
+ return { x: x.min <= x.max ? x : void 0, y };
344
+ }
345
+ /**
346
+ * Range the y labels describe. Below the threshold the axis runs on data
347
+ * bounds, so its datasets' extent is exact and needs no chart to read from.
348
+ */
349
+ yRangeBounds(extents) {
350
+ let axis;
351
+ let id = "y";
352
+ if (this.yAxes?.length) {
353
+ const match = this.yAxes.findIndex(
354
+ (a) => (a.position ?? "left") === this.ySide
355
+ );
356
+ const index = match === -1 ? 0 : match;
357
+ axis = this.yAxes[index];
358
+ id = axis.id ?? `y${index}`;
359
+ }
360
+ const extent = extents.get(id);
361
+ const stackedScale = this.shouldStack() ? this.chart?.scales[id] : void 0;
362
+ const min = axis?.min ?? stackedScale?.min ?? extent?.min;
363
+ const max = axis?.max ?? stackedScale?.max ?? extent?.max;
364
+ return min !== void 0 && max !== void 0 ? { min, max } : void 0;
365
+ }
366
+ /** First and last x label, or nothing when there is no span to label. */
367
+ xEdgeLabelTexts(prepared, extent) {
368
+ if (!this.isNumericXAxis) {
369
+ const labels = prepared.labels.map(String);
370
+ const first = labels[0];
371
+ const last = labels[labels.length - 1];
372
+ return labels.length > 1 && first !== last ? [first, last] : [];
373
+ }
374
+ const min = this.xAxis?.min ?? extent?.min;
375
+ const max = this.xAxis?.max ?? extent?.max;
376
+ if (min === void 0 || max === void 0 || min === max) return [];
377
+ const reference = this.timeDisplay === "minutes" ? this.computeTimeReference() : void 0;
378
+ return [
379
+ formatXValue(min, this.xValueMode, reference),
380
+ formatXValue(max, this.xValueMode, reference)
381
+ ];
382
+ }
383
+ /**
384
+ * Recompute the range labels. A side with a slotted scale is labelled by
385
+ * the scale, so it gets no text here; the y bounds are kept regardless,
386
+ * since the scale's compact band is measured from them.
387
+ */
388
+ refreshRangeLabels() {
389
+ const cache = { y: [], x: [] };
390
+ if (this.isBelowThreshold() && (this.rangeLabelsY || this.rangeLabelsX)) {
391
+ const prepared = this.prepareChartDataAndLabels();
392
+ const extents = this.dataExtents(prepared);
393
+ if (this.rangeLabelsY) {
394
+ cache.bounds = this.yRangeBounds(extents.y);
395
+ if (cache.bounds && !this.hasSlottedScale("left") && !this.hasSlottedScale("right")) {
396
+ const { min, max } = cache.bounds;
397
+ cache.y = yRangeLabelValues(min, max).map(
398
+ (v) => formatRangeValue(v, min, max)
399
+ );
400
+ }
401
+ }
402
+ if (this.rangeLabelsX && !this.hasSlottedScale("top") && !this.hasSlottedScale("bottom")) {
403
+ cache.x = this.xEdgeLabelTexts(prepared, extents.x);
404
+ }
405
+ }
406
+ this.rangeLabelCache = cache;
407
+ return cache;
408
+ }
409
+ /**
410
+ * Paints min / 0 / max and first / last once Chart.js has drawn, inside the
411
+ * gutters `getChartOptions()` reserved. The end labels sit flush with the
412
+ * plot edges so no vertical padding is needed for them.
413
+ */
414
+ createRangeLabelsPlugin() {
415
+ return {
416
+ id: "rangeLabels",
417
+ afterDraw: (chart) => {
418
+ this.lastRangeLabels = [];
419
+ const { y: yTexts, x: xTexts, bounds } = this.rangeLabelCache;
420
+ if (!yTexts.length && !xTexts.length) return;
421
+ const area = chart.chartArea;
422
+ const ctx = chart.ctx;
423
+ const font = this.rangeLabelFont();
424
+ const lineHeight = rangeLabelLineHeight(font);
425
+ ctx.save();
426
+ ctx.font = rangeLabelFontString(font);
427
+ ctx.fillStyle = getCssVariableValue(
428
+ this,
429
+ LINE_GRAPH_LABEL_CONFIG.fontColorVar
430
+ );
431
+ const scale = chart.scales[this.primaryYAxisId];
432
+ if (yTexts.length && bounds && scale) {
433
+ const onRight = this.ySide === "right";
434
+ const x = onRight ? area.right + 8 : area.left - 8;
435
+ ctx.textAlign = onRight ? "left" : "right";
436
+ const values = yRangeLabelValues(bounds.min, bounds.max);
437
+ for (let i = values.length - 1; i >= 0; i--) {
438
+ const isMax = i === values.length - 1;
439
+ const isMin = i === 0;
440
+ const y = isMax ? area.top : isMin ? area.bottom : scale.getPixelForValue(values[i]);
441
+ if (!isMax && !isMin && (y - area.top < lineHeight || area.bottom - y < lineHeight)) {
442
+ continue;
443
+ }
444
+ ctx.textBaseline = isMax ? "top" : isMin ? "bottom" : "middle";
445
+ ctx.fillText(yTexts[i], x, y);
446
+ this.lastRangeLabels.push({ axis: "y", text: yTexts[i], x, y });
447
+ }
448
+ }
449
+ if (xTexts.length === 2) {
450
+ const y = area.bottom + 4;
451
+ ctx.textBaseline = "top";
452
+ ctx.textAlign = "left";
453
+ ctx.fillText(xTexts[0], area.left, y);
454
+ this.lastRangeLabels.push({
455
+ axis: "x",
456
+ text: xTexts[0],
457
+ x: area.left,
458
+ y
459
+ });
460
+ ctx.textAlign = "right";
461
+ ctx.fillText(xTexts[1], area.right, y);
462
+ this.lastRangeLabels.push({
463
+ axis: "x",
464
+ text: xTexts[1],
465
+ x: area.right,
466
+ y
467
+ });
468
+ }
469
+ ctx.restore();
470
+ }
471
+ };
472
+ }
473
+ /**
474
+ * Check if a slotted scale element is actually visible (renders content).
475
+ * For bar-vertical/bar-horizontal elements, checks hasBar and hasScale properties.
476
+ */
477
+ hasVisibleScale(side) {
478
+ const slot = this.slotFor(side);
261
479
  const elements = slot?.assignedElements() ?? [];
262
480
  return elements.some((el) => {
263
481
  const barEl = el;
@@ -637,13 +855,13 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
637
855
  if (this.isUpdatingScales) return false;
638
856
  this.isUpdatingScales = true;
639
857
  try {
640
- const padding = this.calculatePaddingFromScales();
641
- const effectiveWidth = this.width - padding.left - padding.right;
642
- const effectiveHeight = this.height - padding.top - padding.bottom;
858
+ const padding = this.computeChartPadding();
859
+ const effectiveWidth = this.getEffectiveWidth() - padding.left - padding.right;
860
+ const effectiveHeight = this.getEffectiveHeight() - padding.top - padding.bottom;
643
861
  if (effectiveWidth <= 0 || effectiveHeight <= 0) {
644
862
  console.warn("[chart-line-base] Invalid effective dimensions", {
645
- width: this.width,
646
- height: this.height,
863
+ width: this.getEffectiveWidth(),
864
+ height: this.getEffectiveHeight(),
647
865
  padding,
648
866
  effectiveWidth,
649
867
  effectiveHeight
@@ -680,47 +898,139 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
680
898
  };
681
899
  return padding;
682
900
  }
901
+ /**
902
+ * Range a slotted scale on the given side has to cover.
903
+ *
904
+ * A range pinned in the axis configuration is used as is: it is the source
905
+ * of truth, and on the first sync there is no chart to read yet. Otherwise
906
+ * the live Chart.js scale is read under the id `buildScalesConfig()` gives
907
+ * it. A side with no axis of its own follows the first configured one.
908
+ */
909
+ resolveAxisRange(side) {
910
+ let id = side === "x" ? "x" : "y";
911
+ let configured;
912
+ if (side === "x") {
913
+ configured = this.isNumericXAxis ? this.xAxis : void 0;
914
+ } else if (this.yAxes?.length) {
915
+ const match = this.yAxes.findIndex(
916
+ (axis2) => (axis2.position ?? "left") === side
917
+ );
918
+ const index = match === -1 ? 0 : match;
919
+ const axis = this.yAxes[index];
920
+ id = axis.id ?? `y${index}`;
921
+ configured = axis;
922
+ }
923
+ if (configured?.min !== void 0 && configured.max !== void 0) {
924
+ return { min: configured.min, max: configured.max };
925
+ }
926
+ const scale = this.chart?.scales[id];
927
+ return {
928
+ min: scale?.min ?? configured?.min ?? 0,
929
+ max: scale?.max ?? configured?.max ?? 100
930
+ };
931
+ }
932
+ /**
933
+ * Range pushed to the top and bottom scales, or `undefined` to leave them
934
+ * alone. A scale prints its values verbatim, and on a `time` axis the raw
935
+ * range is epoch milliseconds. In `minutes` display the range is converted
936
+ * to the minutes the chart's own labels show; `date` display has no numeric
937
+ * equivalent, so the scale keeps its own range.
938
+ */
939
+ resolveSlottedXRange() {
940
+ const range = this.resolveAxisRange("x");
941
+ if (this.xAxisType !== "time") return range;
942
+ if (this.timeDisplay !== "minutes") {
943
+ if (!this.warnedDateDisplayScale) {
944
+ this.warnedDateDisplayScale = true;
945
+ console.warn(
946
+ "[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."
947
+ );
948
+ }
949
+ return void 0;
950
+ }
951
+ const pinned = this.xAxis?.min !== void 0 && this.xAxis?.max !== void 0;
952
+ const reference = this.computeTimeReference();
953
+ if (reference === void 0 || !this.chart && !pinned) return void 0;
954
+ const minute = 6e4;
955
+ return {
956
+ min: (range.min - reference) / minute,
957
+ max: (range.max - reference) / minute
958
+ };
959
+ }
683
960
  /**
684
961
  * Update properties on slotted scale elements
685
962
  */
686
963
  updateScaleProperties(padding) {
687
- const yMin = this.chart?.scales["y"]?.min ?? 0;
688
- const yMax = this.chart?.scales["y"]?.max ?? 100;
689
- const xMin = this.chart?.scales["x"]?.min ?? 0;
690
- const xMax = this.chart?.scales["x"]?.max ?? 100;
964
+ const leftRange = this.resolveAxisRange("left");
965
+ const rightRange = this.resolveAxisRange("right");
966
+ const xRange = this.resolveSlottedXRange();
691
967
  const effectiveWidth = this.getEffectiveWidth();
692
968
  const effectiveHeight = this.getEffectiveHeight();
693
- const showLabels = this.hasLabelPadding && effectiveWidth >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS && effectiveHeight >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS;
969
+ const aboveThreshold = effectiveWidth >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS && effectiveHeight >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS;
970
+ const showLabels = this.hasLabelPadding && aboveThreshold;
971
+ const compactY = !aboveThreshold && this.rangeLabelsY;
972
+ const compactX = !aboveThreshold && this.rangeLabelsX;
973
+ const compactYThickness = compactY ? this.compactLabelThickness("y") : void 0;
974
+ const compactXThickness = compactX ? this.compactLabelThickness("x") : void 0;
975
+ const mainLabelsFor = (scale, compact) => {
976
+ if (compact) {
977
+ if (!this.savedMainTickmarkLabels.has(scale)) {
978
+ this.savedMainTickmarkLabels.set(
979
+ scale,
980
+ scale.showMainTickmarkLabels ?? false
981
+ );
982
+ }
983
+ return true;
984
+ }
985
+ if (!this.savedMainTickmarkLabels.has(scale)) return void 0;
986
+ const saved = this.savedMainTickmarkLabels.get(scale);
987
+ this.savedMainTickmarkLabels.delete(scale);
988
+ return saved;
989
+ };
990
+ const bandFor = (scale, compact, thickness) => {
991
+ if (compact) {
992
+ if (thickness === void 0 || typeof scale.labelThickness !== "number") {
993
+ return void 0;
994
+ }
995
+ if (!this.savedLabelThickness.has(scale)) {
996
+ this.savedLabelThickness.set(scale, scale.labelThickness);
997
+ }
998
+ return thickness;
999
+ }
1000
+ const saved = this.savedLabelThickness.get(scale);
1001
+ this.savedLabelThickness.delete(scale);
1002
+ return saved;
1003
+ };
694
1004
  const verticalViewBoxPadding = this.fixedAspectRatioScaling ? {
695
1005
  top: Math.round(
696
- padding.top * this.scaleReferenceSize / this.height
1006
+ padding.top * this.scaleReferenceSize / effectiveHeight
697
1007
  ),
698
1008
  bottom: Math.round(
699
- padding.bottom * this.scaleReferenceSize / this.height
1009
+ padding.bottom * this.scaleReferenceSize / effectiveHeight
700
1010
  )
701
1011
  } : { top: padding.top, bottom: padding.bottom };
702
1012
  const horizontalViewBoxPadding = this.fixedAspectRatioScaling ? {
703
1013
  left: Math.round(
704
- padding.left * this.scaleReferenceSize / this.width
1014
+ padding.left * this.scaleReferenceSize / effectiveWidth
705
1015
  ),
706
1016
  right: Math.round(
707
- padding.right * this.scaleReferenceSize / this.width
1017
+ padding.right * this.scaleReferenceSize / effectiveWidth
708
1018
  )
709
1019
  } : { left: padding.left, right: padding.right };
710
1020
  const updates = [];
1021
+ const rangeProps = (side, range) => this.ownsSlottedScaleRange(side) ? { minValue: range.min, maxValue: range.max } : {};
711
1022
  if (this.leftScaleSlot) {
712
1023
  const scales = this.leftScaleSlot.assignedElements();
713
1024
  scales.forEach((scale) => {
714
1025
  const props = {
715
- minValue: yMin,
716
- maxValue: yMax,
1026
+ ...rangeProps("left", leftRange),
717
1027
  height: effectiveHeight,
718
1028
  // Use effective height for proper sizing
719
1029
  paddingTop: verticalViewBoxPadding.top,
720
1030
  paddingBottom: verticalViewBoxPadding.bottom,
721
1031
  paddingStart: verticalViewBoxPadding.top,
722
1032
  paddingEnd: verticalViewBoxPadding.bottom,
723
- showLabels,
1033
+ showLabels: showLabels || compactY,
724
1034
  fixedAspectRatio: this.fixedAspectRatioScaling,
725
1035
  // Use chart's scaleReferenceSize property for proportional scaling
726
1036
  scaleReferenceSize: this.scaleReferenceSize,
@@ -732,6 +1042,10 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
732
1042
  if (this.yStepSize !== void 0) {
733
1043
  props.primaryTickmarkInterval = this.yStepSize;
734
1044
  }
1045
+ const band = bandFor(scale, compactY, compactYThickness);
1046
+ const mainLabels = mainLabelsFor(scale, compactY);
1047
+ if (mainLabels !== void 0) props.showMainTickmarkLabels = mainLabels;
1048
+ if (band !== void 0) props.labelThickness = band;
735
1049
  updates.push([this.leftScaleSlot, scale, props]);
736
1050
  });
737
1051
  }
@@ -739,15 +1053,14 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
739
1053
  const scales = this.rightScaleSlot.assignedElements();
740
1054
  scales.forEach((scale) => {
741
1055
  const props = {
742
- minValue: yMin,
743
- maxValue: yMax,
1056
+ ...rangeProps("right", rightRange),
744
1057
  height: effectiveHeight,
745
1058
  // Use effective height for proper sizing
746
1059
  paddingTop: verticalViewBoxPadding.top,
747
1060
  paddingBottom: verticalViewBoxPadding.bottom,
748
1061
  paddingStart: verticalViewBoxPadding.top,
749
1062
  paddingEnd: verticalViewBoxPadding.bottom,
750
- showLabels,
1063
+ showLabels: showLabels || compactY,
751
1064
  fixedAspectRatio: this.fixedAspectRatioScaling,
752
1065
  // Use chart's scaleReferenceSize property for proportional scaling
753
1066
  scaleReferenceSize: this.scaleReferenceSize,
@@ -759,6 +1072,10 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
759
1072
  if (this.yStepSize !== void 0) {
760
1073
  props.primaryTickmarkInterval = this.yStepSize;
761
1074
  }
1075
+ const band = bandFor(scale, compactY, compactYThickness);
1076
+ const mainLabels = mainLabelsFor(scale, compactY);
1077
+ if (mainLabels !== void 0) props.showMainTickmarkLabels = mainLabels;
1078
+ if (band !== void 0) props.labelThickness = band;
762
1079
  updates.push([this.rightScaleSlot, scale, props]);
763
1080
  });
764
1081
  }
@@ -766,15 +1083,14 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
766
1083
  const scales = this.topScaleSlot.assignedElements();
767
1084
  scales.forEach((scale) => {
768
1085
  const props = {
769
- minValue: xMin,
770
- maxValue: xMax,
1086
+ ...xRange,
771
1087
  width: effectiveWidth,
772
1088
  // Use effective width for proper sizing
773
1089
  paddingLeft: horizontalViewBoxPadding.left,
774
1090
  paddingRight: horizontalViewBoxPadding.right,
775
1091
  paddingStart: horizontalViewBoxPadding.left,
776
1092
  paddingEnd: horizontalViewBoxPadding.right,
777
- showLabels,
1093
+ showLabels: showLabels || compactX,
778
1094
  fixedAspectRatio: this.fixedAspectRatioScaling,
779
1095
  // Use chart's scaleReferenceSize property for proportional scaling
780
1096
  scaleReferenceSize: this.scaleReferenceSize,
@@ -786,6 +1102,10 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
786
1102
  if (this.xStepSize !== void 0) {
787
1103
  props.primaryTickmarkInterval = this.xStepSize;
788
1104
  }
1105
+ const band = bandFor(scale, compactX, compactXThickness);
1106
+ const mainLabels = mainLabelsFor(scale, compactX);
1107
+ if (mainLabels !== void 0) props.showMainTickmarkLabels = mainLabels;
1108
+ if (band !== void 0) props.labelThickness = band;
789
1109
  updates.push([this.topScaleSlot, scale, props]);
790
1110
  });
791
1111
  }
@@ -793,15 +1113,14 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
793
1113
  const scales = this.bottomScaleSlot.assignedElements();
794
1114
  scales.forEach((scale) => {
795
1115
  const props = {
796
- minValue: xMin,
797
- maxValue: xMax,
1116
+ ...xRange,
798
1117
  width: effectiveWidth,
799
1118
  // Use effective width for proper sizing
800
1119
  paddingLeft: horizontalViewBoxPadding.left,
801
1120
  paddingRight: horizontalViewBoxPadding.right,
802
1121
  paddingStart: horizontalViewBoxPadding.left,
803
1122
  paddingEnd: horizontalViewBoxPadding.right,
804
- showLabels,
1123
+ showLabels: showLabels || compactX,
805
1124
  fixedAspectRatio: this.fixedAspectRatioScaling,
806
1125
  // Use chart's scaleReferenceSize property for proportional scaling
807
1126
  scaleReferenceSize: this.scaleReferenceSize,
@@ -813,6 +1132,10 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
813
1132
  if (this.xStepSize !== void 0) {
814
1133
  props.primaryTickmarkInterval = this.xStepSize;
815
1134
  }
1135
+ const band = bandFor(scale, compactX, compactXThickness);
1136
+ const mainLabels = mainLabelsFor(scale, compactX);
1137
+ if (mainLabels !== void 0) props.showMainTickmarkLabels = mainLabels;
1138
+ if (band !== void 0) props.labelThickness = band;
816
1139
  updates.push([this.bottomScaleSlot, scale, props]);
817
1140
  });
818
1141
  }
@@ -820,6 +1143,34 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
820
1143
  Object.assign(scale, props);
821
1144
  });
822
1145
  }
1146
+ /**
1147
+ * Whether the chart's axis range is what a slotted scale on this side should
1148
+ * describe. A subclass that slots a scale of its own and gives it a separate
1149
+ * range returns false for that side, otherwise the sync below overwrites it.
1150
+ */
1151
+ ownsSlottedScaleRange(_side) {
1152
+ return true;
1153
+ }
1154
+ /**
1155
+ * Push the current axis ranges to the slotted scales. The full
1156
+ * `updateScaleProperties()` cascade runs before the chart is (re)built and
1157
+ * so reads the previous chart's ranges; this closes that gap once the new
1158
+ * chart exists, and keeps auto-ranged scales in step with the data.
1159
+ */
1160
+ syncSlottedScaleRanges() {
1161
+ const apply = (side, slot, range) => {
1162
+ if (!slot || !range || !this.ownsSlottedScaleRange(side)) return;
1163
+ slot.assignedElements().forEach((scale) => {
1164
+ scale.minValue = range.min;
1165
+ scale.maxValue = range.max;
1166
+ });
1167
+ };
1168
+ apply("left", this.leftScaleSlot, this.resolveAxisRange("left"));
1169
+ apply("right", this.rightScaleSlot, this.resolveAxisRange("right"));
1170
+ const xRange = this.resolveSlottedXRange();
1171
+ apply("top", this.topScaleSlot, xRange);
1172
+ apply("bottom", this.bottomScaleSlot, xRange);
1173
+ }
823
1174
  hasAnyChanged(changed, props) {
824
1175
  return props.some((prop) => changed.has(prop));
825
1176
  }
@@ -893,7 +1244,10 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
893
1244
  if (this.hasAnyChanged(changed, LINE_GRAPH_DIMENSION_PROP_NAMES)) {
894
1245
  if (this.updateComputedDimensions()) return;
895
1246
  }
896
- if (changed.has("hasLabelPadding") && this.hasExternalScales()) {
1247
+ const belowThreshold = this.isBelowThreshold();
1248
+ const crossedThreshold = this.wasBelowThreshold !== void 0 && this.wasBelowThreshold !== belowThreshold;
1249
+ this.wasBelowThreshold = belowThreshold;
1250
+ if ((changed.has("hasLabelPadding") || changed.has("rangeLabels") || crossedThreshold) && this.hasExternalScales()) {
897
1251
  this.syncScalesAndChart();
898
1252
  return;
899
1253
  }
@@ -948,7 +1302,7 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
948
1302
  if (!this.canvasEl) return;
949
1303
  this.resizeObserver = observeLabelThreshold(
950
1304
  this.canvasEl,
951
- () => (this.canvasEl?.clientHeight ?? 0) >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS,
1305
+ () => (this.canvasEl?.clientHeight ?? 0) >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS && (this.canvasEl?.clientWidth ?? 0) >= RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS,
952
1306
  {
953
1307
  rebuild: () => {
954
1308
  this.chart?.destroy();
@@ -1069,6 +1423,7 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
1069
1423
  const result = {
1070
1424
  ...existingDataset || {},
1071
1425
  data: existingDataset?.data ?? values,
1426
+ yAxisID: existingDataset?.yAxisID ?? this.primaryYAxisId,
1072
1427
  borderColor,
1073
1428
  backgroundColor,
1074
1429
  borderWidth: existingDataset?.borderWidth ?? 2,
@@ -1115,6 +1470,7 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
1115
1470
  const baselineDataset = {
1116
1471
  label: "threshold-baseline",
1117
1472
  data: baselineData,
1473
+ yAxisID: this.primaryYAxisId,
1118
1474
  borderColor: "transparent",
1119
1475
  backgroundColor: "transparent",
1120
1476
  borderWidth: 0,
@@ -1200,6 +1556,36 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
1200
1556
  const datasets = fill && fillMode === "threshold" ? this.createThresholdDatasets(values, chartColors) : [this.buildDataset(values, 0, chartColors)];
1201
1557
  return { datasets, labels };
1202
1558
  }
1559
+ /**
1560
+ * One padding source for the Chart.js layout and the slotted scales: two
1561
+ * sources painted the canvas over a scale and inset a scale inside a chart.
1562
+ */
1563
+ computeChartPadding() {
1564
+ const isTooSmall = this.isBelowThreshold();
1565
+ const scaleFactor = this.getScaleFactor();
1566
+ const defaultPaddingScaled = !this.hasLabelPadding ? 0 : this.fixedAspectRatioScaling ? Math.round(CHART_DIMENSIONS.CANVAS_PADDING * scaleFactor) : CHART_DIMENSIONS.CANVAS_PADDING;
1567
+ const { y: yTexts, x: xTexts } = this.refreshRangeLabels();
1568
+ const context = this.canvasEl?.getContext("2d");
1569
+ const gutters = (yTexts.length || xTexts.length) && context ? measureRangeLabelGutters(
1570
+ context,
1571
+ this.rangeLabelFont(),
1572
+ yTexts,
1573
+ xTexts
1574
+ ) : { side: 0, bottom: 0 };
1575
+ const scalePadding = this.calculatePaddingFromScales();
1576
+ const freeSide = (side) => {
1577
+ if (!isTooSmall) return defaultPaddingScaled;
1578
+ if (side === "bottom") return gutters.bottom;
1579
+ return side === this.ySide ? gutters.side : 0;
1580
+ };
1581
+ const padding = {
1582
+ top: this.externalScaleDimensions.has("top") ? scalePadding.top : freeSide("top"),
1583
+ right: this.externalScaleDimensions.has("right") ? scalePadding.right : freeSide("right"),
1584
+ bottom: this.externalScaleDimensions.has("bottom") ? scalePadding.bottom : freeSide("bottom"),
1585
+ left: this.externalScaleDimensions.has("left") ? scalePadding.left : freeSide("left")
1586
+ };
1587
+ return padding;
1588
+ }
1203
1589
  /**
1204
1590
  * Get Chart.js options with dynamic sizing and padding
1205
1591
  */
@@ -1207,27 +1593,7 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
1207
1593
  const effectiveWidth = this.getEffectiveWidth();
1208
1594
  const effectiveHeight = this.getEffectiveHeight();
1209
1595
  const isTooSmall = effectiveWidth < RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS || effectiveHeight < RECTANGULAR_CHART_DIMENSIONS.MIN_HEIGHT_WITH_LABELS;
1210
- const scaleFactor = this.getScaleFactor();
1211
- let padding;
1212
- const defaultPaddingScaled = !this.hasLabelPadding ? 0 : this.fixedAspectRatioScaling ? Math.round(CHART_DIMENSIONS.CANVAS_PADDING * scaleFactor) : CHART_DIMENSIONS.CANVAS_PADDING;
1213
- if (isTooSmall && this.hasLabelPadding) {
1214
- padding = { top: 0, right: 0, bottom: 0, left: 0 };
1215
- } else if (this.hasExternalScales()) {
1216
- const scalePadding = this.calculatePaddingFromScales();
1217
- padding = {
1218
- top: this.externalScaleDimensions.has("top") ? scalePadding.top : defaultPaddingScaled,
1219
- right: this.externalScaleDimensions.has("right") ? scalePadding.right : defaultPaddingScaled,
1220
- bottom: this.externalScaleDimensions.has("bottom") ? scalePadding.bottom : defaultPaddingScaled,
1221
- left: this.externalScaleDimensions.has("left") ? scalePadding.left : defaultPaddingScaled
1222
- };
1223
- } else {
1224
- padding = {
1225
- top: defaultPaddingScaled,
1226
- right: defaultPaddingScaled,
1227
- bottom: defaultPaddingScaled,
1228
- left: defaultPaddingScaled
1229
- };
1230
- }
1596
+ const padding = this.computeChartPadding();
1231
1597
  if (this.fixedAspectRatioScaling) {
1232
1598
  this.style.setProperty("--chart-width", "100%");
1233
1599
  this.style.setProperty("--chart-height", `${effectiveHeight}px`);
@@ -1282,9 +1648,13 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
1282
1648
  /**
1283
1649
  * Compute reference timestamp for time axis formatting.
1284
1650
  * Returns earliest timestamp for 'date' mode, latest for 'minutes' mode.
1651
+ * A pinned x range supplies the reference instead, so the axis edge stays
1652
+ * the reference while the data has not reached it yet.
1285
1653
  */
1286
1654
  computeTimeReference() {
1287
1655
  if (this.xAxisType !== "time") return void 0;
1656
+ const pinned = this.timeDisplay === "minutes" ? this.xAxis?.max : this.xAxis?.min;
1657
+ if (pinned !== void 0 && Number.isFinite(pinned)) return pinned;
1288
1658
  const timestamps = [];
1289
1659
  if (this.datasets?.length) {
1290
1660
  this.datasets.forEach((ds) => {
@@ -1357,6 +1727,9 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
1357
1727
  // No extra margin
1358
1728
  bounds: "data",
1359
1729
  // Use data bounds for edge-to-edge rendering
1730
+ // A category axis has no numeric range to pin.
1731
+ min: this.isNumericXAxis ? this.xAxis?.min : void 0,
1732
+ max: this.isNumericXAxis ? this.xAxis?.max : void 0,
1360
1733
  grid: {
1361
1734
  display: this.showGrid && this.showGridX,
1362
1735
  color: gridColor,
@@ -1457,10 +1830,14 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
1457
1830
  type: "line",
1458
1831
  data: { labels, datasets },
1459
1832
  options: this.getChartOptions(),
1460
- plugins: this.borderRadiusPosition ? [this.createBorderPlugin()] : []
1833
+ plugins: [
1834
+ ...this.borderRadiusPosition ? [this.createBorderPlugin()] : [],
1835
+ ...this.rangeLabels !== "none" ? [this.createRangeLabelsPlugin()] : []
1836
+ ]
1461
1837
  });
1462
1838
  requestAnimationFrame(() => this.updateLegend());
1463
1839
  this.applyFillModes();
1840
+ this.syncSlottedScaleRanges();
1464
1841
  }
1465
1842
  updateChart() {
1466
1843
  if (!this.chart || !this.canvasEl || !this.canvasEl.isConnected) return;
@@ -1470,6 +1847,7 @@ const _ObcChartLineBase = class _ObcChartLineBase extends LitElement {
1470
1847
  Object.assign(this.chart.options, this.getChartOptions());
1471
1848
  this.applyFillModes();
1472
1849
  this.chart.update();
1850
+ this.syncSlottedScaleRanges();
1473
1851
  requestAnimationFrame(() => this.updateLegend());
1474
1852
  }
1475
1853
  /**
@@ -1595,6 +1973,9 @@ __decorateClass([
1595
1973
  __decorateClass([
1596
1974
  property({ type: Array, attribute: false })
1597
1975
  ], ObcChartLineBase.prototype, "yAxes");
1976
+ __decorateClass([
1977
+ property({ type: Object, attribute: false })
1978
+ ], ObcChartLineBase.prototype, "xAxis");
1598
1979
  __decorateClass([
1599
1980
  property({ type: Boolean })
1600
1981
  ], ObcChartLineBase.prototype, "showGrid");
@@ -1610,6 +1991,9 @@ __decorateClass([
1610
1991
  __decorateClass([
1611
1992
  property({ type: Boolean, attribute: false })
1612
1993
  ], ObcChartLineBase.prototype, "hasLabelPadding");
1994
+ __decorateClass([
1995
+ property({ type: String })
1996
+ ], ObcChartLineBase.prototype, "rangeLabels");
1613
1997
  __decorateClass([
1614
1998
  property({ type: Boolean })
1615
1999
  ], ObcChartLineBase.prototype, "showPoints");
@@ -1676,6 +2060,7 @@ __decorateClass([
1676
2060
  export {
1677
2061
  LineMode,
1678
2062
  ObcChartLineBase,
2063
+ RangeLabels,
1679
2064
  TimeDisplay,
1680
2065
  XAxisType,
1681
2066
  YAxisPosition