@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.57 → 2.0.0-next.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +592 -239
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +292 -8
  4. package/dist/building-blocks/instrument-radial/instrument-radial.d.ts +10 -0
  5. package/dist/building-blocks/instrument-radial/instrument-radial.d.ts.map +1 -1
  6. package/dist/building-blocks/instrument-radial/instrument-radial.js +86 -21
  7. package/dist/building-blocks/instrument-radial/instrument-radial.js.map +1 -1
  8. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js +99 -0
  9. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js.map +1 -0
  10. package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts +42 -7
  11. package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts.map +1 -1
  12. package/dist/navigation-instruments/gauge-radial/gauge-radial.js +178 -31
  13. package/dist/navigation-instruments/gauge-radial/gauge-radial.js.map +1 -1
  14. package/dist/navigation-instruments/readout/readout.css.js +4 -0
  15. package/dist/navigation-instruments/readout/readout.css.js.map +1 -1
  16. package/dist/navigation-instruments/watch/tickmark.d.ts +2 -1
  17. package/dist/navigation-instruments/watch/tickmark.d.ts.map +1 -1
  18. package/dist/navigation-instruments/watch/tickmark.js +24 -4
  19. package/dist/navigation-instruments/watch/tickmark.js.map +1 -1
  20. package/dist/navigation-instruments/watch/watch.d.ts +23 -1
  21. package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
  22. package/dist/navigation-instruments/watch/watch.js +48 -20
  23. package/dist/navigation-instruments/watch/watch.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/building-blocks/instrument-radial/instrument-radial.stories.ts +21 -0
  26. package/src/building-blocks/instrument-radial/instrument-radial.ts +118 -21
  27. package/src/navigation-instruments/gauge-radial/gauge-radial.css +85 -0
  28. package/src/navigation-instruments/gauge-radial/gauge-radial.stories.ts +149 -7
  29. package/src/navigation-instruments/gauge-radial/gauge-radial.ts +212 -33
  30. package/src/navigation-instruments/readout/readout.css +4 -0
  31. package/src/navigation-instruments/watch/tickmark.ts +33 -6
  32. package/src/navigation-instruments/watch/watch.ts +60 -25
@@ -0,0 +1,85 @@
1
+ :host {
2
+ display: block;
3
+ width: 100%;
4
+ height: 100%;
5
+ }
6
+
7
+ /* Host takes each sector's crop aspect (see `sectorClips`): 270/90 square, 180
8
+ wide — so the dial is the same size at any width. */
9
+ :host([sector="270"]),
10
+ :host([sector="90-left"]),
11
+ :host([sector="90-right"]) {
12
+ height: auto;
13
+ aspect-ratio: 1;
14
+ }
15
+
16
+ /* 448 / 251 = the 180° crop's aspect (448 wide × 448·(1 − 44%) tall); keep in
17
+ sync with `sectorClips` bottom=44 in gauge-radial.ts. */
18
+ :host([sector="180"]) {
19
+ height: auto;
20
+ aspect-ratio: 448 / 251;
21
+ }
22
+
23
+ .gauge-radial-root {
24
+ /* Readout vertical placement per layout (% of the cropped host), tuned to the
25
+ watch geometry — re-check if the ring radii or the 448 box change. Which
26
+ layout uses which is in renderReadouts(). */
27
+ --readout-meta-top: 72%;
28
+ --readout-meta-top-needle: 63%;
29
+ --readout-meta-top-180: 60%;
30
+
31
+ position: relative;
32
+ width: 100%;
33
+ height: 100%;
34
+
35
+ .gauge-readout-value {
36
+ position: absolute;
37
+ left: 50%;
38
+ top: 50%;
39
+ transform: translate(-50%, -50%);
40
+ z-index: 1;
41
+ }
42
+
43
+ .gauge-readout-meta {
44
+ position: absolute;
45
+ left: 50%;
46
+ top: var(--readout-meta-top);
47
+ transform: translateX(-50%);
48
+ z-index: 1;
49
+ }
50
+
51
+ &.type-needle .gauge-readout-meta {
52
+ top: var(--readout-meta-top-needle);
53
+ }
54
+
55
+ &.sector-180 .gauge-readout-meta {
56
+ top: var(--readout-meta-top-180);
57
+ }
58
+
59
+ /* Nudge the corner readout inward (up + toward center) so it sits inside the
60
+ open quadrant rather than hugging the box corner. Symmetric for both
61
+ orientations — only the horizontal edge differs. */
62
+ &.sector-90-left .gauge-readout-meta,
63
+ &.sector-90-right .gauge-readout-meta {
64
+ --readout-90-inset: 6%;
65
+ bottom: var(--readout-90-inset);
66
+ top: auto;
67
+ transform: none;
68
+ }
69
+
70
+ &.sector-90-left .gauge-readout-meta {
71
+ left: auto;
72
+ right: var(--readout-90-inset);
73
+ }
74
+
75
+ &.sector-90-right .gauge-readout-meta {
76
+ left: var(--readout-90-inset);
77
+ right: auto;
78
+ }
79
+
80
+ /* 90-right readout sits in the bottom-left corner, so left-align the value
81
+ under the label (enhanced readouts right-align the value by default). */
82
+ &.sector-90-right .gauge-readout-meta::part(value-wrapper) {
83
+ justify-self: start;
84
+ }
85
+ }
@@ -1,33 +1,57 @@
1
1
  import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
- import {ObcGaugeRadial, ObcGaugeRadialType} from './gauge-radial.js';
2
+ import {
3
+ GaugeRadialSector,
4
+ ObcGaugeRadial,
5
+ ObcGaugeRadialType,
6
+ } from './gauge-radial.js';
3
7
  import './gauge-radial.js';
4
8
  import {widthDecorator} from '../../storybook-util.js';
5
9
  import {AdviceType} from '../watch/advice.js';
6
10
  import {TickmarkStyle} from '../watch/tickmark.js';
7
11
  import {InstrumentState, Priority} from '../types.js';
8
12
 
9
- const meta: Meta<typeof ObcGaugeRadial> = {
13
+ type GaugeRadialStoryArgs = Partial<ObcGaugeRadial> & {
14
+ width?: number;
15
+ height?: number;
16
+ };
17
+
18
+ const meta = {
10
19
  title: 'Instruments/Gauge Radial',
11
- tags: ['6.0'],
20
+ tags: ['autodocs', '6.0'],
12
21
  component: 'obc-gauge-radial',
13
22
  decorators: [widthDecorator],
14
23
  args: {
15
24
  width: 400,
16
25
  },
17
26
  argTypes: {
18
- state: {control: 'select', options: Object.values(InstrumentState)},
27
+ width: {control: {type: 'range', min: 100, max: 1000, step: 1}},
28
+ height: {control: {type: 'range', min: 100, max: 1000, step: 1}},
29
+ type: {control: 'select', options: Object.values(ObcGaugeRadialType)},
30
+ sector: {control: 'select', options: Object.values(GaugeRadialSector)},
19
31
  priority: {control: 'select', options: Object.values(Priority)},
20
- tickmarksInside: {control: 'boolean'},
32
+ state: {control: 'select', options: Object.values(InstrumentState)},
21
33
  tickmarkStyle: {
22
34
  control: 'select',
23
35
  options: Object.values(TickmarkStyle),
24
36
  },
25
37
  showLabels: {control: 'boolean'},
38
+ tickmarksInside: {control: 'boolean'},
39
+ showReadout: {control: 'boolean'},
40
+ value: {control: 'number'},
41
+ minValue: {control: 'number'},
42
+ maxValue: {control: 'number'},
43
+ primaryTickmarkInterval: {control: 'number'},
44
+ secondaryTickmarkInterval: {control: 'number'},
45
+ tertiaryTickmarkInterval: {control: 'number'},
46
+ fractionDigits: {control: 'number'},
47
+ label: {control: 'text'},
48
+ unit: {control: 'text'},
49
+ advices: {control: 'object'},
26
50
  },
27
- } satisfies Meta<ObcGaugeRadial>;
51
+ } satisfies Meta<GaugeRadialStoryArgs>;
28
52
 
29
53
  export default meta;
30
- type Story = StoryObj<ObcGaugeRadial>;
54
+ type Story = StoryObj<GaugeRadialStoryArgs>;
31
55
 
32
56
  export const Positive: Story = {
33
57
  args: {
@@ -128,6 +152,124 @@ export const IrregularRange: Story = {
128
152
  },
129
153
  };
130
154
 
155
+ const sectorStoryArgs = {
156
+ value: 50,
157
+ minValue: 0,
158
+ maxValue: 100,
159
+ type: ObcGaugeRadialType.filled,
160
+ showLabels: true,
161
+ } as const;
162
+
163
+ export const Sector270: Story = {
164
+ args: {
165
+ ...sectorStoryArgs,
166
+ sector: GaugeRadialSector.deg270,
167
+ },
168
+ };
169
+
170
+ export const Sector180: Story = {
171
+ args: {
172
+ ...sectorStoryArgs,
173
+ sector: GaugeRadialSector.deg180,
174
+ },
175
+ };
176
+
177
+ export const Sector180WithReadout: Story = {
178
+ args: {
179
+ ...sectorStoryArgs,
180
+ sector: GaugeRadialSector.deg180,
181
+ showReadout: true,
182
+ label: 'Label',
183
+ unit: 'unit',
184
+ value: 123,
185
+ },
186
+ };
187
+
188
+ // Regression for the width-derived "Max-min" end-label inset: wide labels on
189
+ // both ends (-250 / 1000). The old per-side literals (11/6) broke for these;
190
+ // min/max are interval multiples so the end labels sit on the primary grid.
191
+ export const Sector180WideEndLabels: Story = {
192
+ args: {
193
+ ...sectorStoryArgs,
194
+ sector: GaugeRadialSector.deg180,
195
+ tickmarksInside: true,
196
+ minValue: -250,
197
+ maxValue: 1000,
198
+ value: 500,
199
+ primaryTickmarkInterval: 250,
200
+ secondaryTickmarkInterval: 50,
201
+ },
202
+ };
203
+
204
+ export const Sector90Left: Story = {
205
+ args: {
206
+ ...sectorStoryArgs,
207
+ sector: GaugeRadialSector.deg90Left,
208
+ width: 200,
209
+ },
210
+ };
211
+
212
+ export const Sector90Right: Story = {
213
+ args: {
214
+ ...sectorStoryArgs,
215
+ sector: GaugeRadialSector.deg90Right,
216
+ width: 200,
217
+ },
218
+ };
219
+
220
+ export const Sector90LeftWithReadout: Story = {
221
+ args: {
222
+ ...sectorStoryArgs,
223
+ sector: GaugeRadialSector.deg90Left,
224
+ showReadout: true,
225
+ label: 'Label',
226
+ unit: 'unit',
227
+ value: 123,
228
+ width: 200,
229
+ },
230
+ };
231
+
232
+ export const Sector90RightWithReadout: Story = {
233
+ args: {
234
+ ...sectorStoryArgs,
235
+ sector: GaugeRadialSector.deg90Right,
236
+ showReadout: true,
237
+ label: 'Label',
238
+ unit: 'unit',
239
+ value: 123,
240
+ width: 200,
241
+ },
242
+ };
243
+
244
+ const readoutStoryArgs = {
245
+ value: 123,
246
+ minValue: 0,
247
+ maxValue: 100,
248
+ type: ObcGaugeRadialType.filled,
249
+ sector: GaugeRadialSector.deg270,
250
+ showReadout: true,
251
+ label: 'Label',
252
+ unit: 'unit',
253
+ } as const;
254
+
255
+ export const WithReadout: Story = {
256
+ args: readoutStoryArgs,
257
+ };
258
+
259
+ export const WithReadoutEnhanced: Story = {
260
+ args: {
261
+ ...readoutStoryArgs,
262
+ priority: Priority.enhanced,
263
+ },
264
+ };
265
+
266
+ export const WithReadoutNeedle: Story = {
267
+ args: {
268
+ ...readoutStoryArgs,
269
+ type: ObcGaugeRadialType.needle,
270
+ },
271
+ };
272
+
131
273
  export const WideAdviceSpan: Story = {
132
274
  args: {
133
275
  value: 320,
@@ -1,4 +1,6 @@
1
- import {LitElement, html} from 'lit';
1
+ import {LitElement, html, nothing, unsafeCSS, type TemplateResult} from 'lit';
2
+ import {classMap} from 'lit/directives/class-map.js';
3
+ import componentStyle from './gauge-radial.css?inline';
2
4
  import {customElement} from '../../decorator.js';
3
5
  import {property} from 'lit/decorators.js';
4
6
  import {AdviceType} from '../watch/advice.js';
@@ -6,6 +8,11 @@ import {InstrumentState, Priority} from '../types.js';
6
8
  import {SetpointMixin} from '../../svghelpers/setpoint-mixin.js';
7
9
  import '../../building-blocks/instrument-radial/instrument-radial.js';
8
10
  import {TickmarkStyle} from '../watch/tickmark.js';
11
+ import '../readout/readout.js';
12
+ import {
13
+ ReadoutStackVerticalAlignment,
14
+ ReadoutVariant,
15
+ } from '../readout/readout.js';
9
16
 
10
17
  export enum ObcGaugeRadialType {
11
18
  filled = 'filled',
@@ -13,6 +20,13 @@ export enum ObcGaugeRadialType {
13
20
  needle = 'needle',
14
21
  }
15
22
 
23
+ export enum GaugeRadialSector {
24
+ deg270 = '270',
25
+ deg180 = '180',
26
+ deg90Left = '90-left',
27
+ deg90Right = '90-right',
28
+ }
29
+
16
30
  export interface GaugeRadialAdvice {
17
31
  minValue: number;
18
32
  maxValue: number;
@@ -34,14 +48,17 @@ export interface GaugeRadialAdvice {
34
48
  *
35
49
  * - **Three display types**: `filled` (solid arc), `bar` (thinner arc), and
36
50
  * `needle` (pointer indicator) via the `type` property.
37
- * - **Full-range mapping**: The configured `minValue..maxValue` always spans
38
- * the full 270° sweep. Symmetric ranges still place `0` at 12 o'clock.
51
+ * - **Sector sweep**: `sector` selects the arc span (`270`, `180`, `90-left`, or `90-right`).
52
+ * The configured `minValue..maxValue` always spans the full sector. For the
53
+ * centered sectors (`270`, `180`) a symmetric range places `0` at 12 o'clock;
54
+ * for `90-left`/`90-right` the range midpoint sits at the middle of the quadrant.
39
55
  * - **Setpoint via mixin**: `setpoint`, `newSetpoint`, `touching`,
40
56
  * `autoAtSetpointDeadband`, `setpointOverride`, and all other setpoint
41
57
  * properties are provided by `SetpointMixin` and forwarded to the inner
42
58
  * `<obc-instrument-radial>`.
43
59
  * - **Advice zones**: Pass an array of {@link GaugeRadialAdvice} objects to
44
- * render caution/alert arcs on the gauge.
60
+ * render caution/alert arcs on the gauge. Not shown on the `90-left` /
61
+ * `90-right` sectors.
45
62
  *
46
63
  * ## Usage Guidelines
47
64
  *
@@ -50,6 +67,12 @@ export interface GaugeRadialAdvice {
50
67
  * - Provide `primaryTickmarkInterval` and `secondaryTickmarkInterval` to
51
68
  * control tickmark density.
52
69
  * - Enable `showLabels` to show numeric labels at primary tickmarks.
70
+ * - Enable `showReadout` with optional `label` and `unit`. Layout depends on `sector`
71
+ * and `type`: **270** filled/bar — value at center + a label-only `meta` row in
72
+ * the bottom gap (two separate readouts); **180** filled/bar — a single centered
73
+ * stack (value + label/unit) at the bottom; **270** needle — bottom stack;
74
+ * **180** needle — no readout; **90-left** / **90-right** filled/bar — corner
75
+ * readout in a square host; **90** needle — no readout.
53
76
  *
54
77
  * ## Best Practices
55
78
  *
@@ -66,7 +89,7 @@ export interface GaugeRadialAdvice {
66
89
  * minValue="0"
67
90
  * maxValue="100"
68
91
  * type="filled"
69
- * enhanced
92
+ * priority="enhanced"
70
93
  * showLabels
71
94
  * primaryTickmarkInterval="25"
72
95
  * secondaryTickmarkInterval="5"
@@ -98,47 +121,203 @@ export class ObcGaugeRadial extends SetpointMixin(LitElement) {
98
121
  @property({type: Boolean}) tickmarksInside: boolean = false;
99
122
  @property({type: String}) tickmarkStyle: TickmarkStyle =
100
123
  TickmarkStyle.regular;
124
+ /** Caution/alert arcs. Ignored on `sector: 90-left` / `90-right`. */
101
125
  @property({type: Array, attribute: false}) advices: GaugeRadialAdvice[] = [];
126
+ @property({type: String, reflect: true}) sector: GaugeRadialSector =
127
+ GaugeRadialSector.deg270;
128
+ @property({type: Boolean}) showReadout = false;
129
+ @property({type: String}) label = '';
130
+ @property({type: String}) unit = '';
131
+ @property({type: Number}) fractionDigits = 0;
132
+
133
+ private get sectorAngles(): {sweep: number; start: number} {
134
+ switch (this.sector) {
135
+ case GaugeRadialSector.deg180:
136
+ return {sweep: 180, start: -90};
137
+ case GaugeRadialSector.deg90Left:
138
+ return {sweep: 90, start: -90};
139
+ case GaugeRadialSector.deg90Right:
140
+ return {sweep: 90, start: 0};
141
+ case GaugeRadialSector.deg270:
142
+ default:
143
+ return {sweep: 270, start: -135};
144
+ }
145
+ }
102
146
 
103
- getAngle(v: number): number {
147
+ /**
148
+ * Per-edge crop (%) of the shared, origin-centered 448 SVG box each sector
149
+ * shows. All sectors render at the same natural scale, so the dial stays the
150
+ * same size; the sector only windows a different part (270 whole, 180 wide,
151
+ * 90 a quadrant).
152
+ */
153
+ private get sectorClips(): {
154
+ top: number;
155
+ bottom: number;
156
+ left: number;
157
+ right: number;
158
+ } {
159
+ switch (this.sector) {
160
+ case GaugeRadialSector.deg180:
161
+ return {top: 0, bottom: 44, left: 0, right: 0};
162
+ case GaugeRadialSector.deg90Left:
163
+ return {top: 0, bottom: 45, left: 0, right: 45};
164
+ case GaugeRadialSector.deg90Right:
165
+ return {top: 0, bottom: 45, left: 45, right: 0};
166
+ case GaugeRadialSector.deg270:
167
+ default:
168
+ return {top: 0, bottom: 0, left: 0, right: 0};
169
+ }
170
+ }
171
+
172
+ private get isSector90(): boolean {
173
+ return (
174
+ this.sector === GaugeRadialSector.deg90Left ||
175
+ this.sector === GaugeRadialSector.deg90Right
176
+ );
177
+ }
178
+
179
+ // Arrow form so `this` binds when passed as `.getAngle=${this.getAngle}`
180
+ // to <obc-instrument-radial>. Do not convert to a method.
181
+ getAngle = (v: number): number => {
182
+ const {sweep, start} = this.sectorAngles;
104
183
  const span = this.maxValue - this.minValue;
105
184
  if (!Number.isFinite(span) || span <= 0) {
106
- return -135;
185
+ return start;
107
186
  }
108
187
 
109
- return ((v - this.minValue) / span) * 270 - 135;
188
+ return ((v - this.minValue) / span) * sweep + start;
189
+ };
190
+
191
+ /** Renders one gauge readout; `withMeta`/`labelOnly` pick parts. */
192
+ private renderReadout({
193
+ className,
194
+ variant,
195
+ alignment = ReadoutStackVerticalAlignment.vertical,
196
+ withMeta = true,
197
+ labelOnly = false,
198
+ }: {
199
+ className: string;
200
+ variant: ReadoutVariant;
201
+ alignment?: ReadoutStackVerticalAlignment;
202
+ withMeta?: boolean;
203
+ labelOnly?: boolean;
204
+ }): TemplateResult {
205
+ // `labelOnly` already means "no value", so derive it instead of passing both.
206
+ const withValue = !labelOnly;
207
+ return html`
208
+ <obc-readout
209
+ class=${className}
210
+ direction="vertical"
211
+ ?labelOnly=${labelOnly}
212
+ .variant=${variant}
213
+ .alignment=${alignment}
214
+ .valuePriority=${withValue ? this.priority : undefined}
215
+ .value=${withValue ? this.value : undefined}
216
+ .fractionDigits=${this.fractionDigits}
217
+ .label=${withMeta ? this.label : ''}
218
+ .unit=${withMeta ? this.unit : ''}
219
+ ></obc-readout>
220
+ `;
221
+ }
222
+
223
+ private renderReadouts() {
224
+ if (!this.showReadout) {
225
+ return nothing;
226
+ }
227
+
228
+ const isNeedle = this.type === ObcGaugeRadialType.needle;
229
+ const is90 = this.isSector90;
230
+ const is180 = this.sector === GaugeRadialSector.deg180;
231
+
232
+ if (isNeedle && (is180 || is90)) {
233
+ return nothing;
234
+ }
235
+
236
+ // 90 filled/bar: enhanced corner readout (bold value + label/unit).
237
+ if (is90) {
238
+ return this.renderReadout({
239
+ className: 'gauge-readout-meta',
240
+ variant: ReadoutVariant.enhanced,
241
+ });
242
+ }
243
+
244
+ // 270 needle and 180 filled/bar: centered stack (value weight follows `priority`).
245
+ if (isNeedle || is180) {
246
+ return this.renderReadout({
247
+ className: 'gauge-readout-meta',
248
+ variant: ReadoutVariant.stack,
249
+ alignment: ReadoutStackVerticalAlignment.center,
250
+ });
251
+ }
252
+
253
+ // 270 filled/bar: the value sits at the circle center and the label/unit row
254
+ // lower in the bottom gap, so they are two separately-positioned readouts.
255
+ return html`
256
+ ${this.renderReadout({
257
+ className: 'gauge-readout-value',
258
+ variant: ReadoutVariant.enhanced,
259
+ withMeta: false,
260
+ })}
261
+ ${this.label || this.unit
262
+ ? this.renderReadout({
263
+ className: 'gauge-readout-meta',
264
+ variant: ReadoutVariant.stack,
265
+ alignment: ReadoutStackVerticalAlignment.center,
266
+ labelOnly: true,
267
+ })
268
+ : nothing}
269
+ `;
110
270
  }
111
271
 
112
272
  override render() {
273
+ const clips = this.sectorClips;
113
274
  return html`
114
- <obc-instrument-radial
115
- .value=${this.value}
116
- .state=${this.state}
117
- .priority=${this.priority}
118
- .setpoint=${this.setpoint}
119
- .newSetpoint=${this.newSetpoint}
120
- .setpointAtZeroDeadband=${this.setpointAtZeroDeadband}
121
- .setpointOverride=${this.setpointOverride}
122
- .touching=${this.touching}
123
- .autoAtSetpoint=${this.autoAtSetpoint}
124
- .autoAtSetpointDeadband=${this.autoAtSetpointDeadband}
125
- .animateSetpoint=${this.animateSetpoint}
126
- .maxValue=${this.maxValue}
127
- .minValue=${this.minValue}
128
- .getAngle=${this.getAngle}
129
- .showLabels=${this.showLabels}
130
- .primaryTickmarkInterval=${this.primaryTickmarkInterval}
131
- .secondaryTickmarkInterval=${this.secondaryTickmarkInterval}
132
- .tertiaryTickmarkInterval=${this.tertiaryTickmarkInterval}
133
- .type=${this.type}
134
- .needleType=${this.type}
135
- .tickmarksInside=${this.tickmarksInside}
136
- .tickmarkStyle=${this.tickmarkStyle}
137
- .advices=${this.advices}
275
+ <div
276
+ class=${classMap({
277
+ 'gauge-radial-root': true,
278
+ 'type-needle': this.type === ObcGaugeRadialType.needle,
279
+ 'sector-180': this.sector === GaugeRadialSector.deg180,
280
+ 'sector-90-left': this.sector === GaugeRadialSector.deg90Left,
281
+ 'sector-90-right': this.sector === GaugeRadialSector.deg90Right,
282
+ })}
138
283
  >
139
- </obc-instrument-radial>
284
+ <obc-instrument-radial
285
+ .value=${this.value}
286
+ .state=${this.state}
287
+ .priority=${this.priority}
288
+ .setpoint=${this.setpoint}
289
+ .newSetpoint=${this.newSetpoint}
290
+ .setpointAtZeroDeadband=${this.setpointAtZeroDeadband}
291
+ .setpointOverride=${this.setpointOverride}
292
+ .touching=${this.touching}
293
+ .autoAtSetpoint=${this.autoAtSetpoint}
294
+ .autoAtSetpointDeadband=${this.autoAtSetpointDeadband}
295
+ .animateSetpoint=${this.animateSetpoint}
296
+ .maxValue=${this.maxValue}
297
+ .minValue=${this.minValue}
298
+ .getAngle=${this.getAngle}
299
+ .showLabels=${this.showLabels}
300
+ .primaryTickmarkInterval=${this.primaryTickmarkInterval}
301
+ .secondaryTickmarkInterval=${this.secondaryTickmarkInterval}
302
+ .tertiaryTickmarkInterval=${this.tertiaryTickmarkInterval}
303
+ .type=${this.type}
304
+ .needleType=${this.type}
305
+ .tickmarksInside=${this.tickmarksInside}
306
+ .tickmarkStyle=${this.tickmarkStyle}
307
+ .advices=${this.isSector90 ? [] : this.advices}
308
+ .clipTop=${clips.top}
309
+ .clipBottom=${clips.bottom}
310
+ .clipLeft=${clips.left}
311
+ .clipRight=${clips.right}
312
+ .endLabelsMaxMin=${this.sector === GaugeRadialSector.deg180}
313
+ >
314
+ </obc-instrument-radial>
315
+ ${this.renderReadouts()}
316
+ </div>
140
317
  `;
141
318
  }
319
+
320
+ static override styles = unsafeCSS(componentStyle);
142
321
  }
143
322
 
144
323
  declare global {
@@ -423,6 +423,10 @@
423
423
  text-align: center;
424
424
  }
425
425
 
426
+ .readout.stack.vertical.alignment-center .instrument-label-unit-container {
427
+ align-items: center;
428
+ }
429
+
426
430
  .readout.stack.vertical.alignment-vertical {
427
431
  justify-items: end;
428
432
  }
@@ -49,6 +49,7 @@ export function tickmark(
49
49
  maxDigits,
50
50
  color,
51
51
  radiusOffset = 0,
52
+ endLabelsMaxMin = false,
52
53
  }: {
53
54
  size: TickmarkType;
54
55
  style: TickmarkStyle;
@@ -60,6 +61,7 @@ export function tickmark(
60
61
  maxDigits: number;
61
62
  color?: string;
62
63
  radiusOffset?: number;
64
+ endLabelsMaxMin?: boolean;
63
65
  }
64
66
  ): SVGTemplateResult | SVGTemplateResult[] {
65
67
  // check if scale is not infinite
@@ -87,7 +89,9 @@ export function tickmark(
87
89
  innerRadius = 328 / 2 + rOff;
88
90
  outerRadius = 336 / 2 + rOff;
89
91
  } else {
90
- return [textSvg(text ?? '', angle, inside, scale, textRadius)];
92
+ return [
93
+ textSvg(text ?? '', {angle, inside, scale, textRadius, endLabelsMaxMin}),
94
+ ];
91
95
  }
92
96
 
93
97
  // When inside, anchor ticks at the outer ring edge and grow inward,
@@ -115,7 +119,10 @@ export function tickmark(
115
119
  const tick = svg`<line x1=${x1} y1=${y1} x2=${x2} y2=${y2} stroke=${colorName} stroke-width=${strokeWidth} vector-effect="non-scaling-stroke"/>`;
116
120
  if (text) {
117
121
  if (rotation === undefined) {
118
- return [tick, textSvg(text, angle, inside, scale, textRadius)];
122
+ return [
123
+ tick,
124
+ textSvg(text, {angle, inside, scale, textRadius, endLabelsMaxMin}),
125
+ ];
119
126
  } else {
120
127
  const newRadius =
121
128
  textRadius + ((4 / scale + 5) * (inside ? -1 : 1) * maxDigits) / 2;
@@ -132,11 +139,31 @@ export function tickmark(
132
139
 
133
140
  function textSvg(
134
141
  text: string,
135
- angle: number,
136
- inside: boolean,
137
- scale: number,
138
- textRadius: number
142
+ {
143
+ angle,
144
+ inside,
145
+ scale,
146
+ textRadius,
147
+ endLabelsMaxMin = false,
148
+ }: {
149
+ angle: number;
150
+ inside: boolean;
151
+ scale: number;
152
+ textRadius: number;
153
+ endLabelsMaxMin?: boolean;
154
+ }
139
155
  ) {
156
+ const radHoriz = (angle * Math.PI) / 180;
157
+ // "Max-min" placement: horizontal end labels (±90°) sit off the dead-center
158
+ // tick (below outside / lifted inside), inset inward by label width.
159
+ if (endLabelsMaxMin && Math.abs(Math.cos(radHoriz)) < 1e-6) {
160
+ const sin = Math.sin(radHoriz);
161
+ const inward = inside ? (6 + (text.length - 1) * 2.5) / scale : 14 / scale;
162
+ const x = sin * (textRadius - inward);
163
+ const y = inside ? -(6 / scale) : 12 / scale;
164
+ return svg`<text x=${x} y=${y} class="label bottom ${inside ? 'inside' : ''}">${text}</text>`;
165
+ }
166
+
140
167
  let positionClass;
141
168
  if (angle === 0) {
142
169
  positionClass = 'top';