@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.126 → 2.0.0-next.128

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 (35) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +75 -15
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +835 -545
  4. package/dist/automation/analog-valve/analog-valve.d.ts.map +1 -1
  5. package/dist/automation/analog-valve/analog-valve.js +3 -0
  6. package/dist/automation/analog-valve/analog-valve.js.map +1 -1
  7. package/dist/automation/automation-button/abstract-automation-button-motorized.d.ts.map +1 -1
  8. package/dist/automation/automation-button/abstract-automation-button-motorized.js +3 -0
  9. package/dist/automation/automation-button/abstract-automation-button-motorized.js.map +1 -1
  10. package/dist/automation/automation-button/abstract-automation-button-squared.d.ts.map +1 -1
  11. package/dist/automation/automation-button/abstract-automation-button-squared.js +3 -0
  12. package/dist/automation/automation-button/abstract-automation-button-squared.js.map +1 -1
  13. package/dist/automation/automation-button/abstract-automation-button.d.ts +4 -1
  14. package/dist/automation/automation-button/abstract-automation-button.d.ts.map +1 -1
  15. package/dist/automation/automation-button/abstract-automation-button.js +8 -4
  16. package/dist/automation/automation-button/abstract-automation-button.js.map +1 -1
  17. package/dist/automation/digital-valve/digital-valve.d.ts.map +1 -1
  18. package/dist/automation/digital-valve/digital-valve.js +3 -0
  19. package/dist/automation/digital-valve/digital-valve.js.map +1 -1
  20. package/dist/building-blocks/chart-line/chart-line-base.d.ts +10 -2
  21. package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
  22. package/dist/building-blocks/chart-line/chart-line-base.js +27 -8
  23. package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
  24. package/dist/charthelpers/chart-common.css.js +28 -3
  25. package/dist/charthelpers/chart-common.css.js.map +1 -1
  26. package/package.json +6 -6
  27. package/src/automation/analog-valve/analog-valve.ts +3 -0
  28. package/src/automation/automation-button/abstract-automation-button-motorized.ts +3 -0
  29. package/src/automation/automation-button/abstract-automation-button-squared.ts +3 -0
  30. package/src/automation/automation-button/abstract-automation-button.ts +6 -1
  31. package/src/automation/digital-valve/digital-valve.ts +4 -0
  32. package/src/automation/motor/motor.stories.ts +13 -0
  33. package/src/building-blocks/chart-line/chart-line-base.ts +52 -10
  34. package/src/building-blocks/chart-line/chart-sizing-battleground.stories.ts +599 -0
  35. package/src/charthelpers/chart-common.css +27 -3
@@ -0,0 +1,599 @@
1
+ import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
+ import {html, type TemplateResult} from 'lit';
3
+ import {ref} from 'lit/directives/ref.js';
4
+ import '../../bars-graphs/line-graph/line-graph.js';
5
+ import '../../bars-graphs/area-graph/area-graph.js';
6
+ import '../../navigation-instruments/gauge-trend/gauge-trend.js';
7
+ import '../../navigation-instruments/gauge-vertical/gauge-vertical.js';
8
+ import '../../navigation-instruments/gauge-horizontal/gauge-horizontal.js';
9
+ import '../bar-vertical/bar-vertical.js';
10
+ import '../bar-horizontal/bar-horizontal.js';
11
+ import '../../automation/automation-tank/automation-tank.js';
12
+ import {AdviceType} from '../../navigation-instruments/watch/advice.js';
13
+
14
+ const DATA = Array.from({length: 12}, (_, i) => ({
15
+ label: String(i),
16
+ value: 40 + 30 * Math.sin(i / 2),
17
+ }));
18
+
19
+ /**
20
+ * How a subject is expected to size itself.
21
+ *
22
+ * - `fills`: told the cell's measured size as its aspect-ratio reference (the
23
+ * integration `obc-automation-tank` uses), so its painted surface must cover
24
+ * the cell exactly.
25
+ * - `intrinsic`: owns its size by design — the fixed-config gauges, and any
26
+ * chart in pixel mode. Only checked for layout stability.
27
+ */
28
+ enum SizingContract {
29
+ /** Must cover the box it was given. */
30
+ fills = 'fills',
31
+ /** Owns its size; only checked for stability. */
32
+ intrinsic = 'intrinsic',
33
+ }
34
+
35
+ interface Subject {
36
+ label: string;
37
+ contract: SizingContract;
38
+ /**
39
+ * Whether the harness must hand this subject the cell's measured size as its
40
+ * aspect-ratio reference. The tanks measure their own chart cell internally,
41
+ * so they are driven by the cell alone.
42
+ */
43
+ feedsSize?: boolean;
44
+ render: () => TemplateResult;
45
+ }
46
+
47
+ interface Shape {
48
+ label: string;
49
+ outer: string;
50
+ inner: string;
51
+ /** Cells this small are below the components' min-content size. */
52
+ degenerate?: boolean;
53
+ }
54
+
55
+ const FILL_STYLE = 'position:absolute;inset:0';
56
+
57
+ const SHAPES: Shape[] = [
58
+ {label: '1.5:1', outer: '', inner: 'width:300px;height:200px'},
59
+ {label: '1:1', outer: '', inner: 'width:240px;height:240px'},
60
+ {label: '3:1', outer: '', inner: 'width:480px;height:160px'},
61
+ {label: '6:1', outer: '', inner: 'width:600px;height:100px'},
62
+ {label: '1:2', outer: '', inner: 'width:160px;height:320px'},
63
+ {
64
+ label: 'flex min/max + centred',
65
+ outer: 'display:flex;width:420px;height:220px;align-items:center',
66
+ inner: 'flex:1;min-height:150px;max-height:190px;align-self:center',
67
+ },
68
+ {
69
+ label: 'grid track',
70
+ outer:
71
+ 'display:grid;grid-template-columns:1fr 1fr;width:480px;height:200px',
72
+ inner: 'min-width:0;min-height:0',
73
+ },
74
+ {
75
+ label: 'aspect-ratio only',
76
+ outer: '',
77
+ inner: 'width:300px;aspect-ratio:2/1',
78
+ },
79
+ {
80
+ label: 'tiny',
81
+ outer: '',
82
+ inner: 'width:120px;height:90px',
83
+ degenerate: true,
84
+ },
85
+ ];
86
+
87
+ const SUBJECTS: Subject[] = [
88
+ {
89
+ label: 'gauge-trend',
90
+ contract: SizingContract.fills,
91
+ feedsSize: true,
92
+ render: () =>
93
+ html`<obc-gauge-trend
94
+ style=${FILL_STYLE}
95
+ .data=${DATA}
96
+ .minValue=${0}
97
+ .maxValue=${100}
98
+ .value=${55}
99
+ .chartFill=${true}
100
+ .hasLabelPadding=${false}
101
+ ></obc-gauge-trend>`,
102
+ },
103
+ {
104
+ label: 'gauge-trend + bar + scale',
105
+ contract: SizingContract.fills,
106
+ feedsSize: true,
107
+ render: () =>
108
+ html`<obc-gauge-trend
109
+ style=${FILL_STYLE}
110
+ .data=${DATA}
111
+ .minValue=${0}
112
+ .maxValue=${100}
113
+ .value=${55}
114
+ .chartFill=${true}
115
+ .hasBar=${true}
116
+ .hasScale=${true}
117
+ .primaryTickmarkInterval=${25}
118
+ ></obc-gauge-trend>`,
119
+ },
120
+ {
121
+ label: 'gauge-trend + advice + setpoint',
122
+ contract: SizingContract.fills,
123
+ feedsSize: true,
124
+ render: () =>
125
+ html`<obc-gauge-trend
126
+ style=${FILL_STYLE}
127
+ .data=${DATA}
128
+ .minValue=${0}
129
+ .maxValue=${100}
130
+ .value=${55}
131
+ .chartFill=${true}
132
+ .hasBar=${true}
133
+ .hasAdvice=${true}
134
+ .advice=${[
135
+ {min: 75, max: 100, type: AdviceType.caution, hinted: false},
136
+ ]}
137
+ .setpoint=${70}
138
+ .hasLabelPadding=${false}
139
+ ></obc-gauge-trend>`,
140
+ },
141
+ {
142
+ label: 'line-graph (aspect)',
143
+ contract: SizingContract.fills,
144
+ feedsSize: true,
145
+ render: () =>
146
+ html`<obc-line-graph
147
+ style=${FILL_STYLE}
148
+ .data=${DATA}
149
+ .fixedAspectRatioScaling=${true}
150
+ .hasLabelPadding=${false}
151
+ ></obc-line-graph>`,
152
+ },
153
+ {
154
+ label: 'area-graph (aspect)',
155
+ contract: SizingContract.fills,
156
+ feedsSize: true,
157
+ render: () =>
158
+ html`<obc-area-graph
159
+ style=${FILL_STYLE}
160
+ .data=${DATA}
161
+ .fixedAspectRatioScaling=${true}
162
+ .hasLabelPadding=${false}
163
+ ></obc-area-graph>`,
164
+ },
165
+ {
166
+ label: 'tank graph-and-bar',
167
+ contract: SizingContract.fills,
168
+ render: () =>
169
+ html`<obc-automation-tank
170
+ style=${FILL_STYLE}
171
+ chart-mode="graph-and-bar"
172
+ .chartData=${DATA}
173
+ .value=${62}
174
+ .max=${100}
175
+ tag="TK"
176
+ ></obc-automation-tank>`,
177
+ },
178
+ {
179
+ label: 'tank graph horizontal',
180
+ contract: SizingContract.fills,
181
+ render: () =>
182
+ html`<obc-automation-tank
183
+ style=${FILL_STYLE}
184
+ chart-mode="graph"
185
+ orientation="horizontal"
186
+ .chartData=${DATA}
187
+ .value=${62}
188
+ .max=${100}
189
+ tag="TK"
190
+ ></obc-automation-tank>`,
191
+ },
192
+ {
193
+ label: 'tank bar mode',
194
+ contract: SizingContract.fills,
195
+ render: () =>
196
+ html`<obc-automation-tank
197
+ style=${FILL_STYLE}
198
+ chart-mode="bar"
199
+ .value=${62}
200
+ .max=${100}
201
+ tag="TK"
202
+ ></obc-automation-tank>`,
203
+ },
204
+ {
205
+ label: 'tank compact',
206
+ contract: SizingContract.fills,
207
+ render: () =>
208
+ html`<obc-automation-tank
209
+ style=${FILL_STYLE}
210
+ chart-mode="graph-and-bar"
211
+ compact
212
+ .chartData=${DATA}
213
+ .value=${62}
214
+ .max=${100}
215
+ tag="TK"
216
+ ></obc-automation-tank>`,
217
+ },
218
+ {
219
+ label: 'line-graph (pixel mode)',
220
+ contract: SizingContract.intrinsic,
221
+ render: () =>
222
+ html`<obc-line-graph
223
+ .data=${DATA}
224
+ .width=${240}
225
+ .height=${160}
226
+ ></obc-line-graph>`,
227
+ },
228
+ {
229
+ label: 'bar-vertical',
230
+ contract: SizingContract.intrinsic,
231
+ render: () =>
232
+ html`<obc-bar-vertical
233
+ .minValue=${0}
234
+ .maxValue=${100}
235
+ .value=${55}
236
+ .hasBar=${true}
237
+ .hasScale=${true}
238
+ .primaryTickmarkInterval=${25}
239
+ .fixedAspectRatio=${true}
240
+ ></obc-bar-vertical>`,
241
+ },
242
+ {
243
+ label: 'gauge-vertical',
244
+ contract: SizingContract.intrinsic,
245
+ render: () =>
246
+ html`<obc-gauge-vertical
247
+ .minValue=${0}
248
+ .maxValue=${100}
249
+ .value=${55}
250
+ .primaryTickmarkInterval=${25}
251
+ ></obc-gauge-vertical>`,
252
+ },
253
+ ];
254
+
255
+ /**
256
+ * The painted surface to compare against the cell. For the tank that is the
257
+ * canvas (or bar SVG) inside its chart cell, so the tank's own frame, readout
258
+ * and tag are excluded; for everything else it is the component's own canvas
259
+ * or SVG measured against the component box.
260
+ */
261
+ function paintedSurface(
262
+ el: Element
263
+ ): {box: Element; paint: Element} | undefined {
264
+ const shadow = (el as HTMLElement).shadowRoot;
265
+ if (el.tagName === 'OBC-AUTOMATION-TANK') {
266
+ const cell = shadow?.querySelector('.bar-container');
267
+ const paint =
268
+ shadow
269
+ ?.querySelector('obc-gauge-trend')
270
+ ?.shadowRoot?.querySelector('canvas') ??
271
+ shadow?.querySelector('obc-bar-vertical svg, obc-bar-horizontal svg');
272
+ return cell && paint ? {box: cell, paint} : undefined;
273
+ }
274
+ const paint =
275
+ shadow?.querySelector('canvas') ??
276
+ shadow?.querySelector('svg') ??
277
+ el.querySelector('svg');
278
+ return paint ? {box: el, paint} : undefined;
279
+ }
280
+
281
+ /**
282
+ * The box the host reserved for a chart, when the subject is a host rather
283
+ * than a chart itself. Used to tell "no room was given" apart from "room was
284
+ * given and nothing rendered into it".
285
+ */
286
+ function hostChartBox(el: Element): DOMRect | undefined {
287
+ if (el.tagName !== 'OBC-AUTOMATION-TANK') return undefined;
288
+ const cell = (el as HTMLElement).shadowRoot?.querySelector('.bar-container');
289
+ return cell?.getBoundingClientRect();
290
+ }
291
+
292
+ const sizeHistory = new WeakMap<Element, Set<string>>();
293
+ const sizeFed = new WeakSet<Element>();
294
+
295
+ function trackStability(el: Element) {
296
+ if (sizeHistory.has(el)) return;
297
+ const seen = new Set<string>();
298
+ sizeHistory.set(el, seen);
299
+ new ResizeObserver((entries) => {
300
+ const entry = entries[0];
301
+ if (!entry) return;
302
+ seen.add(
303
+ `${Math.round(entry.contentRect.width)}x${Math.round(entry.contentRect.height)}`
304
+ );
305
+ }).observe(el);
306
+ }
307
+
308
+ /**
309
+ * Feed the measured cell size back to the subject as its aspect-ratio
310
+ * reference. This is what `obc-automation-tank` does internally for its own
311
+ * chart cell, and it is the integration the `fills` contract is defined
312
+ * against — a bare chart is never expected to guess its container's shape.
313
+ */
314
+ function feedCellSize(cell: HTMLElement, subject: Element) {
315
+ if (sizeFed.has(subject)) return;
316
+ sizeFed.add(subject);
317
+ const target = subject as unknown as {width: number; height: number};
318
+ new ResizeObserver((entries) => {
319
+ const entry = entries[0];
320
+ if (!entry) return;
321
+ const w = Math.round(entry.contentRect.width);
322
+ const h = Math.round(entry.contentRect.height);
323
+ if (w > 0 && h > 0) {
324
+ target.width = w;
325
+ target.height = h;
326
+ }
327
+ }).observe(cell);
328
+ }
329
+
330
+ /**
331
+ * Attach live measurement to one rendered battleground and repaint verdicts.
332
+ * Each battleground gets its own callback so that unmounting one does not tear
333
+ * down the probes of its siblings.
334
+ */
335
+ function createMeasure(): (root?: Element) => void {
336
+ let probe: number | undefined;
337
+ return (root?: Element) => {
338
+ // Cleared unconditionally, not only on unmount: a re-invocation with a
339
+ // mounted root (a rerender, or HMR) would otherwise leave the previous
340
+ // interval running forever alongside the new one.
341
+ if (probe !== undefined) clearInterval(probe);
342
+ probe = undefined;
343
+ if (!(root instanceof HTMLElement)) return;
344
+
345
+ const paint = () => {
346
+ let pass = 0;
347
+ let total = 0;
348
+ root.querySelectorAll<HTMLElement>('[data-cell]').forEach((cell) => {
349
+ const subject = cell.firstElementChild;
350
+ const readout = cell
351
+ .closest<HTMLElement>('[data-case]')
352
+ ?.querySelector<HTMLElement>('[data-readout]');
353
+ if (!subject || !readout) return;
354
+
355
+ // Wired from here rather than once up front: the `ref` callback runs
356
+ // before the template's child parts commit, so at that point the cells
357
+ // do not exist yet. Both helpers are idempotent.
358
+ trackStability(subject);
359
+ if (cell.dataset.feed === 'true') feedCellSize(cell, subject);
360
+
361
+ const contract = cell.dataset.contract as SizingContract;
362
+ const distinct = sizeHistory.get(subject)?.size ?? 0;
363
+ const surface = paintedSurface(subject);
364
+ const hostBox = hostChartBox(subject);
365
+
366
+ // A host that gave its chart no room at all (a tank squeezed below the
367
+ // height its badges, readout and tag need) cannot be held to the fill
368
+ // contract. Reported rather than hidden, and kept distinct from "the
369
+ // box has room but nothing rendered into it", which is a real failure.
370
+ if (hostBox && hostBox.width * hostBox.height < 1) {
371
+ readout.textContent = 'host left no room for a chart (n/a)';
372
+ readout.style.color = 'var(--element-neutral-color)';
373
+ return;
374
+ }
375
+
376
+ if (!surface) {
377
+ total += 1;
378
+ readout.textContent = 'box has room but nothing rendered FAIL';
379
+ readout.style.color = 'var(--alert-alarm-color)';
380
+ return;
381
+ }
382
+
383
+ const box = surface.box.getBoundingClientRect();
384
+ const px = surface.paint.getBoundingClientRect();
385
+ const fill =
386
+ box.width * box.height > 0
387
+ ? (px.width * px.height) / (box.width * box.height)
388
+ : 0;
389
+ const overflow = Math.max(px.width - box.width, px.height - box.height);
390
+ const stable = distinct <= 1;
391
+ const enforced = contract === SizingContract.fills;
392
+ const ok = stable && (!enforced || (fill >= 0.99 && overflow <= 1));
393
+
394
+ total += 1;
395
+ if (ok) pass += 1;
396
+
397
+ readout.textContent =
398
+ `cell ${Math.round(box.width)}×${Math.round(box.height)}` +
399
+ ` paint ${Math.round(px.width)}×${Math.round(px.height)}` +
400
+ ` fills ${(fill * 100).toFixed(0)}%` +
401
+ ` sizes ${distinct}` +
402
+ (enforced ? (ok ? ' PASS' : ' FAIL') : ' (intrinsic)');
403
+ readout.style.color = ok
404
+ ? 'var(--element-neutral-color)'
405
+ : 'var(--alert-alarm-color)';
406
+ });
407
+
408
+ const summary = root.querySelector<HTMLElement>('[data-summary]');
409
+ if (summary) {
410
+ summary.textContent = `${pass} / ${total} cells pass`;
411
+ summary.style.color =
412
+ pass === total
413
+ ? 'var(--element-neutral-color)'
414
+ : 'var(--alert-alarm-color)';
415
+ }
416
+ };
417
+
418
+ paint();
419
+ probe = window.setInterval(paint, 500);
420
+ };
421
+ }
422
+
423
+ const MONO =
424
+ 'font:11px/1.5 monospace;color:var(--element-neutral-color);white-space:pre';
425
+
426
+ function cellFor(shape: Shape, subject: Subject): TemplateResult {
427
+ const contract = shape.degenerate
428
+ ? SizingContract.intrinsic
429
+ : subject.contract;
430
+ return html`
431
+ <div data-case style="margin:0 12px 12px 0">
432
+ <div style=${MONO}>${shape.label} · ${subject.label}</div>
433
+ <div data-readout style=${MONO}>measuring…</div>
434
+ <div style=${shape.outer}>
435
+ <div
436
+ data-cell
437
+ data-feed=${String(!!subject.feedsSize)}
438
+ data-contract=${contract}
439
+ style="position:relative;overflow:hidden;outline:1px dashed var(--border-divider-color);${shape.inner}"
440
+ >
441
+ ${subject.render()}
442
+ </div>
443
+ ${shape.outer.includes('grid') ? html`<div></div>` : null}
444
+ </div>
445
+ </div>
446
+ `;
447
+ }
448
+
449
+ function battleground(
450
+ shapes: Shape[],
451
+ subjects: Subject[],
452
+ zoom = 1
453
+ ): TemplateResult {
454
+ return html`
455
+ <div ${ref(createMeasure())}>
456
+ <div
457
+ data-summary
458
+ style="font:14px monospace;padding:8px 0;position:sticky;top:0;background:var(--container-background-color);z-index:2"
459
+ >
460
+ measuring…
461
+ </div>
462
+ <div style=${zoom === 1 ? '' : `zoom:${zoom}`}>
463
+ <div style="display:flex;flex-wrap:wrap;align-items:flex-start">
464
+ ${subjects.flatMap((s) => shapes.map((shape) => cellFor(shape, s)))}
465
+ </div>
466
+ </div>
467
+ </div>
468
+ `;
469
+ }
470
+
471
+ const meta: Meta = {
472
+ title: 'Building Blocks/Chart Sizing Battleground',
473
+ tags: ['skip-test'],
474
+ parameters: {
475
+ controls: {disable: true},
476
+ docs: {
477
+ description: {
478
+ component:
479
+ 'A live matrix for the chart sizing contract. Every cell measures ' +
480
+ 'its own painted surface against the box it was given and prints a ' +
481
+ 'verdict, so a sizing regression shows up as a red FAIL rather than ' +
482
+ 'as an impression. Subjects marked `(intrinsic)` own their size by ' +
483
+ 'design and are only checked for layout stability — `sizes` counts ' +
484
+ 'the distinct rendered sizes a `ResizeObserver` has seen, so ' +
485
+ 'anything above 1 after settling means the layout is ringing.',
486
+ },
487
+ },
488
+ },
489
+ };
490
+
491
+ export default meta;
492
+ type Story = StoryObj;
493
+
494
+ /**
495
+ * Every subject in every container shape. `fills` subjects are fed the cell's
496
+ * measured size as their aspect-ratio reference — the integration
497
+ * `obc-automation-tank` uses — so their painted surface must cover the cell.
498
+ *
499
+ * The wide shapes (`3:1`, `6:1`) are the ones that regressed in issue #1138:
500
+ * the canvas could not exceed twice its own height, so the remainder showed up
501
+ * as symmetric empty margins.
502
+ *
503
+ * One cell is expected to be red: `1:2 · tank graph horizontal`. A horizontal
504
+ * tank in a portrait box gets a 30px-wide chart cell, below the width its own
505
+ * layout needs, so the chart overflows it. That is a tank layout limit rather
506
+ * than a chart sizing one — it predates #1138 and is unchanged by its fix.
507
+ */
508
+ export const ContainerShapes: Story = {
509
+ render: () => battleground(SHAPES, SUBJECTS),
510
+ };
511
+
512
+ /**
513
+ * A representative subject set repeated under CSS `zoom`. A global
514
+ * `html { zoom: … }` is how a fixed layout is fitted to a display — the
515
+ * `vue-demo` exposes exactly that as a user setting — so every level here is a
516
+ * shape real deployments run in.
517
+ *
518
+ * Both halves of the double-application are covered: below 1 the canvas used
519
+ * to shrink to `zoom²` of its cell, above 1 it used to overflow by the same
520
+ * factor.
521
+ */
522
+ export const ZoomLevels: Story = {
523
+ render: () => {
524
+ const subjects = SUBJECTS.filter((s) =>
525
+ [
526
+ 'gauge-trend',
527
+ 'gauge-trend + bar + scale',
528
+ 'tank graph-and-bar',
529
+ 'area-graph (aspect)',
530
+ ].includes(s.label)
531
+ );
532
+ const shapes = SHAPES.filter((s) => ['1.5:1', '3:1'].includes(s.label));
533
+ return html`${[0.5, 0.8, 1, 1.25, 1.5, 2].map(
534
+ (zoom) => html`
535
+ <h3
536
+ style="font:600 13px monospace;color:var(--element-active-color);margin:16px 0 4px"
537
+ >
538
+ zoom ${zoom}
539
+ </h3>
540
+ ${battleground(shapes, subjects, zoom)}
541
+ `
542
+ )}`;
543
+ },
544
+ };
545
+
546
+ /**
547
+ * Drives each cell through a sweep of sizes and keeps counting distinct
548
+ * rendered sizes. A settled layout converges to one size per step; a circular
549
+ * constraint between a component and the box that measures it shows up here as
550
+ * a `sizes` count that keeps climbing (issue #1121).
551
+ */
552
+ export const ResizeStress: Story = {
553
+ render: () => {
554
+ const subjects = SUBJECTS.filter((s) =>
555
+ [
556
+ 'gauge-trend + bar + scale',
557
+ 'tank graph-and-bar',
558
+ 'tank graph horizontal',
559
+ 'line-graph (aspect)',
560
+ ].includes(s.label)
561
+ );
562
+ const shape: Shape = {
563
+ label: 'Animated',
564
+ outer: '',
565
+ inner: 'width:300px;height:200px',
566
+ };
567
+ const createdMeasure = createMeasure();
568
+ let timer: number | undefined;
569
+ const drive = (root?: Element) => {
570
+ createdMeasure(root);
571
+ // Same reasoning as createMeasure(): clear before starting, so a
572
+ // rerender cannot leave two sweeps fighting over the same cells.
573
+ if (timer !== undefined) clearInterval(timer);
574
+ timer = undefined;
575
+ if (!(root instanceof HTMLElement)) return;
576
+ const steps = [
577
+ [300, 200],
578
+ [300, 340],
579
+ [520, 340],
580
+ [520, 140],
581
+ [180, 140],
582
+ ];
583
+ let i = 0;
584
+ timer = window.setInterval(() => {
585
+ const [w, h] = steps[i++ % steps.length];
586
+ root.querySelectorAll<HTMLElement>('[data-cell]').forEach((cell) => {
587
+ cell.style.width = `${w}px`;
588
+ cell.style.height = `${h}px`;
589
+ // The size count is per step: the question is whether the layout
590
+ // settles on ONE size for the box it was just given, not how many
591
+ // boxes the sweep has been through.
592
+ const subject = cell.firstElementChild;
593
+ if (subject) sizeHistory.get(subject)?.clear();
594
+ });
595
+ }, 1200);
596
+ };
597
+ return html`<div ${ref(drive)}>${battleground([shape], subjects)}</div>`;
598
+ },
599
+ };
@@ -35,8 +35,32 @@ canvas {
35
35
  display: block;
36
36
  }
37
37
 
38
- /* Fixed aspect ratio scaling mode: canvas fills wrapper */
39
- :host([fixedaspectratioscaling]) canvas {
38
+ /* Fixed aspect ratio scaling mode: the canvas container must be a definite
39
+ box, not shrink-to-fit. The wrapper is a column flex with align-items:
40
+ center, so the container would otherwise size to the canvas' max-content
41
+ width — and a canvas with an auto width and a definite height resolves
42
+ against its intrinsic 2:1 ratio, capping the chart at height * 2. */
43
+ :host([fixedaspectratioscaling]) .canvas-and-slots-container {
40
44
  width: 100%;
41
- height: var(--chart-height, auto);
45
+ }
46
+
47
+ /* Fixed aspect ratio scaling mode: canvas fills wrapper.
48
+
49
+ `!important` is load-bearing, not a shortcut. Chart.js measures the
50
+ container with getBoundingClientRect() — which is scaled by any ancestor
51
+ CSS `zoom` — and writes that number straight back as an inline
52
+ `style="width/height: Npx"`. The browser then scales that CSS-pixel value
53
+ by the same zoom a second time, so the canvas lands at zoom² of its cell
54
+ (64% at zoom 0.8, 156% at zoom 1.25). Overriding the inline style pins the
55
+ canvas to the box the layout actually gave it at every zoom level; Chart.js
56
+ keeps using its own measurement for the drawing buffer, which only affects
57
+ raster resolution, not geometry.
58
+
59
+ No auto margins here either: Chart.js subtracts the canvas' resolved
60
+ margins from the container width, so centring it with `margin: 0 auto`
61
+ makes the leftover space permanently unavailable to it. */
62
+ :host([fixedaspectratioscaling]) canvas {
63
+ width: 100% !important;
64
+ height: var(--chart-height, auto) !important;
65
+ margin: 0;
42
66
  }