@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.151 → 2.0.0-next.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/vitest.setup.ts +12 -1
- package/bundle/openbridge-webcomponents.bundle.js +989 -319
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +1514 -77
- package/dist/alert-severity.d.ts +12 -9
- package/dist/alert-severity.d.ts.map +1 -1
- package/dist/alert-severity.js +30 -38
- package/dist/alert-severity.js.map +1 -1
- package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts +6 -0
- package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts.map +1 -1
- package/dist/building-blocks/bar-horizontal/bar-horizontal.js +6 -1
- package/dist/building-blocks/bar-horizontal/bar-horizontal.js.map +1 -1
- package/dist/building-blocks/bar-vertical/bar-vertical.d.ts +6 -0
- package/dist/building-blocks/bar-vertical/bar-vertical.d.ts.map +1 -1
- package/dist/building-blocks/bar-vertical/bar-vertical.js +6 -1
- package/dist/building-blocks/bar-vertical/bar-vertical.js.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.d.ts +129 -0
- package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.js +464 -79
- package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
- package/dist/building-blocks/external-scale/external-scale.d.ts +11 -0
- package/dist/building-blocks/external-scale/external-scale.d.ts.map +1 -1
- package/dist/building-blocks/external-scale/external-scale.js +45 -12
- package/dist/building-blocks/external-scale/external-scale.js.map +1 -1
- package/dist/charthelpers/index.d.ts +1 -0
- package/dist/charthelpers/index.d.ts.map +1 -1
- package/dist/charthelpers/index.js +7 -1
- package/dist/charthelpers/index.js.map +1 -1
- package/dist/charthelpers/range-labels.d.ts +55 -0
- package/dist/charthelpers/range-labels.d.ts.map +1 -0
- package/dist/charthelpers/range-labels.js +36 -0
- package/dist/charthelpers/range-labels.js.map +1 -0
- package/dist/components/alert-button/alert-button.css.js +13 -21
- package/dist/components/alert-button/alert-button.css.js.map +1 -1
- package/dist/components/alert-button/alert-button.d.ts +9 -4
- package/dist/components/alert-button/alert-button.d.ts.map +1 -1
- package/dist/components/alert-button/alert-button.js +24 -15
- package/dist/components/alert-button/alert-button.js.map +1 -1
- package/dist/components/alert-frame/alert-frame.css.js +109 -33
- package/dist/components/alert-frame/alert-frame.css.js.map +1 -1
- package/dist/components/alert-frame/alert-frame.d.ts +34 -7
- package/dist/components/alert-frame/alert-frame.d.ts.map +1 -1
- package/dist/components/alert-frame/alert-frame.js +134 -30
- package/dist/components/alert-frame/alert-frame.js.map +1 -1
- package/dist/components/alert-icon/alert-icon.d.ts +10 -7
- package/dist/components/alert-icon/alert-icon.d.ts.map +1 -1
- package/dist/components/alert-icon/alert-icon.js +37 -60
- package/dist/components/alert-icon/alert-icon.js.map +1 -1
- package/dist/components/alert-menu-item/alert-menu-item.css.js +1 -4
- package/dist/components/alert-menu-item/alert-menu-item.css.js.map +1 -1
- package/dist/components/date-item/date-item.css.js +3 -3
- package/dist/components/keyboard-full/keyboard-full.css.js +4 -4
- package/dist/components/message-menu-item/message-menu-item.css.js +2 -6
- package/dist/components/message-menu-item/message-menu-item.css.js.map +1 -1
- package/dist/components/sequence-toolbar/sequence-toolbar.css.js +1 -2
- package/dist/components/sequence-toolbar/sequence-toolbar.css.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts +6 -0
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.js +8 -0
- package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
- package/dist/navigation-instruments/instrument-field/instrument-field.css.js +2 -2
- package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
- package/dist/navigation-instruments/readout/readout.js +2 -1
- package/dist/navigation-instruments/readout/readout.js.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js +2 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
- package/dist/openbridge.css +19 -4
- package/dist/palettes/blinking.d.ts +14 -4
- package/dist/palettes/blinking.d.ts.map +1 -1
- package/dist/palettes/blinking.js +41 -35
- package/dist/palettes/blinking.js.map +1 -1
- package/dist/palettes/flashing-controller.d.ts +21 -0
- package/dist/palettes/flashing-controller.d.ts.map +1 -0
- package/dist/palettes/flashing-controller.js +45 -0
- package/dist/palettes/flashing-controller.js.map +1 -0
- package/dist/svghelpers/index.d.ts +1 -0
- package/dist/svghelpers/index.d.ts.map +1 -1
- package/dist/svghelpers/index.js +2 -0
- package/dist/svghelpers/index.js.map +1 -1
- package/dist/svghelpers/rounded-rect.d.ts +15 -0
- package/dist/svghelpers/rounded-rect.d.ts.map +1 -0
- package/dist/svghelpers/rounded-rect.js +29 -0
- package/dist/svghelpers/rounded-rect.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/script/check-css-variables.ts +2 -15
- package/src/alert-severity.spec.ts +56 -0
- package/src/alert-severity.ts +45 -43
- package/src/bars-graphs/area-graph/area-graph.stories.ts +235 -1
- package/src/bars-graphs/line-graph/line-graph.stories.ts +24 -0
- package/src/building-blocks/bar-horizontal/bar-horizontal.ts +7 -0
- package/src/building-blocks/bar-vertical/bar-vertical.ts +7 -0
- package/src/building-blocks/chart-line/chart-line-base.spec.ts +767 -0
- package/src/building-blocks/chart-line/chart-line-base.stories.ts +124 -0
- package/src/building-blocks/chart-line/chart-line-base.ts +671 -97
- package/src/building-blocks/chart-line/chart-sizing-battleground.stories.ts +17 -0
- package/src/building-blocks/external-scale/external-scale.spec.ts +154 -0
- package/src/building-blocks/external-scale/external-scale.ts +78 -20
- package/src/charthelpers/index.ts +1 -0
- package/src/charthelpers/range-labels.spec.ts +46 -0
- package/src/charthelpers/range-labels.ts +93 -0
- package/src/components/alert-button/alert-button.css +10 -23
- package/src/components/alert-button/alert-button.spec.ts +56 -15
- package/src/components/alert-button/alert-button.stories.ts +5 -1
- package/src/components/alert-button/alert-button.ts +33 -24
- package/src/components/alert-frame/alert-flash-scenario.stories.ts +669 -0
- package/src/components/alert-frame/alert-frame.css +106 -33
- package/src/components/alert-frame/alert-frame.spec.ts +176 -17
- package/src/components/alert-frame/alert-frame.stories.ts +61 -0
- package/src/components/alert-frame/alert-frame.ts +179 -41
- package/src/components/alert-icon/alert-icon.spec.ts +82 -27
- package/src/components/alert-icon/alert-icon.stories.ts +7 -1
- package/src/components/alert-icon/alert-icon.ts +48 -74
- package/src/components/alert-menu-item/alert-menu-item.css +1 -4
- package/src/components/date-item/date-item.css +3 -3
- package/src/components/keyboard-full/keyboard-full.css +4 -4
- package/src/components/message-menu-item/message-menu-item.css +2 -6
- package/src/components/sequence-toolbar/sequence-toolbar.css +1 -2
- package/src/navigation-instruments/gauge-trend/gauge-trend.stories.ts +29 -0
- package/src/navigation-instruments/gauge-trend/gauge-trend.ts +11 -0
- package/src/navigation-instruments/instrument-field/instrument-field.css +2 -2
- package/src/navigation-instruments/readout/readout.ts +2 -1
- package/src/navigation-instruments/readout-list-item/readout-list-item.ts +2 -1
- package/src/palettes/blinking.spec.ts +83 -0
- package/src/palettes/blinking.ts +47 -35
- package/src/palettes/flashing-controller.ts +62 -0
- package/src/palettes/manual.css +18 -4
- package/src/svghelpers/index.ts +1 -0
- package/src/svghelpers/rounded-rect.spec.ts +47 -0
- package/src/svghelpers/rounded-rect.ts +42 -0
- package/src/types.ts +20 -0
|
@@ -117,6 +117,23 @@ const SUBJECTS: Subject[] = [
|
|
|
117
117
|
.primaryTickmarkInterval=${25}
|
|
118
118
|
></obc-gauge-trend>`,
|
|
119
119
|
},
|
|
120
|
+
{
|
|
121
|
+
label: 'gauge-trend + range labels',
|
|
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
|
+
.hasScale=${true}
|
|
134
|
+
.rangeLabels=${'xy'}
|
|
135
|
+
></obc-gauge-trend>`,
|
|
136
|
+
},
|
|
120
137
|
{
|
|
121
138
|
label: 'gauge-trend + advice + setpoint',
|
|
122
139
|
contract: SizingContract.fills,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {describe, it, expect, vi, afterEach} from 'vitest';
|
|
2
|
+
import {render, svg, type SVGTemplateResult} from 'lit';
|
|
3
|
+
import {
|
|
4
|
+
renderExternalScale,
|
|
5
|
+
ExternalScaleOrientation,
|
|
6
|
+
ExternalScaleSide,
|
|
7
|
+
ScaleType,
|
|
8
|
+
FillMode,
|
|
9
|
+
AdvicePosition,
|
|
10
|
+
EXTERNAL_SCALE_MAX_TICKS,
|
|
11
|
+
type ExternalScaleConfig,
|
|
12
|
+
} from './external-scale.js';
|
|
13
|
+
import {
|
|
14
|
+
FrameStyle,
|
|
15
|
+
InstrumentState,
|
|
16
|
+
Priority,
|
|
17
|
+
} from '../../navigation-instruments/types.js';
|
|
18
|
+
|
|
19
|
+
const bottomScale = (
|
|
20
|
+
overrides: Partial<ExternalScaleConfig>
|
|
21
|
+
): ExternalScaleConfig => ({
|
|
22
|
+
orientation: ExternalScaleOrientation.horizontal,
|
|
23
|
+
side: ExternalScaleSide.bottom,
|
|
24
|
+
length: 600,
|
|
25
|
+
paddingStart: 32,
|
|
26
|
+
paddingEnd: 32,
|
|
27
|
+
minValue: 0,
|
|
28
|
+
maxValue: 100,
|
|
29
|
+
hasBar: false,
|
|
30
|
+
hasScale: true,
|
|
31
|
+
labels: true,
|
|
32
|
+
scaleBackground: false,
|
|
33
|
+
barThickness: 24,
|
|
34
|
+
tickThickness: 24,
|
|
35
|
+
labelThickness: 60,
|
|
36
|
+
mainTickmarks: [],
|
|
37
|
+
scaleType: ScaleType.regular,
|
|
38
|
+
frameStyle: FrameStyle.regular,
|
|
39
|
+
priority: Priority.regular,
|
|
40
|
+
fillMode: FillMode.fill,
|
|
41
|
+
atSetpoint: false,
|
|
42
|
+
autoAtSetpoint: true,
|
|
43
|
+
autoAtSetpointDeadband: 1,
|
|
44
|
+
setpointAtZeroDeadband: 0.5,
|
|
45
|
+
state: InstrumentState.active,
|
|
46
|
+
advicePosition: AdvicePosition.inner,
|
|
47
|
+
...overrides,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// A twelve-minute window in epoch milliseconds — what a slotted scale
|
|
51
|
+
// receives from a chart on a time axis.
|
|
52
|
+
const TWELVE_MINUTES_MS = 12 * 60_000;
|
|
53
|
+
|
|
54
|
+
describe('tick ladder density guard (#1219)', () => {
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.restoreAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('drops a ladder that would exceed the tick cap instead of building it', () => {
|
|
60
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
61
|
+
const parts = renderExternalScale(
|
|
62
|
+
bottomScale({
|
|
63
|
+
minValue: 0,
|
|
64
|
+
maxValue: TWELVE_MINUTES_MS,
|
|
65
|
+
primaryTickmarkInterval: 2,
|
|
66
|
+
secondaryTickmarkInterval: 1,
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
expect(parts.tickmarks.length).toBeLessThan(EXTERNAL_SCALE_MAX_TICKS);
|
|
70
|
+
expect(parts.labels).toEqual([]);
|
|
71
|
+
expect(warn).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('counts both endpoints: a range of exactly the cap in steps is one over', () => {
|
|
75
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
76
|
+
const parts = renderExternalScale(
|
|
77
|
+
bottomScale({
|
|
78
|
+
minValue: 0,
|
|
79
|
+
maxValue: EXTERNAL_SCALE_MAX_TICKS,
|
|
80
|
+
primaryTickmarkInterval: 1,
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
expect(parts.labels).toEqual([]);
|
|
84
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('keeps a ladder whose count is under the cap', () => {
|
|
88
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
89
|
+
const parts = renderExternalScale(
|
|
90
|
+
bottomScale({
|
|
91
|
+
minValue: 0,
|
|
92
|
+
maxValue: TWELVE_MINUTES_MS,
|
|
93
|
+
primaryTickmarkInterval: 2 * 60_000,
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
// 0, 2 … 12 minutes: seven labels.
|
|
97
|
+
expect(parts.labels).toHaveLength(7);
|
|
98
|
+
expect(parts.tickmarks.length).toBeGreaterThan(0);
|
|
99
|
+
expect(warn).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** Renders one label fragment into a detached <svg> and returns its <text>. */
|
|
104
|
+
function labelElement(label: SVGTemplateResult): SVGTextElement {
|
|
105
|
+
const host = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
106
|
+
render(svg`${label}`, host);
|
|
107
|
+
return host.querySelector('text')!;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const labelText = (label: SVGTemplateResult) =>
|
|
111
|
+
labelElement(label).textContent?.trim();
|
|
112
|
+
const labelAttr = (label: SVGTemplateResult, name: string) =>
|
|
113
|
+
labelElement(label).getAttribute(name);
|
|
114
|
+
|
|
115
|
+
describe('main tickmark labels (#1191)', () => {
|
|
116
|
+
it('labels [min, 0, max] instead of the interval ladder', () => {
|
|
117
|
+
const parts = renderExternalScale(
|
|
118
|
+
bottomScale({
|
|
119
|
+
minValue: -50,
|
|
120
|
+
maxValue: 100,
|
|
121
|
+
primaryTickmarkInterval: 10,
|
|
122
|
+
mainTickmarkLabels: true,
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
expect(parts.labels.map(labelText)).toEqual(['-50', '0', '100']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('clamps the end labels inward on a horizontal scale', () => {
|
|
129
|
+
const parts = renderExternalScale(
|
|
130
|
+
bottomScale({minValue: 0, maxValue: 100, mainTickmarkLabels: true})
|
|
131
|
+
);
|
|
132
|
+
expect(parts.labels.map((l) => labelAttr(l, 'text-anchor'))).toEqual([
|
|
133
|
+
'start',
|
|
134
|
+
'end',
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('clamps the end labels inward on a vertical scale', () => {
|
|
139
|
+
const parts = renderExternalScale(
|
|
140
|
+
bottomScale({
|
|
141
|
+
orientation: ExternalScaleOrientation.vertical,
|
|
142
|
+
side: ExternalScaleSide.left,
|
|
143
|
+
minValue: 0,
|
|
144
|
+
maxValue: 100,
|
|
145
|
+
mainTickmarkLabels: true,
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
// min sits at the bottom and hangs upward, max at the top hangs downward.
|
|
149
|
+
expect(parts.labels.map((l) => labelAttr(l, 'dominant-baseline'))).toEqual([
|
|
150
|
+
'auto',
|
|
151
|
+
'hanging',
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -424,6 +424,11 @@ export interface ExternalScaleConfig {
|
|
|
424
424
|
hasScale: boolean;
|
|
425
425
|
/** Show labels at primary tickmark intervals. */
|
|
426
426
|
labels?: boolean;
|
|
427
|
+
/**
|
|
428
|
+
* Label the main tickmarks (`mainTickmarks`, or min / 0 / max) instead of
|
|
429
|
+
* the primary interval ladder. For scales too short for a ladder.
|
|
430
|
+
*/
|
|
431
|
+
mainTickmarkLabels?: boolean;
|
|
427
432
|
/** Show bar. */
|
|
428
433
|
hasBar: boolean;
|
|
429
434
|
/** Show background behind the scale tickmarks. */
|
|
@@ -932,6 +937,50 @@ function rangeIncludesZero(minValue: number, maxValue: number): boolean {
|
|
|
932
937
|
return minValue <= 0 && maxValue >= 0;
|
|
933
938
|
}
|
|
934
939
|
|
|
940
|
+
/** The main tickmark values inside the range, ascending, without repeats. */
|
|
941
|
+
function resolveMainTickmarkValues(
|
|
942
|
+
config: Pick<ExternalScaleConfig, 'mainTickmarks' | 'minValue' | 'maxValue'>
|
|
943
|
+
): number[] {
|
|
944
|
+
const source = config.mainTickmarks?.length
|
|
945
|
+
? config.mainTickmarks
|
|
946
|
+
: [config.minValue, 0, config.maxValue];
|
|
947
|
+
return [...new Set(source)]
|
|
948
|
+
.filter((v) => v >= config.minValue && v <= config.maxValue)
|
|
949
|
+
.sort((a, b) => a - b);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Ticks one ladder (primary, secondary, tertiary or labels) may hold. A
|
|
954
|
+
* denser ladder cannot be read on any scale length, and building it would
|
|
955
|
+
* exhaust the call stack.
|
|
956
|
+
*/
|
|
957
|
+
export const EXTERNAL_SCALE_MAX_TICKS = 1000;
|
|
958
|
+
|
|
959
|
+
// Bounded: a scale re-renders on every property change, and a live range
|
|
960
|
+
// that stays dense would otherwise grow this without limit.
|
|
961
|
+
const DENSE_LADDER_WARNINGS_KEPT = 64;
|
|
962
|
+
const warnedDenseLadders = new Set<string>();
|
|
963
|
+
|
|
964
|
+
/** True when `interval` would put more than the cap on the range; warns once per pair. */
|
|
965
|
+
function isLadderTooDense(
|
|
966
|
+
config: Pick<ExternalScaleConfig, 'minValue' | 'maxValue'>,
|
|
967
|
+
interval: number
|
|
968
|
+
): boolean {
|
|
969
|
+
const count = Math.floor((config.maxValue - config.minValue) / interval) + 1;
|
|
970
|
+
if (!(count > EXTERNAL_SCALE_MAX_TICKS)) return false;
|
|
971
|
+
const key = `${config.minValue}/${config.maxValue}/${interval}`;
|
|
972
|
+
if (!warnedDenseLadders.has(key)) {
|
|
973
|
+
if (warnedDenseLadders.size >= DENSE_LADDER_WARNINGS_KEPT) {
|
|
974
|
+
warnedDenseLadders.clear();
|
|
975
|
+
}
|
|
976
|
+
warnedDenseLadders.add(key);
|
|
977
|
+
console.warn(
|
|
978
|
+
`[external-scale] tick interval ${interval} over the range ${config.minValue}…${config.maxValue} is ${count} ticks; the ladder is not drawn (limit ${EXTERNAL_SCALE_MAX_TICKS}). A range in epoch milliseconds needs an interval in milliseconds.`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
|
|
935
984
|
function calculateAtSetpoint(config: ExternalScaleConfig): boolean {
|
|
936
985
|
const isTouching = config.touching ?? false;
|
|
937
986
|
return computeAtSetpoint({
|
|
@@ -1207,6 +1256,7 @@ function generateTickmarksAtInterval(
|
|
|
1207
1256
|
const values: number[] = [];
|
|
1208
1257
|
|
|
1209
1258
|
if (interval <= 0 || !Number.isFinite(interval)) return {svgs, values};
|
|
1259
|
+
if (isLadderTooDense(config, interval)) return {svgs, values};
|
|
1210
1260
|
|
|
1211
1261
|
const includesZero = rangeIncludesZero(config.minValue, config.maxValue);
|
|
1212
1262
|
|
|
@@ -1271,16 +1321,7 @@ function generateTickmarks(config: ExternalScaleConfig): SVGTemplateResult[] {
|
|
|
1271
1321
|
const mainLen = config.frameStyle === 'flat' ? main + 4 : main;
|
|
1272
1322
|
const dirLen = isOutwardPositive(config) ? mainLen : -mainLen;
|
|
1273
1323
|
|
|
1274
|
-
|
|
1275
|
-
const mainTickValues =
|
|
1276
|
-
config.mainTickmarks.length > 0
|
|
1277
|
-
? config.mainTickmarks
|
|
1278
|
-
: [config.minValue, 0, config.maxValue];
|
|
1279
|
-
|
|
1280
|
-
for (const value of mainTickValues) {
|
|
1281
|
-
// Skip if outside range
|
|
1282
|
-
if (value < config.minValue || value > config.maxValue) continue;
|
|
1283
|
-
|
|
1324
|
+
for (const value of resolveMainTickmarkValues(config)) {
|
|
1284
1325
|
// Skip min/max tickmarks when scaleBackground is enabled (they align with the background edges)
|
|
1285
1326
|
if (
|
|
1286
1327
|
config.scaleBackground &&
|
|
@@ -1347,10 +1388,7 @@ function generateTickmarks(config: ExternalScaleConfig): SVGTemplateResult[] {
|
|
|
1347
1388
|
}
|
|
1348
1389
|
|
|
1349
1390
|
function generateLabels(config: ExternalScaleConfig): SVGTemplateResult[] {
|
|
1350
|
-
if (!config.labels
|
|
1351
|
-
|
|
1352
|
-
const interval = config.primaryTickmarkInterval;
|
|
1353
|
-
if (interval <= 0 || !Number.isFinite(interval)) return [];
|
|
1391
|
+
if (!config.labels) return [];
|
|
1354
1392
|
|
|
1355
1393
|
const fontFamily = 'var(--font-family-main)';
|
|
1356
1394
|
const fontColor = 'var(--instrument-tick-mark-label-secondary-color)';
|
|
@@ -1365,18 +1403,17 @@ function generateLabels(config: ExternalScaleConfig): SVGTemplateResult[] {
|
|
|
1365
1403
|
? base + (config.hasScale ? effectiveTickThickness : 0) + labelGap()
|
|
1366
1404
|
: base - (config.hasScale ? effectiveTickThickness : 0) - labelGap();
|
|
1367
1405
|
|
|
1368
|
-
const includesZero = rangeIncludesZero(config.minValue, config.maxValue);
|
|
1369
|
-
|
|
1370
1406
|
const labels: SVGTemplateResult[] = [];
|
|
1371
1407
|
|
|
1372
|
-
const push = (v: number) => {
|
|
1408
|
+
const push = (v: number, edge?: {anchor: string; baseline: string}) => {
|
|
1373
1409
|
const main = valueToMainAxis(config, v);
|
|
1374
1410
|
if (isVertical(config)) {
|
|
1375
1411
|
const x = labelPos;
|
|
1376
1412
|
const y = main;
|
|
1377
1413
|
const anchor = isOutwardPositive(config) ? 'start' : 'end';
|
|
1414
|
+
const baseline = edge?.baseline ?? 'middle';
|
|
1378
1415
|
labels.push(
|
|
1379
|
-
svg`<text x=${x} y=${y} text-anchor=${anchor} dominant-baseline
|
|
1416
|
+
svg`<text x=${x} y=${y} text-anchor=${anchor} dominant-baseline=${baseline} font-family=${fontFamily} style="font-size: ${fontSize}" fill=${fontColor}>${v}</text>`
|
|
1380
1417
|
);
|
|
1381
1418
|
return;
|
|
1382
1419
|
}
|
|
@@ -1384,12 +1421,33 @@ function generateLabels(config: ExternalScaleConfig): SVGTemplateResult[] {
|
|
|
1384
1421
|
const y = labelPos;
|
|
1385
1422
|
const x = main;
|
|
1386
1423
|
const baseline = isOutwardPositive(config) ? 'hanging' : 'auto';
|
|
1424
|
+
const anchor = edge?.anchor ?? 'middle';
|
|
1387
1425
|
labels.push(
|
|
1388
|
-
svg`<text x=${x} y=${y} text-anchor
|
|
1426
|
+
svg`<text x=${x} y=${y} text-anchor=${anchor} dominant-baseline=${baseline} font-family=${fontFamily} style="font-size: ${fontSize}" fill=${fontColor}>${v}</text>`
|
|
1389
1427
|
);
|
|
1390
1428
|
};
|
|
1391
1429
|
|
|
1392
|
-
if (
|
|
1430
|
+
if (config.mainTickmarkLabels) {
|
|
1431
|
+
// The end labels turn inward so they stay inside the drawing length.
|
|
1432
|
+
for (const v of resolveMainTickmarkValues(config)) {
|
|
1433
|
+
const edge =
|
|
1434
|
+
v === config.minValue
|
|
1435
|
+
? {anchor: 'start', baseline: 'auto'}
|
|
1436
|
+
: v === config.maxValue
|
|
1437
|
+
? {anchor: 'end', baseline: 'hanging'}
|
|
1438
|
+
: undefined;
|
|
1439
|
+
push(v, edge);
|
|
1440
|
+
}
|
|
1441
|
+
return labels;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const interval = config.primaryTickmarkInterval;
|
|
1445
|
+
if (interval === undefined || interval <= 0 || !Number.isFinite(interval)) {
|
|
1446
|
+
return [];
|
|
1447
|
+
}
|
|
1448
|
+
if (isLadderTooDense(config, interval)) return [];
|
|
1449
|
+
|
|
1450
|
+
if (rangeIncludesZero(config.minValue, config.maxValue)) {
|
|
1393
1451
|
for (let v = 0; v <= config.maxValue; v += interval) push(v);
|
|
1394
1452
|
for (let v = -interval; v >= config.minValue; v -= interval) push(v);
|
|
1395
1453
|
} else {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {describe, it, expect} from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
formatRangeValue,
|
|
4
|
+
yRangeLabelValues,
|
|
5
|
+
measureRangeLabelGutters,
|
|
6
|
+
rangeLabelLineHeight,
|
|
7
|
+
} from './range-labels.js';
|
|
8
|
+
|
|
9
|
+
const font = {family: 'sans-serif', sizePx: 12, weight: '400'};
|
|
10
|
+
|
|
11
|
+
describe('range label values', () => {
|
|
12
|
+
it('keeps integers integer and non-integers at one decimal', () => {
|
|
13
|
+
expect(formatRangeValue(7, 3, 7)).toBe('7');
|
|
14
|
+
expect(formatRangeValue(3.5, 3.5, 7)).toBe('3.5');
|
|
15
|
+
expect(formatRangeValue(7, 3.5, 7)).toBe('7.0');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('adds 0 only when strictly inside the range', () => {
|
|
19
|
+
expect(yRangeLabelValues(-100, 100)).toEqual([-100, 0, 100]);
|
|
20
|
+
expect(yRangeLabelValues(0, 100)).toEqual([0, 100]);
|
|
21
|
+
expect(yRangeLabelValues(3, 7)).toEqual([3, 7]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('range label gutters', () => {
|
|
26
|
+
const ctx = document.createElement('canvas').getContext('2d')!;
|
|
27
|
+
|
|
28
|
+
it('reserves the widest y label plus the gap, and one line for x', () => {
|
|
29
|
+
const g = measureRangeLabelGutters(
|
|
30
|
+
ctx,
|
|
31
|
+
font,
|
|
32
|
+
['3', '1000'],
|
|
33
|
+
['Jan', 'Dec']
|
|
34
|
+
);
|
|
35
|
+
ctx.font = '400 12px sans-serif';
|
|
36
|
+
expect(g.side).toBe(Math.ceil(ctx.measureText('1000').width) + 8);
|
|
37
|
+
expect(g.bottom).toBe(rangeLabelLineHeight(font) + 4);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('reserves nothing for an axis without labels', () => {
|
|
41
|
+
expect(measureRangeLabelGutters(ctx, font, [], [])).toEqual({
|
|
42
|
+
side: 0,
|
|
43
|
+
bottom: 0,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Min / max labels for charts below the label threshold: which values to
|
|
3
|
+
* show and how much gutter their text needs. Pure; the chart measures on
|
|
4
|
+
* its own canvas and draws with its own plugin.
|
|
5
|
+
*/
|
|
6
|
+
export interface RangeLabelFont {
|
|
7
|
+
family: string;
|
|
8
|
+
sizePx: number;
|
|
9
|
+
weight: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param font - Family, size in px and weight of the label text
|
|
14
|
+
* @returns The canvas `font` shorthand for that text
|
|
15
|
+
*/
|
|
16
|
+
export function rangeLabelFontString(font: RangeLabelFont): string {
|
|
17
|
+
return `${font.weight} ${font.sizePx}px ${font.family}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Chart.js' own line-height ratio, so the gutter matches its tick labels.
|
|
22
|
+
*
|
|
23
|
+
* @param font - The label font
|
|
24
|
+
* @returns One text line in px
|
|
25
|
+
*/
|
|
26
|
+
export function rangeLabelLineHeight(font: RangeLabelFont): number {
|
|
27
|
+
return Math.round(font.sizePx * 1.2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One decimal only when an end of the range needs it; both ends share it.
|
|
32
|
+
*
|
|
33
|
+
* @param value - The value to print
|
|
34
|
+
* @param min - Lower end of the range the value belongs to
|
|
35
|
+
* @param max - Upper end of the range
|
|
36
|
+
* @returns The label text
|
|
37
|
+
*/
|
|
38
|
+
export function formatRangeValue(
|
|
39
|
+
value: number,
|
|
40
|
+
min: number,
|
|
41
|
+
max: number
|
|
42
|
+
): string {
|
|
43
|
+
const decimals = Number.isInteger(min) && Number.isInteger(max) ? 0 : 1;
|
|
44
|
+
return value.toFixed(decimals);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param min - Lower end of the y range
|
|
49
|
+
* @param max - Upper end of the y range
|
|
50
|
+
* @returns `[min, 0, max]`, with 0 only strictly inside the range
|
|
51
|
+
*/
|
|
52
|
+
export function yRangeLabelValues(min: number, max: number): number[] {
|
|
53
|
+
return min < 0 && max > 0 ? [min, 0, max] : [min, max];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RangeLabelGutters {
|
|
57
|
+
/** Room on the y-axis side: the widest label plus the gap. */
|
|
58
|
+
side: number;
|
|
59
|
+
/** Room below the plot: one line plus the gap. */
|
|
60
|
+
bottom: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const SIDE_GAP = 8;
|
|
64
|
+
const BOTTOM_GAP = 4;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The x labels sit in a band one line tall, so only their count matters;
|
|
68
|
+
* their width is not measured.
|
|
69
|
+
*
|
|
70
|
+
* @param ctx - A 2d context to measure on; its font is restored afterwards
|
|
71
|
+
* @param font - The label font
|
|
72
|
+
* @param yTexts - Labels for the y side, empty when that side has none
|
|
73
|
+
* @param xTexts - Labels for the bottom, empty when the x side has none
|
|
74
|
+
* @returns Gutter sizes in px; 0 for a side without labels
|
|
75
|
+
*/
|
|
76
|
+
export function measureRangeLabelGutters(
|
|
77
|
+
ctx: CanvasRenderingContext2D,
|
|
78
|
+
font: RangeLabelFont,
|
|
79
|
+
yTexts: string[],
|
|
80
|
+
xTexts: string[]
|
|
81
|
+
): RangeLabelGutters {
|
|
82
|
+
ctx.save();
|
|
83
|
+
ctx.font = rangeLabelFontString(font);
|
|
84
|
+
const widest = yTexts.reduce(
|
|
85
|
+
(width, text) => Math.max(width, ctx.measureText(text).width),
|
|
86
|
+
0
|
|
87
|
+
);
|
|
88
|
+
ctx.restore();
|
|
89
|
+
return {
|
|
90
|
+
side: yTexts.length ? Math.ceil(widest) + SIDE_GAP : 0,
|
|
91
|
+
bottom: xTexts.length ? rangeLabelLineHeight(font) + BOTTOM_GAP : 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -167,53 +167,40 @@ button {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
.
|
|
171
|
-
.blinking.alert-type-level-medium .alert-button {
|
|
170
|
+
.flash-fast .alert-button {
|
|
172
171
|
.visible-wrapper {
|
|
173
|
-
opacity: var(--
|
|
172
|
+
opacity: var(--flash-fast-on);
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
.blink {
|
|
177
|
-
opacity: var(--
|
|
176
|
+
opacity: var(--flash-fast-off);
|
|
178
177
|
}
|
|
179
178
|
}
|
|
180
179
|
|
|
181
|
-
.
|
|
180
|
+
.flash-slow .alert-button {
|
|
182
181
|
.visible-wrapper {
|
|
183
|
-
opacity: var(--
|
|
182
|
+
opacity: var(--flash-slow-on);
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
.blink {
|
|
187
|
-
opacity: var(--
|
|
186
|
+
opacity: var(--flash-slow-off);
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
189
|
|
|
191
|
-
.
|
|
192
|
-
.blinking.alert-type-level-high .alert-button {
|
|
190
|
+
.flash-very-slow .alert-button {
|
|
193
191
|
.visible-wrapper {
|
|
194
|
-
opacity: var(--
|
|
192
|
+
opacity: var(--flash-very-slow-on);
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
.blink {
|
|
198
|
-
opacity: var(--
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.blinking.alert-type-caution .alert-button,
|
|
203
|
-
.blinking.alert-type-level-low .alert-button {
|
|
204
|
-
.visible-wrapper {
|
|
205
|
-
opacity: var(--low-blink-on);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
.blink {
|
|
209
|
-
opacity: var(--low-blink-off);
|
|
196
|
+
opacity: var(--flash-very-slow-off);
|
|
210
197
|
}
|
|
211
198
|
}
|
|
212
199
|
|
|
213
200
|
.counter .alert-button .visible-wrapper,
|
|
214
201
|
.counter .alert-button .blink {
|
|
215
202
|
padding: 0px var(--ui-components-button-padding-horizontal);
|
|
216
|
-
gap: var(--app-components-
|
|
203
|
+
gap: var(--app-components-alert-button-item-counter-spacing);
|
|
217
204
|
}
|
|
218
205
|
|
|
219
206
|
.alert-type-alarm {
|
|
@@ -3,24 +3,67 @@ import './alert-button.js';
|
|
|
3
3
|
import {ObcAlertButton} from './alert-button.js';
|
|
4
4
|
import {render} from 'vitest-browser-lit';
|
|
5
5
|
import {html} from 'lit';
|
|
6
|
+
import {AlertType, FlashingSpeed} from '../../types.js';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
function durations(el: HTMLElement): number[] {
|
|
9
|
+
return el
|
|
10
|
+
.getAnimations()
|
|
11
|
+
.map((a) => (a.effect as KeyframeEffect).getTiming().duration as number);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('obc-alert-button flashing lifecycle', () => {
|
|
15
|
+
async function setup(
|
|
16
|
+
props: Partial<
|
|
17
|
+
Pick<
|
|
18
|
+
ObcAlertButton,
|
|
19
|
+
'nAlerts' | 'alertType' | 'blinking' | 'flashingSpeed'
|
|
20
|
+
>
|
|
21
|
+
> = {}
|
|
22
|
+
) {
|
|
23
|
+
const screen = render(
|
|
24
|
+
html`<obc-alert-button
|
|
25
|
+
.nAlerts=${props.nAlerts ?? 1}
|
|
26
|
+
.alertType=${props.alertType ?? AlertType.Alarm}
|
|
27
|
+
.blinking=${props.blinking ?? true}
|
|
28
|
+
.flashingSpeed=${props.flashingSpeed ?? FlashingSpeed.Default}
|
|
29
|
+
></obc-alert-button>`
|
|
30
|
+
);
|
|
31
|
+
const el = screen.container.querySelector(
|
|
11
32
|
'obc-alert-button'
|
|
12
33
|
) as ObcAlertButton;
|
|
13
34
|
await el.updateComplete;
|
|
14
35
|
return el;
|
|
15
36
|
}
|
|
16
37
|
|
|
17
|
-
it('
|
|
38
|
+
it('flashes fast for a blinking alarm button with alerts', async () => {
|
|
18
39
|
const el = await setup();
|
|
19
40
|
|
|
20
|
-
expect(el
|
|
41
|
+
expect(durations(el)).toEqual([800]);
|
|
42
|
+
expect(el.shadowRoot!.querySelector('.wrapper.flash-fast')).not.toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('flashes slow for a warning and never for caution', async () => {
|
|
46
|
+
expect(durations(await setup({alertType: AlertType.Warning}))).toEqual([
|
|
47
|
+
1600,
|
|
48
|
+
]);
|
|
49
|
+
expect(durations(await setup({alertType: AlertType.Caution}))).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('keeps blinking as the gate', async () => {
|
|
53
|
+
expect(durations(await setup({blinking: false}))).toEqual([]);
|
|
54
|
+
expect(durations(await setup({nAlerts: 0}))).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('honours an explicit flashingSpeed', async () => {
|
|
58
|
+
expect(
|
|
59
|
+
durations(await setup({flashingSpeed: FlashingSpeed.VerySlow}))
|
|
60
|
+
).toEqual([3200]);
|
|
61
|
+
expect(
|
|
62
|
+
durations(await setup({flashingSpeed: FlashingSpeed.Fixed}))
|
|
63
|
+
).toEqual([]);
|
|
21
64
|
});
|
|
22
65
|
|
|
23
|
-
it('cancels the
|
|
66
|
+
it('cancels the animation on disconnect', async () => {
|
|
24
67
|
const el = await setup();
|
|
25
68
|
|
|
26
69
|
el.parentElement!.removeChild(el);
|
|
@@ -28,31 +71,29 @@ describe('obc-alert-button blinking lifecycle', () => {
|
|
|
28
71
|
expect(el.getAnimations()).toHaveLength(0);
|
|
29
72
|
});
|
|
30
73
|
|
|
31
|
-
it('resumes
|
|
74
|
+
it('resumes flashing after disconnect and reconnect', async () => {
|
|
32
75
|
const el = await setup();
|
|
33
76
|
const parent = el.parentElement!;
|
|
34
|
-
const initial = el.getAnimations().length;
|
|
35
77
|
|
|
36
78
|
parent.removeChild(el);
|
|
37
79
|
expect(el.getAnimations()).toHaveLength(0);
|
|
38
80
|
|
|
39
81
|
// Reconnect without touching any property. firstUpdated() will not run
|
|
40
|
-
// again, so this only passes if
|
|
82
|
+
// again, so this only passes if the controller re-syncs on connect.
|
|
41
83
|
parent.appendChild(el);
|
|
42
84
|
await el.updateComplete;
|
|
43
85
|
|
|
44
|
-
expect(el
|
|
86
|
+
expect(durations(el)).toEqual([800]);
|
|
45
87
|
});
|
|
46
88
|
|
|
47
89
|
it('does not accumulate animations across repeated updates', async () => {
|
|
48
90
|
const el = await setup();
|
|
49
|
-
const initial = el.getAnimations().length;
|
|
50
91
|
|
|
51
|
-
el.
|
|
92
|
+
el.large = true;
|
|
52
93
|
await el.updateComplete;
|
|
53
|
-
el.
|
|
94
|
+
el.counter = true;
|
|
54
95
|
await el.updateComplete;
|
|
55
96
|
|
|
56
|
-
expect(el.getAnimations()).toHaveLength(
|
|
97
|
+
expect(el.getAnimations()).toHaveLength(1);
|
|
57
98
|
});
|
|
58
99
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/web-components-vite';
|
|
2
2
|
import {ObcAlertButton, ObcAlertButtonType} from './alert-button.js';
|
|
3
|
-
import {AlertType} from '../../types.js';
|
|
3
|
+
import {AlertType, FlashingSpeed} from '../../types.js';
|
|
4
4
|
import './alert-button.js';
|
|
5
5
|
import {widthDecorator} from '../../storybook-util.js';
|
|
6
6
|
|
|
@@ -27,6 +27,10 @@ const meta: Meta<typeof ObcAlertButton> = {
|
|
|
27
27
|
options: Object.values(ObcAlertButtonType),
|
|
28
28
|
control: {type: 'select'},
|
|
29
29
|
},
|
|
30
|
+
flashingSpeed: {
|
|
31
|
+
options: Object.values(FlashingSpeed),
|
|
32
|
+
control: {type: 'select'},
|
|
33
|
+
},
|
|
30
34
|
width: {control: {type: 'range', min: 64, max: 1028, step: 1}},
|
|
31
35
|
},
|
|
32
36
|
decorators: [widthDecorator],
|