@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.
- package/bundle/openbridge-webcomponents.bundle.js +75 -15
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +835 -545
- package/dist/automation/analog-valve/analog-valve.d.ts.map +1 -1
- package/dist/automation/analog-valve/analog-valve.js +3 -0
- package/dist/automation/analog-valve/analog-valve.js.map +1 -1
- package/dist/automation/automation-button/abstract-automation-button-motorized.d.ts.map +1 -1
- package/dist/automation/automation-button/abstract-automation-button-motorized.js +3 -0
- package/dist/automation/automation-button/abstract-automation-button-motorized.js.map +1 -1
- package/dist/automation/automation-button/abstract-automation-button-squared.d.ts.map +1 -1
- package/dist/automation/automation-button/abstract-automation-button-squared.js +3 -0
- package/dist/automation/automation-button/abstract-automation-button-squared.js.map +1 -1
- package/dist/automation/automation-button/abstract-automation-button.d.ts +4 -1
- package/dist/automation/automation-button/abstract-automation-button.d.ts.map +1 -1
- package/dist/automation/automation-button/abstract-automation-button.js +8 -4
- package/dist/automation/automation-button/abstract-automation-button.js.map +1 -1
- package/dist/automation/digital-valve/digital-valve.d.ts.map +1 -1
- package/dist/automation/digital-valve/digital-valve.js +3 -0
- package/dist/automation/digital-valve/digital-valve.js.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.d.ts +10 -2
- package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.js +27 -8
- package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
- package/dist/charthelpers/chart-common.css.js +28 -3
- package/dist/charthelpers/chart-common.css.js.map +1 -1
- package/package.json +6 -6
- package/src/automation/analog-valve/analog-valve.ts +3 -0
- package/src/automation/automation-button/abstract-automation-button-motorized.ts +3 -0
- package/src/automation/automation-button/abstract-automation-button-squared.ts +3 -0
- package/src/automation/automation-button/abstract-automation-button.ts +6 -1
- package/src/automation/digital-valve/digital-valve.ts +4 -0
- package/src/automation/motor/motor.stories.ts +13 -0
- package/src/building-blocks/chart-line/chart-line-base.ts +52 -10
- package/src/building-blocks/chart-line/chart-sizing-battleground.stories.ts +599 -0
- 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
|
|
39
|
-
|
|
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
|
-
|
|
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
|
}
|