multi-gauge 0.1.0

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.
@@ -0,0 +1,446 @@
1
+ import { localRect } from '../layout/PanelLayout.js';
2
+
3
+ const HYSTERESIS = 0.62;
4
+ const EDGE_HIT_SIZE = 18;
5
+
6
+ function setRect(element, rect) {
7
+ element.style.left = `${rect.x}px`;
8
+ element.style.top = `${rect.y}px`;
9
+ element.style.width = `${rect.width}px`;
10
+ element.style.height = `${rect.height}px`;
11
+ }
12
+
13
+ /** Transactional DOM interaction layer. GridLayout remains the source of truth. */
14
+ export class GridEditor {
15
+ #canvas;
16
+ #overlay;
17
+ #placeholder;
18
+ #layout;
19
+ #actions;
20
+ #nodes = new Map();
21
+ #rectangles = new Map();
22
+ #drag;
23
+ #frame;
24
+ #parentPosition;
25
+ #panelLayout;
26
+
27
+ constructor(canvas, layout, actions) {
28
+ this.#canvas = canvas;
29
+ this.#layout = layout;
30
+ this.#actions = actions;
31
+ const parent = canvas.parentElement;
32
+ if (!parent) {
33
+ throw new Error('Grid editing requires the canvas to have a parent element.');
34
+ }
35
+ this.#parentPosition = parent.style.position;
36
+ if (getComputedStyle(parent).position === 'static') {
37
+ parent.style.position = 'relative';
38
+ }
39
+ this.#overlay = document.createElement('div');
40
+ this.#overlay.dataset.multigaugeEditor = '';
41
+ Object.assign(this.#overlay.style, {
42
+ position: 'absolute',
43
+ zIndex: '10',
44
+ overflow: 'hidden',
45
+ touchAction: 'none'
46
+ });
47
+ this.#placeholder = document.createElement('div');
48
+ this.#placeholder.dataset.gridPlaceholder = '';
49
+ Object.assign(this.#placeholder.style, {
50
+ position: 'absolute',
51
+ display: 'none',
52
+ boxSizing: 'border-box',
53
+ border: '1px solid #00eaff',
54
+ borderRadius: '7px',
55
+ background: 'rgba(0, 234, 255, 0.10)',
56
+ pointerEvents: 'none',
57
+ transition: 'transform 90ms ease, width 90ms ease, height 90ms ease'
58
+ });
59
+ this.#overlay.append(this.#placeholder);
60
+ parent.append(this.#overlay);
61
+ this.#overlay.addEventListener('pointerdown', this.#onPointerDown);
62
+ this.#overlay.addEventListener('pointermove', this.#onPointerMove);
63
+ this.#overlay.addEventListener('pointerup', this.#onPointerUp);
64
+ this.#overlay.addEventListener('pointercancel', this.#onPointerCancel);
65
+ window.addEventListener('keydown', this.#onKeyDown);
66
+ }
67
+
68
+ /** Synchronize committed geometry without rebuilding existing DOM nodes. */
69
+ refresh(rectangles, panelLayout) {
70
+ this.#panelLayout = panelLayout;
71
+ const gridRect = panelLayout.gridRect;
72
+ this.#rectangles = new Map([...rectangles]
73
+ .map(([id, rect]) => [id, localRect(rect, gridRect)]));
74
+ const style = getComputedStyle(this.#canvas);
75
+ const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
76
+ const paddingTop = Number.parseFloat(style.paddingTop) || 0;
77
+ Object.assign(this.#overlay.style, {
78
+ left: `${this.#canvas.offsetLeft + this.#canvas.clientLeft + paddingLeft + gridRect.x}px`,
79
+ top: `${this.#canvas.offsetTop + this.#canvas.clientTop + paddingTop + gridRect.y}px`,
80
+ width: `${gridRect.width}px`,
81
+ height: `${gridRect.height}px`
82
+ });
83
+ for (const [id, node] of this.#nodes) {
84
+ if (!rectangles.has(id)) {
85
+ node.remove();
86
+ this.#nodes.delete(id);
87
+ }
88
+ }
89
+ for (const [id, rect] of this.#rectangles) {
90
+ let item = this.#nodes.get(id);
91
+ if (!item) {
92
+ item = this.#createItem(id);
93
+ this.#nodes.set(id, item);
94
+ this.#overlay.append(item);
95
+ }
96
+ setRect(item, rect);
97
+ if (!this.#drag) {
98
+ item.style.transform = 'translate3d(0, 0, 0)';
99
+ }
100
+ item.style.borderColor = this.#drag?.id === id ? '#00eaff' : 'transparent';
101
+ }
102
+ }
103
+
104
+ destroy() {
105
+ const parent = this.#canvas.parentElement;
106
+ this.#cancelDrag();
107
+ this.#overlay.removeEventListener('pointerdown', this.#onPointerDown);
108
+ this.#overlay.removeEventListener('pointermove', this.#onPointerMove);
109
+ this.#overlay.removeEventListener('pointerup', this.#onPointerUp);
110
+ this.#overlay.removeEventListener('pointercancel', this.#onPointerCancel);
111
+ window.removeEventListener('keydown', this.#onKeyDown);
112
+ this.#overlay.remove();
113
+ if (parent) {
114
+ parent.style.position = this.#parentPosition;
115
+ }
116
+ }
117
+
118
+ #createItem(id) {
119
+ const item = document.createElement('div');
120
+ item.dataset.gaugeId = id;
121
+ Object.assign(item.style, {
122
+ position: 'absolute',
123
+ boxSizing: 'border-box',
124
+ border: '1px solid transparent',
125
+ borderRadius: '7px',
126
+ cursor: 'grab',
127
+ background: 'transparent',
128
+ willChange: 'transform',
129
+ contain: 'layout style paint'
130
+ });
131
+ return item;
132
+ }
133
+
134
+ #onPointerDown = (event) => {
135
+ if (this.#drag || event.button !== 0) {
136
+ return;
137
+ }
138
+ const item = event.target.closest('[data-gauge-id]');
139
+ if (!item) {
140
+ return;
141
+ }
142
+ const id = item.dataset.gaugeId;
143
+ const resizeEdges = this.#resizeEdges(item, event);
144
+ const resize = Boolean(resizeEdges);
145
+ const initial = this.#layout.get(id);
146
+ const rect = this.#rectangles.get(id);
147
+ if (!initial || !rect) {
148
+ return;
149
+ }
150
+ const point = this.#pointerLocal(event);
151
+ this.#drag = {
152
+ id,
153
+ item,
154
+ pointerId: event.pointerId,
155
+ startX: point.x,
156
+ startY: point.y,
157
+ x: point.x,
158
+ y: point.y,
159
+ dx: 0,
160
+ dy: 0,
161
+ initial,
162
+ initialRect: { ...rect },
163
+ gridRow: initial.row,
164
+ gridCol: initial.col,
165
+ rowSpan: initial.rowSpan,
166
+ colSpan: initial.colSpan,
167
+ resize,
168
+ resizeX: resizeEdges?.x ?? 0,
169
+ resizeY: resizeEdges?.y ?? 0,
170
+ valid: true,
171
+ previewed: false,
172
+ session: this.#layout.beginDrag(id, resize ? 'resize' : 'move')
173
+ };
174
+ item.setPointerCapture(event.pointerId);
175
+ item.style.cursor = resize ? this.#resizeCursor(resizeEdges) : 'grabbing';
176
+ item.style.zIndex = '3';
177
+ item.style.borderStyle = 'solid';
178
+ item.style.borderColor = '#00eaff';
179
+ item.style.background = 'rgba(0, 234, 255, 0.08)';
180
+ this.#placeholder.style.display = 'block';
181
+ setRect(this.#placeholder, rect);
182
+ event.preventDefault();
183
+ };
184
+
185
+ #onPointerMove = (event) => {
186
+ if (!this.#drag) {
187
+ const item = event.target.closest?.('[data-gauge-id]');
188
+ if (item) {
189
+ item.style.cursor = this.#resizeCursor(this.#resizeEdges(item, event));
190
+ }
191
+ return;
192
+ }
193
+ if (event.pointerId !== this.#drag.pointerId) {
194
+ return;
195
+ }
196
+ const point = this.#pointerLocal(event);
197
+ this.#drag.x = point.x;
198
+ this.#drag.y = point.y;
199
+ this.#drag.dx = point.x - this.#drag.startX;
200
+ this.#drag.dy = point.y - this.#drag.startY;
201
+ if (this.#frame === undefined) {
202
+ this.#frame = requestAnimationFrame(this.#applyPointerTransform);
203
+ }
204
+ this.#updateGridPreview();
205
+ event.preventDefault();
206
+ };
207
+
208
+ #applyPointerTransform = () => {
209
+ this.#frame = undefined;
210
+ if (!this.#drag) {
211
+ return;
212
+ }
213
+ if (this.#drag.resize) {
214
+ const minWidth = Math.min(24, this.#drag.initialRect.width);
215
+ const minHeight = Math.min(24, this.#drag.initialRect.height);
216
+ const widthDelta = this.#drag.dx * this.#drag.resizeX;
217
+ const heightDelta = this.#drag.dy * this.#drag.resizeY;
218
+ const width = Math.max(minWidth, this.#drag.initialRect.width + widthDelta);
219
+ const height = Math.max(minHeight, this.#drag.initialRect.height + heightDelta);
220
+ const offsetX = this.#drag.resizeX < 0
221
+ ? this.#drag.initialRect.width - width
222
+ : 0;
223
+ const offsetY = this.#drag.resizeY < 0
224
+ ? this.#drag.initialRect.height - height
225
+ : 0;
226
+ const scaleX = width / this.#drag.initialRect.width;
227
+ const scaleY = height / this.#drag.initialRect.height;
228
+ this.#drag.item.style.transformOrigin = '0 0';
229
+ this.#drag.item.style.transform = `translate3d(${offsetX}px, ${offsetY}px, 0) scale(${scaleX}, ${scaleY})`;
230
+ } else {
231
+ this.#drag.item.style.transform = `translate3d(${this.#drag.dx}px, ${this.#drag.dy}px, 0)`;
232
+ }
233
+ };
234
+
235
+ #updateGridPreview() {
236
+ const drag = this.#drag;
237
+ const { rows, columns, gap } = this.#layout.config;
238
+ const gridRect = this.#panelLayout.gridRect;
239
+ const cellWidth = (gridRect.width - gap * (columns - 1)) / columns;
240
+ const cellHeight = (gridRect.height - gap * (rows - 1)) / rows;
241
+ const columnPitch = cellWidth + gap;
242
+ const rowPitch = cellHeight + gap;
243
+ let nextRow = drag.gridRow;
244
+ let nextCol = drag.gridCol;
245
+ let nextRowSpan = drag.rowSpan;
246
+ let nextColSpan = drag.colSpan;
247
+
248
+ if (drag.resize) {
249
+ if (drag.resizeX < 0) {
250
+ nextCol = this.#quantize(drag.initial.col + drag.dx / columnPitch,
251
+ drag.gridCol, 0, drag.initial.col + drag.initial.colSpan - 1);
252
+ nextColSpan = drag.initial.colSpan + drag.initial.col - nextCol;
253
+ } else if (drag.resizeX > 0) {
254
+ nextColSpan = this.#quantize(drag.initial.colSpan + drag.dx / columnPitch,
255
+ drag.colSpan, 1, columns - drag.initial.col);
256
+ }
257
+ if (drag.resizeY < 0) {
258
+ nextRow = this.#quantize(drag.initial.row + drag.dy / rowPitch,
259
+ drag.gridRow, 0, drag.initial.row + drag.initial.rowSpan - 1);
260
+ nextRowSpan = drag.initial.rowSpan + drag.initial.row - nextRow;
261
+ } else if (drag.resizeY > 0) {
262
+ nextRowSpan = this.#quantize(drag.initial.rowSpan + drag.dy / rowPitch,
263
+ drag.rowSpan, 1, rows - drag.initial.row);
264
+ }
265
+ } else {
266
+ nextCol = this.#quantize(drag.initial.col + drag.dx / columnPitch,
267
+ drag.gridCol, 0, columns - drag.initial.colSpan);
268
+ nextRow = this.#quantize(drag.initial.row + drag.dy / rowPitch,
269
+ drag.gridRow, 0, rows - drag.initial.rowSpan);
270
+ }
271
+ if (nextRow === drag.gridRow && nextCol === drag.gridCol
272
+ && nextRowSpan === drag.rowSpan && nextColSpan === drag.colSpan) {
273
+ return;
274
+ }
275
+ drag.gridRow = nextRow;
276
+ drag.gridCol = nextCol;
277
+ drag.rowSpan = nextRowSpan;
278
+ drag.colSpan = nextColSpan;
279
+ drag.previewed = true;
280
+ const preview = drag.resize
281
+ ? drag.session.preview({
282
+ row: nextRow,
283
+ col: nextCol,
284
+ rowSpan: nextRowSpan,
285
+ colSpan: nextColSpan
286
+ })
287
+ : drag.session.preview({ row: nextRow, col: nextCol });
288
+ drag.valid = preview.valid;
289
+ this.#showPreview(preview.entries, preview.valid, {
290
+ ...drag.initial,
291
+ row: nextRow,
292
+ col: nextCol,
293
+ rowSpan: nextRowSpan,
294
+ colSpan: nextColSpan
295
+ });
296
+ this.#actions.preview(preview.entries);
297
+ }
298
+
299
+ #showPreview(entries, valid, candidate) {
300
+ const gridRect = this.#panelLayout.gridRect;
301
+ const previewRects = new Map([...this.#layout.rectangles(gridRect, entries)]
302
+ .map(([id, rect]) => [id, localRect(rect, gridRect)]));
303
+ for (const [id, node] of this.#nodes) {
304
+ if (id === this.#drag.id) {
305
+ continue;
306
+ }
307
+ const committed = this.#rectangles.get(id);
308
+ const preview = previewRects.get(id);
309
+ if (committed && preview) {
310
+ node.style.transform = `translate3d(${preview.x - committed.x}px, ${preview.y - committed.y}px, 0)`;
311
+ }
312
+ }
313
+ const placeholderRect = valid
314
+ ? previewRects.get(this.#drag.id)
315
+ : localRect(this.#layout.rectangles(
316
+ gridRect,
317
+ new Map([[this.#drag.id, candidate]])
318
+ ).get(this.#drag.id), gridRect);
319
+ setRect(this.#placeholder, placeholderRect);
320
+ this.#placeholder.style.borderColor = valid ? '#00eaff' : '#ff4d6d';
321
+ this.#placeholder.style.background = valid
322
+ ? 'rgba(0, 234, 255, 0.10)'
323
+ : 'rgba(255, 77, 109, 0.10)';
324
+ }
325
+
326
+ #onPointerUp = (event) => {
327
+ if (!this.#drag || event.pointerId !== this.#drag.pointerId) {
328
+ return;
329
+ }
330
+ if (!this.#drag.previewed) {
331
+ this.#drag.session.cancel();
332
+ this.#finishDrag(null);
333
+ event.preventDefault();
334
+ return;
335
+ }
336
+ const committed = this.#drag.valid && this.#drag.session.commit();
337
+ if (!committed) {
338
+ this.#drag.session.cancel();
339
+ }
340
+ this.#finishDrag(committed);
341
+ event.preventDefault();
342
+ };
343
+
344
+ #onPointerCancel = (event) => {
345
+ if (this.#drag && event.pointerId === this.#drag.pointerId) {
346
+ this.#cancelDrag();
347
+ }
348
+ };
349
+
350
+ #onKeyDown = (event) => {
351
+ if (event.key === 'Escape' && this.#drag) {
352
+ event.preventDefault();
353
+ this.#cancelDrag();
354
+ }
355
+ };
356
+
357
+ #cancelDrag() {
358
+ if (!this.#drag) {
359
+ return;
360
+ }
361
+ const previewed = this.#drag.previewed;
362
+ this.#drag.session.cancel();
363
+ this.#finishDrag(previewed ? false : null);
364
+ }
365
+
366
+ #finishDrag(committed) {
367
+ const drag = this.#drag;
368
+ if (!drag) {
369
+ return;
370
+ }
371
+ if (this.#frame !== undefined) {
372
+ cancelAnimationFrame(this.#frame);
373
+ this.#frame = undefined;
374
+ }
375
+ if (drag.item.hasPointerCapture?.(drag.pointerId)) {
376
+ drag.item.releasePointerCapture(drag.pointerId);
377
+ }
378
+ drag.item.style.transform = 'translate3d(0, 0, 0)';
379
+ drag.item.style.transformOrigin = '';
380
+ drag.item.style.cursor = 'grab';
381
+ drag.item.style.zIndex = '';
382
+ drag.item.style.borderColor = 'transparent';
383
+ drag.item.style.background = 'transparent';
384
+ this.#placeholder.style.display = 'none';
385
+ this.#drag = null;
386
+ for (const node of this.#nodes.values()) {
387
+ node.style.transform = 'translate3d(0, 0, 0)';
388
+ }
389
+ if (committed === true) {
390
+ this.#actions.commit();
391
+ } else if (committed === false) {
392
+ this.#actions.cancel();
393
+ }
394
+ this.refresh(this.#layout.rectangles(this.#panelLayout.gridRect), this.#panelLayout);
395
+ }
396
+
397
+ #pointerLocal(event) {
398
+ const bounds = this.#overlay.getBoundingClientRect();
399
+ const gridRect = this.#panelLayout.gridRect;
400
+ return {
401
+ x: bounds.width > 0 ? (event.clientX - bounds.left) * gridRect.width / bounds.width : 0,
402
+ y: bounds.height > 0 ? (event.clientY - bounds.top) * gridRect.height / bounds.height : 0
403
+ };
404
+ }
405
+
406
+ #resizeEdges(item, event) {
407
+ const bounds = item.getBoundingClientRect();
408
+ const hitX = Math.min(EDGE_HIT_SIZE, bounds.width * 0.24);
409
+ const hitY = Math.min(EDGE_HIT_SIZE, bounds.height * 0.24);
410
+ const left = event.clientX - bounds.left <= hitX;
411
+ const right = bounds.right - event.clientX <= hitX;
412
+ const top = event.clientY - bounds.top <= hitY;
413
+ const bottom = bounds.bottom - event.clientY <= hitY;
414
+ if (!(left || right || top || bottom)) {
415
+ return null;
416
+ }
417
+ return {
418
+ x: left ? -1 : right ? 1 : 0,
419
+ y: top ? -1 : bottom ? 1 : 0
420
+ };
421
+ }
422
+
423
+ #resizeCursor(edges) {
424
+ if (!edges) {
425
+ return 'grab';
426
+ }
427
+ if (edges.x === 0) {
428
+ return 'ns-resize';
429
+ }
430
+ if (edges.y === 0) {
431
+ return 'ew-resize';
432
+ }
433
+ return edges.x === edges.y ? 'nwse-resize' : 'nesw-resize';
434
+ }
435
+
436
+ #quantize(raw, current, min, max) {
437
+ let value = current;
438
+ while (value < max && raw > value + HYSTERESIS) {
439
+ value += 1;
440
+ }
441
+ while (value > min && raw < value - HYSTERESIS) {
442
+ value -= 1;
443
+ }
444
+ return value;
445
+ }
446
+ }
@@ -0,0 +1,167 @@
1
+ function bucket(width, height) {
2
+ if (width < 180 || height < 150) {
3
+ return 'small';
4
+ }
5
+ if (width < 300 || height < 230) {
6
+ return 'medium';
7
+ }
8
+ return 'large';
9
+ }
10
+
11
+ /**
12
+ * Fit one unwrapped line using a caller-provided real metrics function.
13
+ * Returns the chosen text and size without mutating or changing its case.
14
+ */
15
+ export function fitText(text, maxWidth, options) {
16
+ const value = String(text ?? '');
17
+ const measure = options?.measure;
18
+ if (typeof measure !== 'function') {
19
+ throw new TypeError('fitText() requires a measure(text, size, font) function.');
20
+ }
21
+ const targetSize = Math.max(1, Number(options.targetSize) || 11);
22
+ const minSize = Math.min(targetSize, Math.max(1, Number(options.minSize) || 9));
23
+ const step = Math.max(0.25, Number(options.step) || 0.5);
24
+ const font = options.font ?? 'regular';
25
+ const ellipsis = options.ellipsis ?? '…';
26
+ const available = Math.max(0, Number(maxWidth) || 0);
27
+
28
+ for (let size = targetSize; size >= minSize; size -= step) {
29
+ const width = measure(value, size, font);
30
+ if (width <= available) {
31
+ return { text: value, size, width, truncated: false, font };
32
+ }
33
+ }
34
+
35
+ const ellipsisWidth = measure(ellipsis, minSize, font);
36
+ if (ellipsisWidth > available) {
37
+ return { text: '', size: minSize, width: 0, truncated: value.length > 0, font };
38
+ }
39
+ let fitted = '';
40
+ for (const character of value) {
41
+ const candidate = `${fitted}${character}${ellipsis}`;
42
+ if (measure(candidate, minSize, font) > available) {
43
+ break;
44
+ }
45
+ fitted += character;
46
+ }
47
+ const result = fitted.length < value.length ? `${fitted}${ellipsis}` : fitted;
48
+ return {
49
+ text: result,
50
+ size: minSize,
51
+ width: measure(result, minSize, font),
52
+ truncated: result !== value,
53
+ font
54
+ };
55
+ }
56
+
57
+ /** Return the common meta/gauge/readout composition for one gauge cell. */
58
+ export function layoutCell({
59
+ width,
60
+ height,
61
+ gaugeType = 'arc',
62
+ orientation = 'horizontal',
63
+ hasInfo = false,
64
+ hasUnit = false,
65
+ hasMarkers = false,
66
+ hasMarkerLabels = false,
67
+ hasBandLabels = false
68
+ } = {}) {
69
+ const cellWidth = Math.max(1, Number(width) || 1);
70
+ const cellHeight = Math.max(1, Number(height) || 1);
71
+ const level = bucket(cellWidth, cellHeight);
72
+ const sizes = {
73
+ small: { padding: 8, label: 10, minLabel: 8.5, info: 9, value: 17, unit: 11, readout: 25 },
74
+ medium: { padding: 11, label: 11, minLabel: 9, info: 10, value: 23, unit: 12, readout: 32 },
75
+ large: { padding: 13, label: 12, minLabel: 9, info: 11, value: 29, unit: 13, readout: 39 }
76
+ }[level];
77
+ const showInfo = hasInfo && level !== 'small';
78
+ const showUnit = hasUnit && cellWidth >= 72 && cellHeight >= 82;
79
+ const labelLineHeight = sizes.label + 4;
80
+ const infoLineHeight = showInfo ? sizes.info + 3 : 0;
81
+ const metaHeight = labelLineHeight + (showInfo ? infoLineHeight + 1 : 0);
82
+ const innerWidth = Math.max(1, cellWidth - sizes.padding * 2);
83
+ const metaY = sizes.padding;
84
+ const gaugeY = metaY + metaHeight + 3;
85
+ const availableAfterMeta = Math.max(1, cellHeight - sizes.padding - gaugeY);
86
+ const valueSize = gaugeType === 'status' ? Math.min(sizes.value, 23) : sizes.value;
87
+ const centeredReadout = gaugeType === 'arc' || gaugeType === 'compass';
88
+ const sideReadout = gaugeType === 'linear' && orientation === 'vertical';
89
+ const footerReadoutHeight = Math.min(sizes.readout, Math.max(1, availableAfterMeta * 0.48));
90
+ const footerReadoutY = Math.max(gaugeY + 1, cellHeight - sizes.padding - footerReadoutHeight);
91
+ const gaugeHeight = centeredReadout || sideReadout
92
+ ? availableAfterMeta
93
+ : Math.max(1, footerReadoutY - gaugeY - 3);
94
+ const stackedReadoutHeight = Math.min(
95
+ gaugeHeight * 0.62,
96
+ valueSize + (showUnit ? sizes.unit + 6 : 2)
97
+ );
98
+ const sideReadoutHeight = Math.min(
99
+ gaugeHeight * 0.5,
100
+ Math.max(valueSize, showUnit ? sizes.unit : 0) + 4
101
+ );
102
+ const readoutHeight = centeredReadout
103
+ ? stackedReadoutHeight
104
+ : sideReadout ? sideReadoutHeight : footerReadoutHeight;
105
+ const readoutY = centeredReadout || sideReadout
106
+ ? gaugeY + (gaugeHeight - readoutHeight) / 2
107
+ : footerReadoutY;
108
+ const sideGap = sideReadout ? 4 : 0;
109
+ const sideGaugeRatio = hasBandLabels ? 0.56 : 0.44;
110
+ const sideGaugeWidth = sideReadout
111
+ ? Math.max(24, Math.min(innerWidth * sideGaugeRatio, innerWidth - sideGap - 1))
112
+ : innerWidth;
113
+ const readoutX = sideReadout ? sizes.padding + sideGaugeWidth + sideGap : sizes.padding;
114
+ const readoutWidth = sideReadout
115
+ ? Math.max(1, innerWidth - sideGaugeWidth - sideGap)
116
+ : innerWidth;
117
+
118
+ const labelRect = { x: sizes.padding, y: metaY, width: innerWidth, height: labelLineHeight };
119
+ const infoRect = {
120
+ x: sizes.padding,
121
+ y: metaY + labelLineHeight + 1,
122
+ width: innerWidth,
123
+ height: infoLineHeight
124
+ };
125
+
126
+ return {
127
+ level,
128
+ padding: sizes.padding,
129
+ metaRect: {
130
+ x: sizes.padding,
131
+ y: metaY,
132
+ width: innerWidth,
133
+ height: metaHeight
134
+ },
135
+ labelRect,
136
+ infoRect,
137
+ gaugeRect: {
138
+ x: sizes.padding,
139
+ y: gaugeY,
140
+ width: sideGaugeWidth,
141
+ height: gaugeHeight
142
+ },
143
+ readoutRect: {
144
+ x: readoutX,
145
+ y: readoutY,
146
+ width: readoutWidth,
147
+ height: readoutHeight
148
+ },
149
+ readoutMode: centeredReadout
150
+ ? 'center-stacked'
151
+ : sideReadout ? 'side-inline' : 'footer-inline',
152
+ typography: {
153
+ label: { size: sizes.label, minSize: sizes.minLabel, font: 'label', alpha: 0.95 },
154
+ info: { size: sizes.info, minSize: 8.5, font: 'regular', alpha: 0.70 },
155
+ value: { size: valueSize, minSize: gaugeType === 'status' ? 12 : 15, font: 'value', alpha: 1 },
156
+ unit: { size: sizes.unit, minSize: 9, font: 'unit', alpha: 0.75 },
157
+ ticks: { alpha: 0.52 }
158
+ },
159
+ showInfo,
160
+ showUnit,
161
+ showImportantMarker: hasMarkers,
162
+ showTicks: level !== 'small',
163
+ showBandLabels: hasBandLabels && gaugeType !== 'status',
164
+ showMarkerLabels: hasMarkerLabels,
165
+ showCompassLabels: level !== 'small'
166
+ };
167
+ }