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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/docs/multigauge-demo.png +0 -0
- package/package.json +29 -0
- package/src/MultiGauge.js +358 -0
- package/src/assets/fonts/InterVariable.woff2 +0 -0
- package/src/assets/fonts/LICENSE.txt +92 -0
- package/src/errors.js +7 -0
- package/src/gpu/GpuRuntime.js +153 -0
- package/src/gpu/IconCache.js +69 -0
- package/src/gpu/Renderer.js +370 -0
- package/src/gpu/SceneBuilder.js +736 -0
- package/src/gpu/TextAtlas.js +304 -0
- package/src/gpu/shaders.js +212 -0
- package/src/index.js +2 -0
- package/src/interaction/GridEditor.js +446 -0
- package/src/layout/CellLayout.js +167 -0
- package/src/layout/GridLayout.js +405 -0
- package/src/layout/PanelLayout.js +70 -0
- package/src/model/GaugeModel.js +196 -0
- package/src/theme.js +41 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { fitText, layoutCell } from '../layout/CellLayout.js';
|
|
2
|
+
import { layoutHeader } from '../layout/PanelLayout.js';
|
|
3
|
+
import { normalizeValue, projectBand, resolveBand, resolveStatus } from '../model/GaugeModel.js';
|
|
4
|
+
import { color, semanticColor } from '../theme.js';
|
|
5
|
+
|
|
6
|
+
const TAU = Math.PI * 2;
|
|
7
|
+
const BAND_THICKNESS = 2;
|
|
8
|
+
const RADIAL_BAND_LABEL_OFFSET = 12;
|
|
9
|
+
const RADIAL_TARGET_LABEL_OFFSET = 36;
|
|
10
|
+
const RADIAL_BAND_LABEL_SPACE = 20;
|
|
11
|
+
const RADIAL_TARGET_LABEL_SPACE = 44;
|
|
12
|
+
const MIN_RADIAL_RADIUS = 12;
|
|
13
|
+
const RADIAL_TRACK_INSET = 8;
|
|
14
|
+
const RADIAL_TICK_GAP = 3;
|
|
15
|
+
const radians = (degrees) => degrees * Math.PI / 180;
|
|
16
|
+
export const TEXT_INSTANCE_SIZE = 16;
|
|
17
|
+
|
|
18
|
+
function tangentTextAngle(angle) {
|
|
19
|
+
return Math.cos(angle) < 0 ? angle + Math.PI : angle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function radialBaseSpace(halfExtent) {
|
|
23
|
+
return Math.min(10, Math.max(5, halfExtent * 0.1));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canReserveRadialTier(halfExtent, baseRadius, space) {
|
|
27
|
+
const candidate = halfExtent - space;
|
|
28
|
+
return candidate >= Math.max(24, baseRadius * 0.58);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function labelFits(atlas, value, maxWidth) {
|
|
32
|
+
return maxWidth > 0 && atlas.measure(String(value), 8, 'label') <= maxWidth;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fitOptionalLabel(atlas, value, maxWidth) {
|
|
36
|
+
const fitted = fitText(value, maxWidth, {
|
|
37
|
+
targetSize: 9,
|
|
38
|
+
minSize: 8,
|
|
39
|
+
measure: (text, size, font) => atlas.measure(text, size, font),
|
|
40
|
+
font: 'label'
|
|
41
|
+
});
|
|
42
|
+
return fitted.text && !fitted.truncated ? fitted : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compassBandData(gauge, band) {
|
|
46
|
+
const range = gauge.max - gauge.min;
|
|
47
|
+
const segments = projectBand(band, gauge.min, gauge.max, true);
|
|
48
|
+
const lengths = segments.map(([from, to]) => (to - from) / range);
|
|
49
|
+
const total = lengths.reduce((sum, length) => sum + length, 0);
|
|
50
|
+
let remaining = total / 2;
|
|
51
|
+
let midpoint = segments[0][0];
|
|
52
|
+
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
|
|
53
|
+
const [from, to] = segments[segmentIndex];
|
|
54
|
+
if (remaining <= lengths[segmentIndex]) {
|
|
55
|
+
midpoint = from + remaining * range;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
remaining -= lengths[segmentIndex];
|
|
59
|
+
}
|
|
60
|
+
return { band, segments, total, midpoint };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shape(output, geometry, rgba, parameters, extra = [0, 0, 0, 0]) {
|
|
64
|
+
output.push(...geometry, ...rgba, ...parameters, ...extra, 0, 0, 0, 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function arc(output, x, y, radius, thickness, start, end, rgba, gaugeIndex = 0, dynamicMode = 0) {
|
|
68
|
+
shape(output, [x, y, radius + 3, radius + 3], rgba, [1, start, end, radius - thickness],
|
|
69
|
+
[radius, gaugeIndex, dynamicMode, 0]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function box(output, x, y, width, height, rgba, radius = 0, border = 0, gaugeIndex = 0, dynamicMode = 0) {
|
|
73
|
+
shape(output, [x + width / 2, y + height / 2, width / 2, height / 2], rgba,
|
|
74
|
+
[0, radius, border, 0], [0, gaugeIndex, dynamicMode, 0]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function capsule(output, x, y, width, height, rgba, gaugeIndex = 0, dynamicMode = 0) {
|
|
78
|
+
shape(output, [x + width / 2, y + height / 2, width / 2, height / 2], rgba,
|
|
79
|
+
[2, 0, 0, 0], [0, gaugeIndex, dynamicMode, 0]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function dynamicMarker(output, rect, rgba, gaugeIndex, vertical) {
|
|
83
|
+
shape(output, [rect.x + rect.width / 2, rect.y + rect.height / 2, rect.width / 2, rect.height / 2],
|
|
84
|
+
rgba, [3, 1.2, 0, 0], [0, gaugeIndex, vertical ? 5 : 4, 0]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function compassMarker(output, cx, cy, radius, rgba, gaugeIndex) {
|
|
88
|
+
const scale = Math.min(1, Math.max(0.55, radius / 64));
|
|
89
|
+
const extent = radius + 8;
|
|
90
|
+
shape(output, [cx, cy, extent, extent], rgba,
|
|
91
|
+
[6, 2.5 * scale, radius, 7 * scale], [0, gaugeIndex, 7, 0]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function radialStroke(
|
|
95
|
+
output,
|
|
96
|
+
cx,
|
|
97
|
+
cy,
|
|
98
|
+
markerRadius,
|
|
99
|
+
angle,
|
|
100
|
+
halfWidth,
|
|
101
|
+
halfLength,
|
|
102
|
+
rgba,
|
|
103
|
+
gaugeIndex
|
|
104
|
+
) {
|
|
105
|
+
const extent = markerRadius + halfLength + 2;
|
|
106
|
+
shape(output, [cx, cy, extent, extent], rgba,
|
|
107
|
+
[7, halfWidth, markerRadius, halfLength], [angle, gaugeIndex, 0, 0]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function radialMarker(output, cx, cy, radius, angle, rgba, gaugeIndex) {
|
|
111
|
+
const scale = Math.min(1, Math.max(0.55, radius / 64));
|
|
112
|
+
radialStroke(output, cx, cy, radius + 5 * scale, angle,
|
|
113
|
+
2 * scale, 8 * scale, rgba, gaugeIndex);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function radialTick(output, cx, cy, radius, angle, major, rgba, gaugeIndex) {
|
|
117
|
+
if (radius < 30) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const scale = Math.min(1, Math.max(0.55, radius / 64));
|
|
121
|
+
const halfLength = (major ? 5 : 3.5) * scale;
|
|
122
|
+
const outerRadius = radius - RADIAL_TRACK_INSET - RADIAL_TICK_GAP;
|
|
123
|
+
radialStroke(output, cx, cy, outerRadius - halfLength, angle,
|
|
124
|
+
(major ? 1.15 : 0.8) * scale, halfLength, rgba, gaugeIndex);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function textWidth(atlas, value, size, font = 'regular') {
|
|
128
|
+
return atlas.measure(value, size, font);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function snapToPhysicalPixel(value, pixelRatio) {
|
|
132
|
+
return Math.round(value * pixelRatio) / pixelRatio;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function addText(
|
|
136
|
+
atlas,
|
|
137
|
+
output,
|
|
138
|
+
value,
|
|
139
|
+
x,
|
|
140
|
+
y,
|
|
141
|
+
size,
|
|
142
|
+
rgba,
|
|
143
|
+
align = 'left',
|
|
144
|
+
font = 'regular',
|
|
145
|
+
verticalPosition = 'top',
|
|
146
|
+
rotation = 0
|
|
147
|
+
) {
|
|
148
|
+
const string = String(value);
|
|
149
|
+
const style = atlas.style(font, size);
|
|
150
|
+
const pixelRatio = atlas.pixelRatio;
|
|
151
|
+
const scale = size / style.logicalSize / pixelRatio;
|
|
152
|
+
let cursor = x;
|
|
153
|
+
const measured = textWidth(atlas, string, size, font);
|
|
154
|
+
if (align === 'center') {
|
|
155
|
+
cursor -= measured / 2;
|
|
156
|
+
} else if (align === 'right') {
|
|
157
|
+
cursor -= measured;
|
|
158
|
+
}
|
|
159
|
+
const metrics = atlas.metrics(size, font);
|
|
160
|
+
const baseline = snapToPhysicalPixel(
|
|
161
|
+
verticalPosition === 'baseline' ? y : y + metrics.ascent,
|
|
162
|
+
pixelRatio
|
|
163
|
+
);
|
|
164
|
+
const pivotY = verticalPosition === 'baseline'
|
|
165
|
+
? y + (metrics.descent - metrics.ascent) / 2
|
|
166
|
+
: y + metrics.lineHeight / 2;
|
|
167
|
+
const rotated = rotation !== 0;
|
|
168
|
+
const cos = rotated ? Math.cos(rotation) : 1;
|
|
169
|
+
const sin = rotated ? Math.sin(rotation) : 0;
|
|
170
|
+
const glyphs = style.glyphs;
|
|
171
|
+
const fallback = glyphs?.get('?');
|
|
172
|
+
for (const character of string) {
|
|
173
|
+
const glyph = glyphs?.get(character) ?? fallback ?? atlas.glyph(character, font, size);
|
|
174
|
+
if (character !== ' ') {
|
|
175
|
+
const left = snapToPhysicalPixel(
|
|
176
|
+
cursor + (glyph.offsetX - glyph.bearingLeft) * scale,
|
|
177
|
+
pixelRatio
|
|
178
|
+
);
|
|
179
|
+
const top = snapToPhysicalPixel(baseline - glyph.bearingTop * scale, pixelRatio);
|
|
180
|
+
const right = snapToPhysicalPixel(left + glyph.width * scale, pixelRatio);
|
|
181
|
+
const bottom = snapToPhysicalPixel(top + glyph.height * scale, pixelRatio);
|
|
182
|
+
const centerX = (left + right) / 2;
|
|
183
|
+
const centerY = (top + bottom) / 2;
|
|
184
|
+
const dx = centerX - x;
|
|
185
|
+
const dy = centerY - pivotY;
|
|
186
|
+
const offset = output.length;
|
|
187
|
+
output.length = offset + TEXT_INSTANCE_SIZE;
|
|
188
|
+
output[offset] = rotated ? x + dx * cos - dy * sin : centerX;
|
|
189
|
+
output[offset + 1] = rotated ? pivotY + dx * sin + dy * cos : centerY;
|
|
190
|
+
output[offset + 2] = (right - left) / 2;
|
|
191
|
+
output[offset + 3] = (bottom - top) / 2;
|
|
192
|
+
output[offset + 4] = glyph.u0;
|
|
193
|
+
output[offset + 5] = glyph.v0;
|
|
194
|
+
output[offset + 6] = glyph.u1;
|
|
195
|
+
output[offset + 7] = glyph.v1;
|
|
196
|
+
output[offset + 8] = rgba[0];
|
|
197
|
+
output[offset + 9] = rgba[1];
|
|
198
|
+
output[offset + 10] = rgba[2];
|
|
199
|
+
output[offset + 11] = rgba[3];
|
|
200
|
+
output[offset + 12] = cos;
|
|
201
|
+
output[offset + 13] = sin;
|
|
202
|
+
output[offset + 14] = 0;
|
|
203
|
+
output[offset + 15] = 0;
|
|
204
|
+
}
|
|
205
|
+
cursor += glyph.advance * scale;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function absoluteCellLayout(rect, gauge) {
|
|
210
|
+
const layout = layoutCell({
|
|
211
|
+
width: rect.width,
|
|
212
|
+
height: rect.height,
|
|
213
|
+
gaugeType: gauge.type,
|
|
214
|
+
orientation: gauge.orientation,
|
|
215
|
+
hasInfo: Boolean(gauge.info),
|
|
216
|
+
hasUnit: Boolean(gauge.unit),
|
|
217
|
+
hasMarkers: gauge.markers.length > 0,
|
|
218
|
+
hasMarkerLabels: gauge.markers.some((marker) => Boolean(marker.label)),
|
|
219
|
+
hasBandLabels: gauge.bands.some((band) => Boolean(band.label))
|
|
220
|
+
});
|
|
221
|
+
const absolute = { ...layout };
|
|
222
|
+
for (const name of ['metaRect', 'labelRect', 'infoRect', 'gaugeRect', 'readoutRect']) {
|
|
223
|
+
absolute[name] = {
|
|
224
|
+
...layout[name],
|
|
225
|
+
x: rect.x + layout[name].x,
|
|
226
|
+
y: rect.y + layout[name].y
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return absolute;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function addArcGauge(scene, gauge, rect, theme, index, layout) {
|
|
233
|
+
const cx = rect.x + rect.width / 2;
|
|
234
|
+
const cy = rect.y + rect.height / 2;
|
|
235
|
+
const halfExtent = Math.min(rect.width, rect.height) / 2;
|
|
236
|
+
const start = radians(gauge.startAngle);
|
|
237
|
+
const end = radians(gauge.endAngle);
|
|
238
|
+
const baseSpace = radialBaseSpace(halfExtent);
|
|
239
|
+
const baseRadius = Math.max(MIN_RADIAL_RADIUS, halfExtent - baseSpace);
|
|
240
|
+
const markerRadius = halfExtent - RADIAL_TARGET_LABEL_SPACE;
|
|
241
|
+
const markerLabelWidth = Math.min(markerRadius, rect.width * 0.4);
|
|
242
|
+
const showMarkerLabels = layout.showMarkerLabels
|
|
243
|
+
&& canReserveRadialTier(halfExtent, baseRadius, RADIAL_TARGET_LABEL_SPACE)
|
|
244
|
+
&& gauge.markers.some((marker) => marker.label
|
|
245
|
+
&& labelFits(scene.atlas, marker.label, markerLabelWidth));
|
|
246
|
+
const bandRadius = halfExtent - (showMarkerLabels
|
|
247
|
+
? RADIAL_TARGET_LABEL_SPACE
|
|
248
|
+
: RADIAL_BAND_LABEL_SPACE);
|
|
249
|
+
const bandLabelFits = (band) => {
|
|
250
|
+
if (!band.label) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const fromAngle = start + normalizeValue(gauge, band.from) * (end - start);
|
|
254
|
+
const toAngle = start + normalizeValue(gauge, band.to) * (end - start);
|
|
255
|
+
const labelRadius = bandRadius + RADIAL_BAND_LABEL_OFFSET;
|
|
256
|
+
return labelFits(scene.atlas, band.label, Math.max(0, Math.min(
|
|
257
|
+
bandRadius * 1.2,
|
|
258
|
+
Math.abs(toAngle - fromAngle) * labelRadius
|
|
259
|
+
)));
|
|
260
|
+
};
|
|
261
|
+
const showBandLabels = layout.showBandLabels
|
|
262
|
+
&& (showMarkerLabels
|
|
263
|
+
|| canReserveRadialTier(halfExtent, baseRadius, RADIAL_BAND_LABEL_SPACE))
|
|
264
|
+
&& gauge.bands.some(bandLabelFits);
|
|
265
|
+
const labelSpace = showMarkerLabels
|
|
266
|
+
? RADIAL_TARGET_LABEL_SPACE
|
|
267
|
+
: showBandLabels ? RADIAL_BAND_LABEL_SPACE : baseSpace;
|
|
268
|
+
const radius = Math.max(MIN_RADIAL_RADIUS, halfExtent - labelSpace);
|
|
269
|
+
arc(scene.shapes, cx, cy, radius, 3, start, end, color(theme.grid), index);
|
|
270
|
+
if (layout.showTicks) {
|
|
271
|
+
for (let tick = 0; tick <= 10; tick += 1) {
|
|
272
|
+
const angle = start + tick / 10 * (end - start);
|
|
273
|
+
radialTick(scene.shapes, cx, cy, radius, angle,
|
|
274
|
+
tick === 0 || tick === 5 || tick === 10,
|
|
275
|
+
color(theme.muted, layout.typography.ticks.alpha), index);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const band of gauge.bands) {
|
|
279
|
+
for (const [from, to] of projectBand(band, gauge.min, gauge.max)) {
|
|
280
|
+
const fromAngle = start + normalizeValue(gauge, from) * (end - start);
|
|
281
|
+
const toAngle = start + normalizeValue(gauge, to) * (end - start);
|
|
282
|
+
arc(scene.shapes, cx, cy, radius + 6, BAND_THICKNESS,
|
|
283
|
+
fromAngle,
|
|
284
|
+
toAngle,
|
|
285
|
+
semanticColor(theme, band.kind, 'normal'), index);
|
|
286
|
+
if (showBandLabels && band.label) {
|
|
287
|
+
const angle = (fromAngle + toAngle) / 2;
|
|
288
|
+
const labelRadius = radius + RADIAL_BAND_LABEL_OFFSET;
|
|
289
|
+
const available = Math.max(0, Math.min(
|
|
290
|
+
radius * 1.2,
|
|
291
|
+
Math.abs(toAngle - fromAngle) * labelRadius
|
|
292
|
+
));
|
|
293
|
+
const fitted = fitOptionalLabel(scene.atlas, band.label, available);
|
|
294
|
+
if (fitted) {
|
|
295
|
+
addText(scene.atlas, scene.text, fitted.text,
|
|
296
|
+
cx + Math.sin(angle) * labelRadius,
|
|
297
|
+
cy - Math.cos(angle) * labelRadius - fitted.size / 2,
|
|
298
|
+
fitted.size, semanticColor(theme, band.kind, 'normal'),
|
|
299
|
+
'center', 'label', 'top', tangentTextAngle(angle));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
arc(scene.shapes, cx, cy, radius, 8, start, end, color(theme.accent, 0.12), index, 1);
|
|
305
|
+
arc(scene.shapes, cx, cy, radius, 3, start, end, color(theme.accent), index, 1);
|
|
306
|
+
for (const marker of gauge.markers) {
|
|
307
|
+
const angle = start + normalizeValue(gauge, marker.value) * (end - start);
|
|
308
|
+
radialMarker(scene.shapes, cx, cy, radius, angle,
|
|
309
|
+
semanticColor(theme, marker.kind, 'target'), index);
|
|
310
|
+
if (showMarkerLabels && marker.label) {
|
|
311
|
+
const labelRadius = radius + RADIAL_TARGET_LABEL_OFFSET;
|
|
312
|
+
const fitted = fitOptionalLabel(
|
|
313
|
+
scene.atlas,
|
|
314
|
+
marker.label,
|
|
315
|
+
Math.min(radius, rect.width * 0.4)
|
|
316
|
+
);
|
|
317
|
+
if (fitted) {
|
|
318
|
+
addText(scene.atlas, scene.text, fitted.text,
|
|
319
|
+
cx + Math.sin(angle) * labelRadius,
|
|
320
|
+
cy - Math.cos(angle) * labelRadius - fitted.size / 2,
|
|
321
|
+
fitted.size, semanticColor(theme, marker.kind, 'target'),
|
|
322
|
+
'center', 'label');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function addLinearGauge(scene, gauge, rect, theme, index, layout) {
|
|
329
|
+
const vertical = gauge.orientation === 'vertical';
|
|
330
|
+
const length = vertical ? Math.max(1, rect.height - 14) : rect.width * 0.78;
|
|
331
|
+
const thickness = Math.max(5, Math.min(18, vertical ? rect.width * 0.13 : rect.height * 0.18));
|
|
332
|
+
const x = vertical ? rect.x + (rect.width - thickness) / 2 : rect.x + (rect.width - length) / 2;
|
|
333
|
+
const y = vertical ? rect.y + (rect.height - length) / 2 : rect.y + (rect.height - thickness) / 2;
|
|
334
|
+
const width = vertical ? thickness : length;
|
|
335
|
+
const height = vertical ? length : thickness;
|
|
336
|
+
capsule(scene.shapes, x, y, width, height, color(theme.grid), index);
|
|
337
|
+
for (const band of gauge.bands) {
|
|
338
|
+
const from = normalizeValue(gauge, band.from);
|
|
339
|
+
const to = normalizeValue(gauge, band.to);
|
|
340
|
+
const rgba = semanticColor(theme, band.kind, 'normal');
|
|
341
|
+
if (vertical) {
|
|
342
|
+
capsule(scene.shapes, x + width + 5, y + height * (1 - Math.max(from, to)),
|
|
343
|
+
BAND_THICKNESS, height * Math.abs(to - from), rgba, index);
|
|
344
|
+
} else {
|
|
345
|
+
capsule(scene.shapes, x + width * Math.min(from, to), y + height + 5,
|
|
346
|
+
width * Math.abs(to - from), BAND_THICKNESS, rgba, index);
|
|
347
|
+
}
|
|
348
|
+
if (layout.showBandLabels && band.label) {
|
|
349
|
+
const midpoint = (from + to) / 2;
|
|
350
|
+
if (vertical) {
|
|
351
|
+
const available = Math.max(0, x - rect.x - 8);
|
|
352
|
+
const fitted = fitOptionalLabel(scene.atlas, band.label, available);
|
|
353
|
+
if (fitted) {
|
|
354
|
+
addText(scene.atlas, scene.text, fitted.text, x - 7,
|
|
355
|
+
y + height * (1 - midpoint) - fitted.size / 2,
|
|
356
|
+
fitted.size, rgba, 'right', 'label');
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
const available = width * Math.abs(to - from);
|
|
360
|
+
const fitted = fitOptionalLabel(scene.atlas, band.label, available);
|
|
361
|
+
if (fitted) {
|
|
362
|
+
addText(scene.atlas, scene.text, fitted.text, x + width * midpoint,
|
|
363
|
+
y + height + 9, fitted.size, rgba, 'center', 'label');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (gauge.mode === 'fill' || gauge.mode === 'fill-marker') {
|
|
369
|
+
capsule(scene.shapes, x, y, width, height, color(theme.accent), index, vertical ? 3 : 2);
|
|
370
|
+
}
|
|
371
|
+
if (gauge.mode === 'marker' || gauge.mode === 'fill-marker') {
|
|
372
|
+
const markerRect = vertical
|
|
373
|
+
? { x: x - 4, y, width: width + 8, height }
|
|
374
|
+
: { x, y: y - 4, width, height: height + 8 };
|
|
375
|
+
dynamicMarker(scene.shapes, markerRect, color(theme.accent), index, vertical);
|
|
376
|
+
}
|
|
377
|
+
for (const marker of gauge.markers) {
|
|
378
|
+
const normalized = normalizeValue(gauge, marker.value);
|
|
379
|
+
if (vertical) {
|
|
380
|
+
capsule(scene.shapes, x - 5, y + height * (1 - normalized) - 1, width + 10, 2,
|
|
381
|
+
semanticColor(theme, marker.kind, 'target'), index);
|
|
382
|
+
} else {
|
|
383
|
+
capsule(scene.shapes, x + width * normalized - 1, y - 5, 2, height + 10,
|
|
384
|
+
semanticColor(theme, marker.kind, 'target'), index);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function addCompass(scene, gauge, rect, theme, index, layout) {
|
|
390
|
+
const cx = rect.x + rect.width / 2;
|
|
391
|
+
const cy = rect.y + rect.height / 2;
|
|
392
|
+
const halfExtent = Math.min(rect.width, rect.height) / 2;
|
|
393
|
+
const baseSpace = radialBaseSpace(halfExtent);
|
|
394
|
+
const baseRadius = Math.max(MIN_RADIAL_RADIUS, halfExtent - baseSpace);
|
|
395
|
+
const markerRadius = halfExtent - RADIAL_TARGET_LABEL_SPACE;
|
|
396
|
+
const markerLabelWidth = Math.min(markerRadius, rect.width * 0.4);
|
|
397
|
+
const showMarkerLabels = layout.showMarkerLabels
|
|
398
|
+
&& canReserveRadialTier(halfExtent, baseRadius, RADIAL_TARGET_LABEL_SPACE)
|
|
399
|
+
&& gauge.markers.some((marker) => marker.label
|
|
400
|
+
&& labelFits(scene.atlas, marker.label, markerLabelWidth));
|
|
401
|
+
const projectedBands = gauge.bands.map((band) => compassBandData(gauge, band));
|
|
402
|
+
const bandRadius = halfExtent - (showMarkerLabels
|
|
403
|
+
? RADIAL_TARGET_LABEL_SPACE
|
|
404
|
+
: RADIAL_BAND_LABEL_SPACE);
|
|
405
|
+
const showBandLabels = layout.showBandLabels
|
|
406
|
+
&& (showMarkerLabels
|
|
407
|
+
|| canReserveRadialTier(halfExtent, baseRadius, RADIAL_BAND_LABEL_SPACE))
|
|
408
|
+
&& projectedBands.some(({ band, total }) => band.label
|
|
409
|
+
&& labelFits(scene.atlas, band.label, Math.max(0, Math.min(
|
|
410
|
+
bandRadius * 1.2,
|
|
411
|
+
total * TAU * (bandRadius + RADIAL_BAND_LABEL_OFFSET)
|
|
412
|
+
))));
|
|
413
|
+
const labelSpace = showMarkerLabels
|
|
414
|
+
? RADIAL_TARGET_LABEL_SPACE
|
|
415
|
+
: showBandLabels ? RADIAL_BAND_LABEL_SPACE : baseSpace;
|
|
416
|
+
const radius = Math.max(MIN_RADIAL_RADIUS, halfExtent - labelSpace);
|
|
417
|
+
arc(scene.shapes, cx, cy, radius, 2, 0, TAU, color(theme.grid), index);
|
|
418
|
+
for (const { band, segments, total, midpoint } of projectedBands) {
|
|
419
|
+
for (const [from, to] of segments) {
|
|
420
|
+
arc(scene.shapes, cx, cy, radius + 5, BAND_THICKNESS,
|
|
421
|
+
normalizeValue(gauge, from) * TAU,
|
|
422
|
+
normalizeValue(gauge, to === gauge.max ? gauge.min : to) * TAU
|
|
423
|
+
+ (to === gauge.max ? TAU : 0),
|
|
424
|
+
semanticColor(theme, band.kind, 'normal'), index);
|
|
425
|
+
}
|
|
426
|
+
if (showBandLabels && band.label) {
|
|
427
|
+
const angle = normalizeValue(gauge, midpoint) * TAU;
|
|
428
|
+
const labelRadius = radius + RADIAL_BAND_LABEL_OFFSET;
|
|
429
|
+
const available = Math.max(0, Math.min(radius * 1.2, total * TAU * labelRadius));
|
|
430
|
+
const fitted = fitOptionalLabel(scene.atlas, band.label, available);
|
|
431
|
+
if (fitted) {
|
|
432
|
+
addText(scene.atlas, scene.text, fitted.text,
|
|
433
|
+
cx + Math.sin(angle) * labelRadius,
|
|
434
|
+
cy - Math.cos(angle) * labelRadius - fitted.size / 2,
|
|
435
|
+
fitted.size, semanticColor(theme, band.kind, 'normal'),
|
|
436
|
+
'center', 'label', 'top', tangentTextAngle(angle));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (layout.showTicks) {
|
|
441
|
+
for (let tick = 0; tick < 12; tick += 1) {
|
|
442
|
+
if (tick % 3 === 0) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const angle = tick / 12 * TAU;
|
|
446
|
+
radialTick(scene.shapes, cx, cy, radius, angle, false,
|
|
447
|
+
color(theme.muted, layout.typography.ticks.alpha), index);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
for (const marker of gauge.markers) {
|
|
451
|
+
const angle = normalizeValue(gauge, marker.value) * TAU;
|
|
452
|
+
radialMarker(scene.shapes, cx, cy, radius, angle,
|
|
453
|
+
semanticColor(theme, marker.kind, 'target'), index);
|
|
454
|
+
if (showMarkerLabels && marker.label) {
|
|
455
|
+
const labelRadius = radius + RADIAL_TARGET_LABEL_OFFSET;
|
|
456
|
+
const fitted = fitOptionalLabel(
|
|
457
|
+
scene.atlas,
|
|
458
|
+
marker.label,
|
|
459
|
+
Math.min(radius, rect.width * 0.4)
|
|
460
|
+
);
|
|
461
|
+
if (fitted) {
|
|
462
|
+
addText(scene.atlas, scene.text, fitted.text,
|
|
463
|
+
cx + Math.sin(angle) * labelRadius,
|
|
464
|
+
cy - Math.cos(angle) * labelRadius - fitted.size / 2,
|
|
465
|
+
fitted.size, semanticColor(theme, marker.kind, 'target'),
|
|
466
|
+
'center', 'label');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
compassMarker(scene.shapes, cx, cy, radius, color(theme.accent), index);
|
|
471
|
+
if (layout.showCompassLabels) {
|
|
472
|
+
const labelRadius = Math.max(4, radius - 12);
|
|
473
|
+
for (const [label, angle] of [['N', 0], ['E', Math.PI / 2], ['S', Math.PI], ['W', Math.PI * 1.5]]) {
|
|
474
|
+
addText(scene.atlas, scene.text, label, cx + Math.sin(angle) * labelRadius,
|
|
475
|
+
cy - Math.cos(angle) * labelRadius - layout.typography.unit.size / 2,
|
|
476
|
+
layout.typography.unit.size, color(theme.text, 0.55),
|
|
477
|
+
'center', layout.typography.unit.font);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function addStatus(scene, rect, theme, index) {
|
|
483
|
+
const radius = Math.max(5, Math.min(rect.width, rect.height) * 0.14);
|
|
484
|
+
const cx = rect.x + rect.width / 2;
|
|
485
|
+
const cy = rect.y + rect.height / 2;
|
|
486
|
+
shape(scene.shapes, [cx, cy, radius * 2, radius * 2], color(theme.inactive, 0.25),
|
|
487
|
+
[5, radius * 1.6, 0, 0], [0, index, 6, 0]);
|
|
488
|
+
shape(scene.shapes, [cx, cy, radius, radius], color(theme.inactive),
|
|
489
|
+
[5, radius, 0, 0], [0, index, 6, 0]);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function buildStaticScene(atlas, gauges, rectangles, header, theme, panelLayout) {
|
|
493
|
+
const scene = { shapes: [], text: [], atlas, cellLayouts: new Map(), iconRect: null };
|
|
494
|
+
const { headerRect } = panelLayout;
|
|
495
|
+
if (header && headerRect.height > 0) {
|
|
496
|
+
box(scene.shapes, headerRect.x, headerRect.y, headerRect.width, headerRect.height,
|
|
497
|
+
color(theme.surface), 8);
|
|
498
|
+
const headerContent = layoutHeader(headerRect, { hasIcon: Boolean(header.icon) });
|
|
499
|
+
scene.iconRect = headerContent.iconRect;
|
|
500
|
+
const badgeVisible = Boolean(header.badge) && headerRect.width > 420;
|
|
501
|
+
const textWidthAvailable = badgeVisible
|
|
502
|
+
? headerContent.badgeRect.x - headerContent.title.x - 12
|
|
503
|
+
: headerRect.x + headerRect.width - headerContent.title.x - 18;
|
|
504
|
+
if (header.title) {
|
|
505
|
+
const fitted = fitText(header.title, textWidthAvailable, {
|
|
506
|
+
targetSize: headerContent.title.size,
|
|
507
|
+
minSize: 11,
|
|
508
|
+
measure: (text, size, font) => atlas.measure(text, size, font),
|
|
509
|
+
font: 'header'
|
|
510
|
+
});
|
|
511
|
+
addText(atlas, scene.text, fitted.text, headerContent.title.x, headerContent.title.y,
|
|
512
|
+
fitted.size, color(theme.text, 0.95), 'left', 'header');
|
|
513
|
+
}
|
|
514
|
+
if (header.subtitle && headerRect.width > 280) {
|
|
515
|
+
const fitted = fitText(header.subtitle, textWidthAvailable, {
|
|
516
|
+
targetSize: headerContent.subtitle.size,
|
|
517
|
+
minSize: 9,
|
|
518
|
+
measure: (text, size, font) => atlas.measure(text, size, font),
|
|
519
|
+
font: 'regular'
|
|
520
|
+
});
|
|
521
|
+
addText(atlas, scene.text, fitted.text, headerContent.subtitle.x, headerContent.subtitle.y,
|
|
522
|
+
fitted.size, color(theme.text, 0.70), 'left', 'regular');
|
|
523
|
+
}
|
|
524
|
+
if (badgeVisible) {
|
|
525
|
+
const fitted = fitText(header.badge, headerContent.badgeRect.width - 16, {
|
|
526
|
+
targetSize: 10,
|
|
527
|
+
minSize: 9,
|
|
528
|
+
measure: (text, size, font) => atlas.measure(text, size, font),
|
|
529
|
+
font: 'label'
|
|
530
|
+
});
|
|
531
|
+
const badgeWidth = fitted.width + 16;
|
|
532
|
+
const badgeRect = {
|
|
533
|
+
...headerContent.badgeRect,
|
|
534
|
+
x: headerRect.x + headerRect.width - 18 - badgeWidth,
|
|
535
|
+
width: badgeWidth
|
|
536
|
+
};
|
|
537
|
+
box(scene.shapes, badgeRect.x, badgeRect.y, badgeRect.width, badgeRect.height,
|
|
538
|
+
color(theme.accent, 0.09), 6, 1);
|
|
539
|
+
addText(atlas, scene.text, fitted.text, badgeRect.x + badgeRect.width / 2,
|
|
540
|
+
badgeRect.y + 5, fitted.size, color(theme.accent), 'center', 'label');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const measure = (text, size, font) => atlas.measure(text, size, font);
|
|
544
|
+
gauges.forEach((gauge, index) => {
|
|
545
|
+
const rect = rectangles.get(gauge.id);
|
|
546
|
+
if (!rect) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const layout = absoluteCellLayout(rect, gauge);
|
|
550
|
+
scene.cellLayouts.set(gauge.id, layout);
|
|
551
|
+
box(scene.shapes, rect.x, rect.y, rect.width, rect.height, color(theme.surface), 7);
|
|
552
|
+
box(scene.shapes, rect.x, rect.y, rect.width, rect.height, color(theme.grid, 0.7), 7, 1);
|
|
553
|
+
|
|
554
|
+
const fittedLabel = fitText(gauge.label, layout.labelRect.width, {
|
|
555
|
+
targetSize: layout.typography.label.size,
|
|
556
|
+
minSize: layout.typography.label.minSize,
|
|
557
|
+
measure,
|
|
558
|
+
font: layout.typography.label.font
|
|
559
|
+
});
|
|
560
|
+
addText(atlas, scene.text, fittedLabel.text, layout.labelRect.x, layout.labelRect.y,
|
|
561
|
+
fittedLabel.size, color(theme.text, layout.typography.label.alpha),
|
|
562
|
+
'left', layout.typography.label.font);
|
|
563
|
+
if (layout.showInfo) {
|
|
564
|
+
const fittedInfo = fitText(gauge.info, layout.infoRect.width, {
|
|
565
|
+
targetSize: layout.typography.info.size,
|
|
566
|
+
minSize: layout.typography.info.minSize,
|
|
567
|
+
measure,
|
|
568
|
+
font: layout.typography.info.font
|
|
569
|
+
});
|
|
570
|
+
addText(atlas, scene.text, fittedInfo.text, layout.infoRect.x, layout.infoRect.y,
|
|
571
|
+
fittedInfo.size, color(theme.text, layout.typography.info.alpha),
|
|
572
|
+
'left', layout.typography.info.font);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (gauge.type === 'arc') {
|
|
576
|
+
addArcGauge(scene, gauge, layout.gaugeRect, theme, index, layout);
|
|
577
|
+
} else if (gauge.type === 'linear') {
|
|
578
|
+
addLinearGauge(scene, gauge, layout.gaugeRect, theme, index, layout);
|
|
579
|
+
} else if (gauge.type === 'compass') {
|
|
580
|
+
addCompass(scene, gauge, layout.gaugeRect, theme, index, layout);
|
|
581
|
+
} else {
|
|
582
|
+
addStatus(scene, layout.gaugeRect, theme, index);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
return scene;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function formatGaugeValue(gauge) {
|
|
589
|
+
if (gauge.type === 'status') {
|
|
590
|
+
return resolveStatus(gauge, gauge.value).label;
|
|
591
|
+
}
|
|
592
|
+
const value = Number(gauge.value);
|
|
593
|
+
if (gauge.type === 'compass') {
|
|
594
|
+
return Math.round(value).toString().padStart(3, '0');
|
|
595
|
+
}
|
|
596
|
+
return Math.abs(value) >= 100 ? String(Math.round(value)) : String(Math.round(value * 10) / 10);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** A compact key for deciding whether dynamic glyph geometry or color actually changed. */
|
|
600
|
+
export function dynamicTextKey(gauges) {
|
|
601
|
+
let key = '';
|
|
602
|
+
for (const gauge of gauges) {
|
|
603
|
+
const formatted = formatGaugeValue(gauge);
|
|
604
|
+
if (gauge.type === 'status') {
|
|
605
|
+
const status = resolveStatus(gauge, gauge.value);
|
|
606
|
+
key += `${formatted.length}:${formatted}@status:${status.kind ?? 'normal'}|`;
|
|
607
|
+
} else {
|
|
608
|
+
const activeBand = resolveBand(gauge);
|
|
609
|
+
const bandKind = activeBand ? activeBand.kind ?? 'normal' : 'text';
|
|
610
|
+
key += `${formatted.length}:${formatted}@${bandKind}|`;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return key;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Write the small per-gauge value/color records without allocating an intermediate array. */
|
|
617
|
+
export function buildDynamicValues(gauges, theme, output = []) {
|
|
618
|
+
const length = Math.max(8, gauges.length * 8);
|
|
619
|
+
if (Array.isArray(output)) {
|
|
620
|
+
output.length = length;
|
|
621
|
+
} else if (output.length < length) {
|
|
622
|
+
throw new RangeError('Dynamic value output is smaller than required.');
|
|
623
|
+
}
|
|
624
|
+
let offset = 0;
|
|
625
|
+
const accent = color(theme.accent);
|
|
626
|
+
for (const gauge of gauges) {
|
|
627
|
+
const normalized = gauge.type === 'status' ? 0 : normalizeValue(gauge, gauge.value);
|
|
628
|
+
const status = gauge.type === 'status' ? resolveStatus(gauge, gauge.value) : null;
|
|
629
|
+
const rgba = status
|
|
630
|
+
? semanticColor(theme, status.kind, status.kind === 'inactive' ? 'inactive' : 'normal')
|
|
631
|
+
: accent;
|
|
632
|
+
output[offset] = normalized;
|
|
633
|
+
output[offset + 1] = Number(gauge.value) || 0;
|
|
634
|
+
output[offset + 2] = 0;
|
|
635
|
+
output[offset + 3] = 0;
|
|
636
|
+
output[offset + 4] = rgba[0];
|
|
637
|
+
output[offset + 5] = rgba[1];
|
|
638
|
+
output[offset + 6] = rgba[2];
|
|
639
|
+
output[offset + 7] = rgba[3];
|
|
640
|
+
offset += 8;
|
|
641
|
+
}
|
|
642
|
+
if (gauges.length === 0) {
|
|
643
|
+
output[0] = 0;
|
|
644
|
+
output[1] = 0;
|
|
645
|
+
output[2] = 0;
|
|
646
|
+
output[3] = 0;
|
|
647
|
+
output[4] = 1;
|
|
648
|
+
output[5] = 1;
|
|
649
|
+
output[6] = 1;
|
|
650
|
+
output[7] = 1;
|
|
651
|
+
}
|
|
652
|
+
return output;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Rebuild glyph instances only when the formatted readout or semantic color changes. */
|
|
656
|
+
export function buildDynamicText(atlas, gauges, cellLayouts, theme, output = []) {
|
|
657
|
+
output.length = 0;
|
|
658
|
+
const measure = (value, size, font) => atlas.measure(value, size, font);
|
|
659
|
+
gauges.forEach((gauge) => {
|
|
660
|
+
const status = gauge.type === 'status' ? resolveStatus(gauge, gauge.value) : null;
|
|
661
|
+
const activeBand = gauge.type === 'status' ? null : resolveBand(gauge);
|
|
662
|
+
const valueColor = activeBand
|
|
663
|
+
? semanticColor(theme, activeBand.kind, 'normal')
|
|
664
|
+
: color(theme.text);
|
|
665
|
+
const layout = cellLayouts.get(gauge.id);
|
|
666
|
+
if (!layout) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const formatted = formatGaugeValue(gauge);
|
|
670
|
+
const stacked = layout.readoutMode.endsWith('-stacked');
|
|
671
|
+
const unit = layout.showUnit && gauge.unit
|
|
672
|
+
? fitText(gauge.unit, stacked ? layout.readoutRect.width : layout.readoutRect.width * 0.34, {
|
|
673
|
+
targetSize: layout.typography.unit.size,
|
|
674
|
+
minSize: layout.typography.unit.minSize,
|
|
675
|
+
measure,
|
|
676
|
+
font: layout.typography.unit.font
|
|
677
|
+
})
|
|
678
|
+
: null;
|
|
679
|
+
const unitWidth = !stacked && unit ? unit.width + 6 : 0;
|
|
680
|
+
const fitted = fitText(formatted, layout.readoutRect.width - unitWidth, {
|
|
681
|
+
targetSize: layout.typography.value.size,
|
|
682
|
+
minSize: layout.typography.value.minSize,
|
|
683
|
+
measure,
|
|
684
|
+
font: layout.typography.value.font
|
|
685
|
+
});
|
|
686
|
+
const readoutColor = status
|
|
687
|
+
? semanticColor(theme, status.kind, status.kind === 'inactive' ? 'inactive' : 'normal')
|
|
688
|
+
: valueColor;
|
|
689
|
+
const valueWidth = textWidth(atlas, fitted.text, fitted.size, layout.typography.value.font);
|
|
690
|
+
const valueMetrics = atlas.metrics(fitted.size, layout.typography.value.font);
|
|
691
|
+
if (stacked) {
|
|
692
|
+
const unitMetrics = unit
|
|
693
|
+
? atlas.metrics(unit.size, layout.typography.unit.font)
|
|
694
|
+
: { ascent: 0, descent: 0, lineHeight: 0 };
|
|
695
|
+
const gap = unit ? 4 : 0;
|
|
696
|
+
const totalHeight = valueMetrics.lineHeight + gap + unitMetrics.lineHeight;
|
|
697
|
+
const top = layout.readoutRect.y + (layout.readoutRect.height - totalHeight) / 2;
|
|
698
|
+
addText(atlas, output, fitted.text,
|
|
699
|
+
layout.readoutRect.x + layout.readoutRect.width / 2,
|
|
700
|
+
top + valueMetrics.ascent, fitted.size, readoutColor,
|
|
701
|
+
'center', layout.typography.value.font, 'baseline');
|
|
702
|
+
if (unit) {
|
|
703
|
+
addText(atlas, output, unit.text,
|
|
704
|
+
layout.readoutRect.x + layout.readoutRect.width / 2,
|
|
705
|
+
top + valueMetrics.lineHeight + gap + unitMetrics.ascent,
|
|
706
|
+
unit.size, color(theme.text, layout.typography.unit.alpha),
|
|
707
|
+
'center', layout.typography.unit.font, 'baseline');
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
const groupX = layout.readoutRect.x
|
|
711
|
+
+ (layout.readoutRect.width - valueWidth - unitWidth) / 2;
|
|
712
|
+
const unitMetrics = unit
|
|
713
|
+
? atlas.metrics(unit.size, layout.typography.unit.font)
|
|
714
|
+
: { ascent: 0, descent: 0 };
|
|
715
|
+
const ascent = Math.max(valueMetrics.ascent, unitMetrics.ascent);
|
|
716
|
+
const descent = Math.max(valueMetrics.descent, unitMetrics.descent);
|
|
717
|
+
const baseline = layout.readoutRect.y
|
|
718
|
+
+ (layout.readoutRect.height - ascent - descent) / 2 + ascent;
|
|
719
|
+
addText(atlas, output, fitted.text, groupX, baseline, fitted.size, readoutColor,
|
|
720
|
+
'left', layout.typography.value.font, 'baseline');
|
|
721
|
+
if (unit) {
|
|
722
|
+
addText(atlas, output, unit.text, groupX + valueWidth + 6, baseline,
|
|
723
|
+
unit.size, color(theme.text, layout.typography.unit.alpha),
|
|
724
|
+
'left', layout.typography.unit.font, 'baseline');
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
return output;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function buildDynamicData(atlas, gauges, cellLayouts, theme) {
|
|
732
|
+
return {
|
|
733
|
+
values: buildDynamicValues(gauges, theme),
|
|
734
|
+
text: buildDynamicText(atlas, gauges, cellLayouts, theme)
|
|
735
|
+
};
|
|
736
|
+
}
|