@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.101 → 2.0.0-next.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/openbridge-webcomponents.bundle.js +580 -143
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +629 -407
- package/dist/bars-graphs/donut-chart/donut-chart.d.ts +18 -16
- package/dist/bars-graphs/donut-chart/donut-chart.d.ts.map +1 -1
- package/dist/bars-graphs/donut-chart/donut-chart.js.map +1 -1
- package/dist/bars-graphs/pie-chart/pie-chart.d.ts +14 -12
- package/dist/bars-graphs/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/bars-graphs/pie-chart/pie-chart.js.map +1 -1
- package/dist/bars-graphs/polar-chart/polar-chart.d.ts +19 -16
- package/dist/bars-graphs/polar-chart/polar-chart.d.ts.map +1 -1
- package/dist/bars-graphs/polar-chart/polar-chart.js.map +1 -1
- package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.d.ts +8 -8
- package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.d.ts.map +1 -1
- package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.js.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.d.ts +8 -25
- package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
- package/dist/building-blocks/instrument-radial/instrument-radial.d.ts +22 -2
- package/dist/building-blocks/instrument-radial/instrument-radial.d.ts.map +1 -1
- package/dist/building-blocks/instrument-radial/instrument-radial.js +61 -37
- package/dist/building-blocks/instrument-radial/instrument-radial.js.map +1 -1
- package/dist/integration-systems/integration-bar/integration-bar.d.ts +20 -24
- package/dist/integration-systems/integration-bar/integration-bar.d.ts.map +1 -1
- package/dist/integration-systems/integration-bar/integration-bar.js.map +1 -1
- package/dist/integration-systems/integration-button/integration-button.d.ts +15 -13
- package/dist/integration-systems/integration-button/integration-button.d.ts.map +1 -1
- package/dist/integration-systems/integration-button/integration-button.js.map +1 -1
- package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.d.ts +14 -0
- package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.d.ts.map +1 -1
- package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.js +30 -11
- package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.js.map +1 -1
- package/dist/navigation-instruments/compass/compass.d.ts +68 -31
- package/dist/navigation-instruments/compass/compass.d.ts.map +1 -1
- package/dist/navigation-instruments/compass/compass.js +36 -16
- package/dist/navigation-instruments/compass/compass.js.map +1 -1
- package/dist/navigation-instruments/compass-flat/compass-flat.d.ts +8 -9
- package/dist/navigation-instruments/compass-flat/compass-flat.d.ts.map +1 -1
- package/dist/navigation-instruments/compass-flat/compass-flat.js.map +1 -1
- package/dist/navigation-instruments/compass-sector/compass-sector.d.ts.map +1 -1
- package/dist/navigation-instruments/compass-sector/compass-sector.js.map +1 -1
- package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js +34 -2
- package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js.map +1 -1
- package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts +15 -0
- package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts.map +1 -1
- package/dist/navigation-instruments/gauge-radial/gauge-radial.js +35 -1
- package/dist/navigation-instruments/gauge-radial/gauge-radial.js.map +1 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts +0 -5
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
- package/dist/navigation-instruments/graph-mini/graph-mini.d.ts +1 -1
- package/dist/navigation-instruments/graph-mini/graph-mini.d.ts.map +1 -1
- package/dist/navigation-instruments/graph-mini/graph-mini.js.map +1 -1
- package/dist/navigation-instruments/heading/heading.d.ts +19 -1
- package/dist/navigation-instruments/heading/heading.d.ts.map +1 -1
- package/dist/navigation-instruments/heading/heading.js +34 -16
- package/dist/navigation-instruments/heading/heading.js.map +1 -1
- package/dist/navigation-instruments/indicator-graph/indicator-graph.d.ts +1 -1
- package/dist/navigation-instruments/indicator-graph/indicator-graph.d.ts.map +1 -1
- package/dist/navigation-instruments/indicator-graph/indicator-graph.js.map +1 -1
- package/dist/navigation-instruments/rudder/rudder.d.ts +15 -2
- package/dist/navigation-instruments/rudder/rudder.d.ts.map +1 -1
- package/dist/navigation-instruments/rudder/rudder.js +36 -26
- package/dist/navigation-instruments/rudder/rudder.js.map +1 -1
- package/dist/navigation-instruments/speed-gauge/speed-gauge.d.ts +15 -1
- package/dist/navigation-instruments/speed-gauge/speed-gauge.d.ts.map +1 -1
- package/dist/navigation-instruments/speed-gauge/speed-gauge.js +31 -3
- package/dist/navigation-instruments/speed-gauge/speed-gauge.js.map +1 -1
- package/dist/navigation-instruments/watch/watch.d.ts +45 -17
- package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
- package/dist/navigation-instruments/watch/watch.js +53 -38
- package/dist/navigation-instruments/watch/watch.js.map +1 -1
- package/dist/svghelpers/radial-frame.d.ts +122 -0
- package/dist/svghelpers/radial-frame.d.ts.map +1 -0
- package/dist/svghelpers/radial-frame.js +259 -0
- package/dist/svghelpers/radial-frame.js.map +1 -0
- package/package.json +1 -1
- package/script/check-slot-event-docs.ts +35 -0
- package/src/bars-graphs/donut-chart/donut-chart.ts +18 -16
- package/src/bars-graphs/pie-chart/pie-chart.ts +14 -12
- package/src/bars-graphs/polar-chart/polar-chart.ts +19 -16
- package/src/bars-graphs/radial-bar-chart/radial-bar-chart.ts +8 -8
- package/src/building-blocks/chart-line/chart-line-base.ts +8 -25
- package/src/building-blocks/instrument-radial/instrument-radial.stories.ts +187 -6
- package/src/building-blocks/instrument-radial/instrument-radial.ts +98 -47
- package/src/integration-systems/integration-bar/integration-bar.ts +20 -24
- package/src/integration-systems/integration-button/integration-button.ts +15 -13
- package/src/navigation-instruments/azimuth-thruster/azimuth-thruster.stories.ts +78 -1
- package/src/navigation-instruments/azimuth-thruster/azimuth-thruster.ts +55 -11
- package/src/navigation-instruments/compass/compass.stories.ts +88 -1
- package/src/navigation-instruments/compass/compass.ts +99 -46
- package/src/navigation-instruments/compass-flat/compass-flat.ts +8 -9
- package/src/navigation-instruments/compass-sector/compass-sector.ts +7 -0
- package/src/navigation-instruments/gauge-radial/gauge-radial.css +32 -2
- package/src/navigation-instruments/gauge-radial/gauge-radial.stories.ts +265 -1
- package/src/navigation-instruments/gauge-radial/gauge-radial.ts +59 -1
- package/src/navigation-instruments/gauge-trend/gauge-trend.ts +0 -5
- package/src/navigation-instruments/graph-mini/graph-mini.ts +1 -1
- package/src/navigation-instruments/heading/heading.stories.ts +92 -1
- package/src/navigation-instruments/heading/heading.ts +50 -16
- package/src/navigation-instruments/indicator-graph/indicator-graph.ts +1 -1
- package/src/navigation-instruments/rudder/rudder.stories.ts +83 -1
- package/src/navigation-instruments/rudder/rudder.ts +61 -34
- package/src/navigation-instruments/speed-gauge/speed-gauge.stories.ts +94 -1
- package/src/navigation-instruments/speed-gauge/speed-gauge.ts +57 -4
- package/src/navigation-instruments/watch/watch.stories.ts +140 -1
- package/src/navigation-instruments/watch/watch.ts +100 -60
- package/src/storybook-util.ts +59 -0
- package/src/svghelpers/radial-frame.spec.ts +307 -0
- package/src/svghelpers/radial-frame.ts +508 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { WatchArea } from '../navigation-instruments/watch/watch.js';
|
|
2
|
+
import { ZoomToFitArcFrame } from './arc-frame.js';
|
|
3
|
+
/** Approximate digit advance at 12px font-instrument-label. */
|
|
4
|
+
export declare const DIGIT_WIDTH_PX = 7;
|
|
5
|
+
/** Compass NSWE label width assumed by label.ts (`labelWidth = 16`). */
|
|
6
|
+
export declare const NSWE_LABEL_WIDTH_PX = 16;
|
|
7
|
+
/**
|
|
8
|
+
* North-arrow glyph allowance for outside-decor reserves (px). The arrow
|
|
9
|
+
* renders regardless of `showLabels`, so consumers charge this portion
|
|
10
|
+
* whenever the arrow sits outside the ring.
|
|
11
|
+
*/
|
|
12
|
+
export declare const NORTH_ARROW_WIDTH_PX = 16;
|
|
13
|
+
/**
|
|
14
|
+
* Labels are dropped (not clipped) when their reserve would exceed this share
|
|
15
|
+
* of the box. 0.45 makes a 4-digit gauge hide labels below a ~182px container
|
|
16
|
+
* — near the canvas charts' fixed MIN_HEIGHT_WITH_LABELS = 192px threshold —
|
|
17
|
+
* while 2-digit labels survive down to ~120px. Designers may later prefer a
|
|
18
|
+
* fixed pixel threshold instead (see charthelpers/constants.ts).
|
|
19
|
+
*/
|
|
20
|
+
export declare const LABEL_RESERVE_MAX_FRACTION = 0.45;
|
|
21
|
+
/**
|
|
22
|
+
* Pixel drop of "Max-min" end labels (`endLabelsMaxMin`) below the ±90° line:
|
|
23
|
+
* anchored at `12px/scale` with `dominant-baseline: central` (tickmark.ts),
|
|
24
|
+
* so the glyph bottom reaches ~12 + 6 + descender ≈ 20px — plus 1px pad.
|
|
25
|
+
*/
|
|
26
|
+
export declare const END_MAXMIN_LABEL_DROP_PX = 21;
|
|
27
|
+
/**
|
|
28
|
+
* Pixel drop of regular side labels at the ±90° ends: glyph centered on the
|
|
29
|
+
* line (half-height ~6px + descender), plus pad.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SIDE_LABEL_DROP_PX = 13;
|
|
32
|
+
/** Per-edge viewBox crops in percent (0–100), matching obc-watch `clip*`. */
|
|
33
|
+
export interface RadialClips {
|
|
34
|
+
top: number;
|
|
35
|
+
bottom: number;
|
|
36
|
+
left: number;
|
|
37
|
+
right: number;
|
|
38
|
+
}
|
|
39
|
+
export interface RadialFrameOptions {
|
|
40
|
+
/** Legacy padding: the frame never shrinks below `(176 + basePadding) * 2`. */
|
|
41
|
+
basePadding: number;
|
|
42
|
+
/** Max label pixel width at 12px font; 0/undefined = no outside labels. */
|
|
43
|
+
labelWidthPx?: number;
|
|
44
|
+
/** Un-zoomed viewBox crops; ignored when `zoomToFitArc` is set. */
|
|
45
|
+
clips?: RadialClips;
|
|
46
|
+
/**
|
|
47
|
+
* Pixel extent the outside labels need past the ±90° horizontal line
|
|
48
|
+
* toward a top/bottom crop edge (see END_MAXMIN_LABEL_DROP_PX /
|
|
49
|
+
* SIDE_LABEL_DROP_PX). When a `clips.top`/`clips.bottom` crop would cut
|
|
50
|
+
* into that drop, the clip is lowered so the labels stay inside the box.
|
|
51
|
+
* Absent/0 keeps the requested clips untouched (legacy geometry).
|
|
52
|
+
*/
|
|
53
|
+
labelDropPx?: number;
|
|
54
|
+
/** Host client box in px; absent or zero ⇒ scale-1 fallback (first paint). */
|
|
55
|
+
containerPx?: {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
};
|
|
59
|
+
/** Outer-ring diameter in px; pins scale for equal circumference (mode b). */
|
|
60
|
+
faceDiameter?: number;
|
|
61
|
+
zoomToFitArc?: boolean;
|
|
62
|
+
/** Visible arc segments; required for the zoom path. */
|
|
63
|
+
areas?: WatchArea[];
|
|
64
|
+
/** Innermost visible radius (depends on WatchCircleType); zoom path only. */
|
|
65
|
+
innerRadius?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface RadialFrame extends ZoomToFitArcFrame {
|
|
68
|
+
/** Px per SVG unit implied by container or faceDiameter (1 when unknown). */
|
|
69
|
+
scale: number;
|
|
70
|
+
/** SVG units added beyond the base geometry for labels (0 when none/hidden). */
|
|
71
|
+
labelReserve: number;
|
|
72
|
+
/** True when the label reserve exceeded the cap; callers strip tick texts. */
|
|
73
|
+
labelsHidden: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* True when `labelDropPx` lowered a top/bottom clip: the box's aspect no
|
|
76
|
+
* longer matches the requested clip fractions, so hosts with an
|
|
77
|
+
* aspect-ratio derived from those fractions must follow the frame instead.
|
|
78
|
+
*/
|
|
79
|
+
clipsAdjusted: boolean;
|
|
80
|
+
/** Fixed host size in px; defined only when `faceDiameter` is set. */
|
|
81
|
+
hostWidthPx?: number;
|
|
82
|
+
/** Fixed host size in px; defined only when `faceDiameter` is set. */
|
|
83
|
+
hostHeightPx?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Measure the host's available box in px. Radial instrument hosts render
|
|
87
|
+
* inline by default, so clientWidth/Height are often 0 — fall back to the
|
|
88
|
+
* parent box, like obc-watch's getScale() has always done (issue #1032).
|
|
89
|
+
*/
|
|
90
|
+
export declare function measureContainerPx(host: HTMLElement): {
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Apply/remove the fixed intrinsic host size a `faceDiameter` frame pins.
|
|
96
|
+
* The hosts render inline by default, so a pinned size also needs
|
|
97
|
+
* `display: block` for width/height to apply. Returns whether the size is
|
|
98
|
+
* currently pinned; pass that back as `wasPinned` on the next call so an
|
|
99
|
+
* unpinned host's own inline styles are never clobbered.
|
|
100
|
+
*/
|
|
101
|
+
export declare function applyPinnedHostSize(host: HTMLElement, frame: RadialFrame | undefined, wasPinned: boolean): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Additionally observe an internal element that always generates a box (the
|
|
104
|
+
* shadow `<svg>` or `.container`). Several radial hosts render inline (no
|
|
105
|
+
* `:host {display}` rule), and ResizeObserver never fires for inline boxes —
|
|
106
|
+
* the host reports a permanent 0×0 size, so a controller observing only the
|
|
107
|
+
* host misses every resize. That freezes the container-dependent frame and
|
|
108
|
+
* the `--scale` font counter-scaling at their first-paint values.
|
|
109
|
+
*/
|
|
110
|
+
export declare function observeInnerBox(controller: {
|
|
111
|
+
observe(target: Element): void;
|
|
112
|
+
}, renderRoot: ParentNode): void;
|
|
113
|
+
/**
|
|
114
|
+
* Estimate the widest label's pixel width at the 12px instrument label font.
|
|
115
|
+
*/
|
|
116
|
+
export declare function estimateLabelWidthPx(texts: readonly (string | undefined)[]): number;
|
|
117
|
+
/**
|
|
118
|
+
* Compute the viewBox frame for a radial instrument, reserving width-aware
|
|
119
|
+
* room for outside tick labels. See the module documentation for the model.
|
|
120
|
+
*/
|
|
121
|
+
export declare function computeRadialFrame(opts: RadialFrameOptions): RadialFrame;
|
|
122
|
+
//# sourceMappingURL=radial-frame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"radial-frame.d.ts","sourceRoot":"","sources":["../../src/svghelpers/radial-frame.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0CAA0C,CAAC;AACxE,OAAO,EAA2B,KAAK,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AA6DhF,+DAA+D;AAC/D,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;IAC9C,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,YAAY,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAWA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,SAAS,EAAE,OAAO,GACjB,OAAO,CAaT;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE;IAAC,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;CAAC,EAC5C,UAAU,EAAE,UAAU,GACrB,IAAI,CAKN;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GACrC,MAAM,CAQR;AAoJD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,WAAW,CA4DxE"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { computeZoomToFitArcFrame } from "./arc-frame.js";
|
|
2
|
+
const OUTER_RING_RADIUS = 368 / 2;
|
|
3
|
+
const RADIAL_VIEWBOX_BASE = 176;
|
|
4
|
+
const LABEL_RADIAL_PAD_UNITS = 3;
|
|
5
|
+
const LABEL_ANCHOR_OFFSET_PX = 9;
|
|
6
|
+
const LABEL_EDGE_PAD_PX = 4;
|
|
7
|
+
const DIGIT_WIDTH_PX = 7;
|
|
8
|
+
const NSWE_LABEL_WIDTH_PX = 16;
|
|
9
|
+
const NORTH_ARROW_WIDTH_PX = 16;
|
|
10
|
+
const LABEL_RESERVE_MAX_FRACTION = 0.45;
|
|
11
|
+
const END_MAXMIN_LABEL_DROP_PX = 21;
|
|
12
|
+
const SIDE_LABEL_DROP_PX = 13;
|
|
13
|
+
function measureContainerPx(host) {
|
|
14
|
+
let width = host.clientWidth;
|
|
15
|
+
let height = host.clientHeight;
|
|
16
|
+
if (width === 0 || height === 0) {
|
|
17
|
+
const box = host.parentElement?.getBoundingClientRect();
|
|
18
|
+
if (box) {
|
|
19
|
+
width = box.width;
|
|
20
|
+
height = box.height;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { width, height };
|
|
24
|
+
}
|
|
25
|
+
function applyPinnedHostSize(host, frame, wasPinned) {
|
|
26
|
+
if (frame?.hostWidthPx !== void 0 && frame.hostHeightPx !== void 0) {
|
|
27
|
+
host.style.width = `${frame.hostWidthPx}px`;
|
|
28
|
+
host.style.height = `${frame.hostHeightPx}px`;
|
|
29
|
+
host.style.display = "block";
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (wasPinned) {
|
|
33
|
+
host.style.removeProperty("width");
|
|
34
|
+
host.style.removeProperty("height");
|
|
35
|
+
host.style.removeProperty("display");
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function observeInnerBox(controller, renderRoot) {
|
|
40
|
+
const box = renderRoot.querySelector("svg, .container");
|
|
41
|
+
if (box) {
|
|
42
|
+
controller.observe(box);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function estimateLabelWidthPx(texts) {
|
|
46
|
+
let maxChars = 0;
|
|
47
|
+
for (const t of texts) {
|
|
48
|
+
if (t !== void 0 && t.length > maxChars) {
|
|
49
|
+
maxChars = t.length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return maxChars * DIGIT_WIDTH_PX;
|
|
53
|
+
}
|
|
54
|
+
function clipFractions(clips) {
|
|
55
|
+
if (!clips) {
|
|
56
|
+
return { fx: 1, fy: 1 };
|
|
57
|
+
}
|
|
58
|
+
const fx = 1 - (clips.left + clips.right) / 100;
|
|
59
|
+
const fy = 1 - (clips.top + clips.bottom) / 100;
|
|
60
|
+
return { fx: fx > 0 ? fx : 1, fy: fy > 0 ? fy : 1 };
|
|
61
|
+
}
|
|
62
|
+
function buildViewBox(side, clips) {
|
|
63
|
+
const { fx, fy } = clipFractions(clips);
|
|
64
|
+
const width = side * fx;
|
|
65
|
+
const height = side * fy;
|
|
66
|
+
const x = -side / 2 + side * (clips?.left ?? 0) / 100;
|
|
67
|
+
const y = -side / 2 + side * (clips?.top ?? 0) / 100;
|
|
68
|
+
return {
|
|
69
|
+
x,
|
|
70
|
+
y,
|
|
71
|
+
width,
|
|
72
|
+
height,
|
|
73
|
+
viewBox: `${x} ${y} ${width} ${height}`,
|
|
74
|
+
radiusOffset: 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function pinnedScale(opts) {
|
|
78
|
+
if (opts.faceDiameter === void 0) {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const scale = opts.faceDiameter / (2 * OUTER_RING_RADIUS);
|
|
82
|
+
return Number.isFinite(scale) && scale > 0 ? scale : void 0;
|
|
83
|
+
}
|
|
84
|
+
function effectiveContainer(opts, fx, fy) {
|
|
85
|
+
const w = opts.containerPx?.width ?? 0;
|
|
86
|
+
const h = opts.containerPx?.height ?? 0;
|
|
87
|
+
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
return Math.min(w / fx, h / fy);
|
|
91
|
+
}
|
|
92
|
+
function containerScale(opts, frame) {
|
|
93
|
+
const w = opts.containerPx?.width ?? 0;
|
|
94
|
+
const h = opts.containerPx?.height ?? 0;
|
|
95
|
+
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
const scale = Math.min(w / frame.width, h / frame.height);
|
|
99
|
+
return Number.isFinite(scale) && scale > 0 ? scale : void 0;
|
|
100
|
+
}
|
|
101
|
+
function withHostPx(frame, pinned, scale, labelReserve, labelsHidden, clipsAdjusted = false) {
|
|
102
|
+
const result = {
|
|
103
|
+
...frame,
|
|
104
|
+
scale,
|
|
105
|
+
labelReserve,
|
|
106
|
+
labelsHidden,
|
|
107
|
+
clipsAdjusted
|
|
108
|
+
};
|
|
109
|
+
if (pinned) {
|
|
110
|
+
result.hostWidthPx = frame.width * scale;
|
|
111
|
+
result.hostHeightPx = frame.height * scale;
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
function solveSide(opts, labelCostPx, clips) {
|
|
116
|
+
const baseSide = (RADIAL_VIEWBOX_BASE + opts.basePadding) * 2;
|
|
117
|
+
const { fx, fy } = clipFractions(clips);
|
|
118
|
+
const labelSideAt = (scale) => 2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS + labelCostPx / scale);
|
|
119
|
+
let side = baseSide;
|
|
120
|
+
let labelsHidden = false;
|
|
121
|
+
if (labelCostPx > 0) {
|
|
122
|
+
const pinned = pinnedScale(opts);
|
|
123
|
+
if (pinned !== void 0) {
|
|
124
|
+
const grown = Math.max(baseSide, labelSideAt(pinned));
|
|
125
|
+
labelsHidden = 2 * (labelCostPx / pinned) / grown > LABEL_RESERVE_MAX_FRACTION;
|
|
126
|
+
side = labelsHidden ? baseSide : grown;
|
|
127
|
+
} else {
|
|
128
|
+
const container = effectiveContainer(opts, fx, fy);
|
|
129
|
+
if (container === void 0) {
|
|
130
|
+
side = Math.max(baseSide, labelSideAt(1));
|
|
131
|
+
} else if (2 * labelCostPx / container > LABEL_RESERVE_MAX_FRACTION) {
|
|
132
|
+
labelsHidden = true;
|
|
133
|
+
} else {
|
|
134
|
+
side = Math.max(
|
|
135
|
+
baseSide,
|
|
136
|
+
2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS) / (1 - 2 * labelCostPx / container)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { side, labelsHidden };
|
|
142
|
+
}
|
|
143
|
+
function adjustClipsForLabelDrop(clips, side, scale, labelDropPx) {
|
|
144
|
+
const requiredUnits = labelDropPx / scale;
|
|
145
|
+
const requiredClipPct = Math.max(0, (0.5 - requiredUnits / side) * 100);
|
|
146
|
+
const bottom = clips.bottom > requiredClipPct ? requiredClipPct : clips.bottom;
|
|
147
|
+
const top = clips.top > requiredClipPct ? requiredClipPct : clips.top;
|
|
148
|
+
if (bottom === clips.bottom && top === clips.top) {
|
|
149
|
+
return clips;
|
|
150
|
+
}
|
|
151
|
+
return { ...clips, top, bottom };
|
|
152
|
+
}
|
|
153
|
+
function computeRadialFrame(opts) {
|
|
154
|
+
const labelWidthPx = opts.labelWidthPx ?? 0;
|
|
155
|
+
const labelCostPx = labelWidthPx > 0 ? LABEL_ANCHOR_OFFSET_PX + labelWidthPx + LABEL_EDGE_PAD_PX : 0;
|
|
156
|
+
if (opts.zoomToFitArc && opts.areas && opts.areas.length > 0) {
|
|
157
|
+
return computeZoomedFrame(opts, labelCostPx);
|
|
158
|
+
}
|
|
159
|
+
const baseSide = (RADIAL_VIEWBOX_BASE + opts.basePadding) * 2;
|
|
160
|
+
let clips = opts.clips;
|
|
161
|
+
let { side, labelsHidden } = solveSide(opts, labelCostPx, clips);
|
|
162
|
+
const labelDropPx = opts.labelDropPx ?? 0;
|
|
163
|
+
if (labelDropPx > 0 && labelCostPx > 0 && !labelsHidden && clips) {
|
|
164
|
+
for (let i = 0; i < 2; i++) {
|
|
165
|
+
const scale2 = pinnedScale(opts) ?? containerScale(opts, buildViewBox(side, clips)) ?? 1;
|
|
166
|
+
const adjusted = adjustClipsForLabelDrop(
|
|
167
|
+
clips,
|
|
168
|
+
side,
|
|
169
|
+
scale2,
|
|
170
|
+
labelDropPx
|
|
171
|
+
);
|
|
172
|
+
if (adjusted === clips) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
clips = adjusted;
|
|
176
|
+
const solved = solveSide(opts, labelCostPx, clips);
|
|
177
|
+
side = solved.side;
|
|
178
|
+
labelsHidden = solved.labelsHidden;
|
|
179
|
+
}
|
|
180
|
+
if (labelsHidden) {
|
|
181
|
+
clips = opts.clips;
|
|
182
|
+
const solved = solveSide(opts, labelCostPx, clips);
|
|
183
|
+
side = solved.side;
|
|
184
|
+
labelsHidden = solved.labelsHidden;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const frame = buildViewBox(side, clips);
|
|
188
|
+
const pinned = pinnedScale(opts);
|
|
189
|
+
const scale = pinned ?? containerScale(opts, frame) ?? 1;
|
|
190
|
+
return withHostPx(
|
|
191
|
+
frame,
|
|
192
|
+
pinned !== void 0,
|
|
193
|
+
scale,
|
|
194
|
+
(side - baseSide) / 2,
|
|
195
|
+
labelsHidden,
|
|
196
|
+
clips !== opts.clips
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
function computeZoomedFrame(opts, labelCostPx) {
|
|
200
|
+
let reserve = 0;
|
|
201
|
+
let labelsHidden = false;
|
|
202
|
+
let frame = zoomFrameAt(opts, opts.basePadding);
|
|
203
|
+
if (labelCostPx > 0) {
|
|
204
|
+
for (let i = 0; i < 2; i++) {
|
|
205
|
+
const passScale = zoomedScale(opts, frame);
|
|
206
|
+
const reserveUnits = labelCostPx / passScale;
|
|
207
|
+
if (2 * reserveUnits / Math.max(frame.width, frame.height) > LABEL_RESERVE_MAX_FRACTION) {
|
|
208
|
+
labelsHidden = true;
|
|
209
|
+
reserve = 0;
|
|
210
|
+
frame = zoomFrameAt(opts, opts.basePadding);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
reserve = reserveUnits;
|
|
214
|
+
frame = zoomFrameAt(opts, opts.basePadding + reserve);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const pinned = pinnedZoomedScale(opts, frame);
|
|
218
|
+
const scale = pinned ?? containerScale(opts, frame) ?? 1;
|
|
219
|
+
return withHostPx(
|
|
220
|
+
frame,
|
|
221
|
+
pinned !== void 0,
|
|
222
|
+
scale,
|
|
223
|
+
labelsHidden ? 0 : reserve,
|
|
224
|
+
labelsHidden
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
function zoomFrameAt(opts, extension) {
|
|
228
|
+
return computeZoomToFitArcFrame({
|
|
229
|
+
areas: opts.areas,
|
|
230
|
+
outerRadius: OUTER_RING_RADIUS,
|
|
231
|
+
innerRadius: opts.innerRadius ?? OUTER_RING_RADIUS,
|
|
232
|
+
extension,
|
|
233
|
+
targetSize: (RADIAL_VIEWBOX_BASE + extension) * 2
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function pinnedZoomedScale(opts, frame) {
|
|
237
|
+
if (opts.faceDiameter === void 0) {
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
const scale = opts.faceDiameter / (2 * (OUTER_RING_RADIUS + frame.radiusOffset));
|
|
241
|
+
return Number.isFinite(scale) && scale > 0 ? scale : void 0;
|
|
242
|
+
}
|
|
243
|
+
function zoomedScale(opts, frame) {
|
|
244
|
+
return pinnedZoomedScale(opts, frame) ?? containerScale(opts, frame) ?? 1;
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
DIGIT_WIDTH_PX,
|
|
248
|
+
END_MAXMIN_LABEL_DROP_PX,
|
|
249
|
+
LABEL_RESERVE_MAX_FRACTION,
|
|
250
|
+
NORTH_ARROW_WIDTH_PX,
|
|
251
|
+
NSWE_LABEL_WIDTH_PX,
|
|
252
|
+
SIDE_LABEL_DROP_PX,
|
|
253
|
+
applyPinnedHostSize,
|
|
254
|
+
computeRadialFrame,
|
|
255
|
+
estimateLabelWidthPx,
|
|
256
|
+
measureContainerPx,
|
|
257
|
+
observeInnerBox
|
|
258
|
+
};
|
|
259
|
+
//# sourceMappingURL=radial-frame.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"radial-frame.js","sources":["../../src/svghelpers/radial-frame.ts"],"sourcesContent":["import type {WatchArea} from '../navigation-instruments/watch/watch.js';\nimport {computeZoomToFitArcFrame, type ZoomToFitArcFrame} from './arc-frame.js';\n\n/**\n * Shared frame computation for circular/radial watch-based instruments.\n *\n * Every radial instrument (watch, instrument-radial/gauge-radial, speed-gauge,\n * azimuth-thruster, compass, heading, rudder, pitch/roll, …) needs the same\n * three outputs to render: an SVG viewBox for `<obc-watch>` and its overlay,\n * a zoom radius offset, and — since issue #1021 — enough reserved room around\n * the outer ring that tick labels are not clipped when the instrument shrinks\n * inside a flex container.\n *\n * `computeRadialFrame()` is the single source of truth for that geometry.\n * Consumers call it once per render and pass the resulting frame to\n * `<obc-watch .arcFrame=...>` (which uses it verbatim) and to their own\n * overlay `<svg viewBox=...>`, guaranteeing the two layers stay aligned.\n *\n * ## Label reserve (issue #1021)\n *\n * Tick labels render at `font-size: calc(12px / scale)` so they keep a\n * constant on-screen size while the ring scales. Their footprint in SVG units\n * is therefore `px / scale`, where `scale = containerPx / viewBoxSide` — the\n * smaller the instrument, the more SVG units a label needs. The frame solves\n * the resulting equation in closed form:\n *\n * ```\n * side/2 >= (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS) + P/scale\n * scale = containerPx / side\n * => side = 2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS) / (1 - 2P/containerPx)\n * ```\n *\n * where `P` is the label's pixel cost beyond the ring (anchor offsets + text\n * width + edge padding). The viewBox never shrinks below the legacy\n * `(176 + basePadding) * 2` box, so instruments without labels — or with\n * labels that already fit — render byte-identical to the pre-#1021 output.\n *\n * When the reserve would exceed `LABEL_RESERVE_MAX_FRACTION` of the box, the\n * frame reports `labelsHidden: true` and falls back to the base box; callers\n * must then strip tick label texts (graceful degradation instead of clipping,\n * mirroring the canvas charts).\n *\n * ## Equal circumference (`faceDiameter`)\n *\n * When `faceDiameter` (px) is given, the outer-ring diameter is pinned:\n * `scale = faceDiameter / 368`. The frame then reports `hostWidthPx` /\n * `hostHeightPx` so the host element can adopt a fixed intrinsic size, and\n * instruments sharing a `faceDiameter` value have identical ring\n * circumference regardless of label width or arc extent — the SVG counterpart\n * of `obc-donut-chart`'s fixed-height ⇒ fixed-circumference contract.\n */\n\n// Keep in sync with watch.ts OUTER_RING_RADIUS (importing it would be circular).\nconst OUTER_RING_RADIUS = 368 / 2;\n/** Legacy viewBox base: `side = (RADIAL_VIEWBOX_BASE + padding) * 2`. */\nconst RADIAL_VIEWBOX_BASE = 176;\n/** Non-scaled part of the tickmark text radius pad (`3/scale + 3` in tickmark.ts). */\nconst LABEL_RADIAL_PAD_UNITS = 3;\n/** Scaled label anchor offsets in px: `3/scale` textRadius pad + `6/scale` xOffset. */\nconst LABEL_ANCHOR_OFFSET_PX = 9;\n/** Breathing room between the label's far edge and the viewBox edge, in px. */\nconst LABEL_EDGE_PAD_PX = 4;\n/** Approximate digit advance at 12px font-instrument-label. */\nexport const DIGIT_WIDTH_PX = 7;\n/** Compass NSWE label width assumed by label.ts (`labelWidth = 16`). */\nexport const NSWE_LABEL_WIDTH_PX = 16;\n/**\n * North-arrow glyph allowance for outside-decor reserves (px). The arrow\n * renders regardless of `showLabels`, so consumers charge this portion\n * whenever the arrow sits outside the ring.\n */\nexport const NORTH_ARROW_WIDTH_PX = 16;\n/**\n * Labels are dropped (not clipped) when their reserve would exceed this share\n * of the box. 0.45 makes a 4-digit gauge hide labels below a ~182px container\n * — near the canvas charts' fixed MIN_HEIGHT_WITH_LABELS = 192px threshold —\n * while 2-digit labels survive down to ~120px. Designers may later prefer a\n * fixed pixel threshold instead (see charthelpers/constants.ts).\n */\nexport const LABEL_RESERVE_MAX_FRACTION = 0.45;\n/**\n * Pixel drop of \"Max-min\" end labels (`endLabelsMaxMin`) below the ±90° line:\n * anchored at `12px/scale` with `dominant-baseline: central` (tickmark.ts),\n * so the glyph bottom reaches ~12 + 6 + descender ≈ 20px — plus 1px pad.\n */\nexport const END_MAXMIN_LABEL_DROP_PX = 21;\n/**\n * Pixel drop of regular side labels at the ±90° ends: glyph centered on the\n * line (half-height ~6px + descender), plus pad.\n */\nexport const SIDE_LABEL_DROP_PX = 13;\n\n/** Per-edge viewBox crops in percent (0–100), matching obc-watch `clip*`. */\nexport interface RadialClips {\n top: number;\n bottom: number;\n left: number;\n right: number;\n}\n\nexport interface RadialFrameOptions {\n /** Legacy padding: the frame never shrinks below `(176 + basePadding) * 2`. */\n basePadding: number;\n /** Max label pixel width at 12px font; 0/undefined = no outside labels. */\n labelWidthPx?: number;\n /** Un-zoomed viewBox crops; ignored when `zoomToFitArc` is set. */\n clips?: RadialClips;\n /**\n * Pixel extent the outside labels need past the ±90° horizontal line\n * toward a top/bottom crop edge (see END_MAXMIN_LABEL_DROP_PX /\n * SIDE_LABEL_DROP_PX). When a `clips.top`/`clips.bottom` crop would cut\n * into that drop, the clip is lowered so the labels stay inside the box.\n * Absent/0 keeps the requested clips untouched (legacy geometry).\n */\n labelDropPx?: number;\n /** Host client box in px; absent or zero ⇒ scale-1 fallback (first paint). */\n containerPx?: {width: number; height: number};\n /** Outer-ring diameter in px; pins scale for equal circumference (mode b). */\n faceDiameter?: number;\n zoomToFitArc?: boolean;\n /** Visible arc segments; required for the zoom path. */\n areas?: WatchArea[];\n /** Innermost visible radius (depends on WatchCircleType); zoom path only. */\n innerRadius?: number;\n}\n\nexport interface RadialFrame extends ZoomToFitArcFrame {\n /** Px per SVG unit implied by container or faceDiameter (1 when unknown). */\n scale: number;\n /** SVG units added beyond the base geometry for labels (0 when none/hidden). */\n labelReserve: number;\n /** True when the label reserve exceeded the cap; callers strip tick texts. */\n labelsHidden: boolean;\n /**\n * True when `labelDropPx` lowered a top/bottom clip: the box's aspect no\n * longer matches the requested clip fractions, so hosts with an\n * aspect-ratio derived from those fractions must follow the frame instead.\n */\n clipsAdjusted: boolean;\n /** Fixed host size in px; defined only when `faceDiameter` is set. */\n hostWidthPx?: number;\n /** Fixed host size in px; defined only when `faceDiameter` is set. */\n hostHeightPx?: number;\n}\n\n/**\n * Measure the host's available box in px. Radial instrument hosts render\n * inline by default, so clientWidth/Height are often 0 — fall back to the\n * parent box, like obc-watch's getScale() has always done (issue #1032).\n */\nexport function measureContainerPx(host: HTMLElement): {\n width: number;\n height: number;\n} {\n let width = host.clientWidth;\n let height = host.clientHeight;\n if (width === 0 || height === 0) {\n const box = host.parentElement?.getBoundingClientRect();\n if (box) {\n width = box.width;\n height = box.height;\n }\n }\n return {width, height};\n}\n\n/**\n * Apply/remove the fixed intrinsic host size a `faceDiameter` frame pins.\n * The hosts render inline by default, so a pinned size also needs\n * `display: block` for width/height to apply. Returns whether the size is\n * currently pinned; pass that back as `wasPinned` on the next call so an\n * unpinned host's own inline styles are never clobbered.\n */\nexport function applyPinnedHostSize(\n host: HTMLElement,\n frame: RadialFrame | undefined,\n wasPinned: boolean\n): boolean {\n if (frame?.hostWidthPx !== undefined && frame.hostHeightPx !== undefined) {\n host.style.width = `${frame.hostWidthPx}px`;\n host.style.height = `${frame.hostHeightPx}px`;\n host.style.display = 'block';\n return true;\n }\n if (wasPinned) {\n host.style.removeProperty('width');\n host.style.removeProperty('height');\n host.style.removeProperty('display');\n }\n return false;\n}\n\n/**\n * Additionally observe an internal element that always generates a box (the\n * shadow `<svg>` or `.container`). Several radial hosts render inline (no\n * `:host {display}` rule), and ResizeObserver never fires for inline boxes —\n * the host reports a permanent 0×0 size, so a controller observing only the\n * host misses every resize. That freezes the container-dependent frame and\n * the `--scale` font counter-scaling at their first-paint values.\n */\nexport function observeInnerBox(\n controller: {observe(target: Element): void},\n renderRoot: ParentNode\n): void {\n const box = renderRoot.querySelector('svg, .container');\n if (box) {\n controller.observe(box);\n }\n}\n\n/**\n * Estimate the widest label's pixel width at the 12px instrument label font.\n */\nexport function estimateLabelWidthPx(\n texts: readonly (string | undefined)[]\n): number {\n let maxChars = 0;\n for (const t of texts) {\n if (t !== undefined && t.length > maxChars) {\n maxChars = t.length;\n }\n }\n return maxChars * DIGIT_WIDTH_PX;\n}\n\nfunction clipFractions(clips?: RadialClips): {fx: number; fy: number} {\n if (!clips) {\n return {fx: 1, fy: 1};\n }\n const fx = 1 - (clips.left + clips.right) / 100;\n const fy = 1 - (clips.top + clips.bottom) / 100;\n return {fx: fx > 0 ? fx : 1, fy: fy > 0 ? fy : 1};\n}\n\nfunction buildViewBox(side: number, clips?: RadialClips): ZoomToFitArcFrame {\n const {fx, fy} = clipFractions(clips);\n const width = side * fx;\n const height = side * fy;\n const x = -side / 2 + (side * (clips?.left ?? 0)) / 100;\n const y = -side / 2 + (side * (clips?.top ?? 0)) / 100;\n return {\n x,\n y,\n width,\n height,\n viewBox: `${x} ${y} ${width} ${height}`,\n radiusOffset: 0,\n };\n}\n\nfunction pinnedScale(opts: RadialFrameOptions): number | undefined {\n if (opts.faceDiameter === undefined) {\n return undefined;\n }\n const scale = opts.faceDiameter / (2 * OUTER_RING_RADIUS);\n return Number.isFinite(scale) && scale > 0 ? scale : undefined;\n}\n\n/** Container px available for the full (un-clipped) square, or undefined. */\nfunction effectiveContainer(\n opts: RadialFrameOptions,\n fx: number,\n fy: number\n): number | undefined {\n const w = opts.containerPx?.width ?? 0;\n const h = opts.containerPx?.height ?? 0;\n if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {\n return undefined;\n }\n return Math.min(w / fx, h / fy);\n}\n\nfunction containerScale(\n opts: RadialFrameOptions,\n frame: {width: number; height: number}\n): number | undefined {\n const w = opts.containerPx?.width ?? 0;\n const h = opts.containerPx?.height ?? 0;\n if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {\n return undefined;\n }\n const scale = Math.min(w / frame.width, h / frame.height);\n return Number.isFinite(scale) && scale > 0 ? scale : undefined;\n}\n\nfunction withHostPx(\n frame: ZoomToFitArcFrame,\n pinned: boolean,\n scale: number,\n labelReserve: number,\n labelsHidden: boolean,\n clipsAdjusted = false\n): RadialFrame {\n const result: RadialFrame = {\n ...frame,\n scale,\n labelReserve,\n labelsHidden,\n clipsAdjusted,\n };\n if (pinned) {\n result.hostWidthPx = frame.width * scale;\n result.hostHeightPx = frame.height * scale;\n }\n return result;\n}\n\n/**\n * Solve the un-zoomed box side and label visibility for a given clip window.\n */\nfunction solveSide(\n opts: RadialFrameOptions,\n labelCostPx: number,\n clips: RadialClips | undefined\n): {side: number; labelsHidden: boolean} {\n const baseSide = (RADIAL_VIEWBOX_BASE + opts.basePadding) * 2;\n const {fx, fy} = clipFractions(clips);\n const labelSideAt = (scale: number): number =>\n 2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS + labelCostPx / scale);\n\n let side = baseSide;\n let labelsHidden = false;\n\n if (labelCostPx > 0) {\n const pinned = pinnedScale(opts);\n if (pinned !== undefined) {\n const grown = Math.max(baseSide, labelSideAt(pinned));\n labelsHidden =\n (2 * (labelCostPx / pinned)) / grown > LABEL_RESERVE_MAX_FRACTION;\n side = labelsHidden ? baseSide : grown;\n } else {\n const container = effectiveContainer(opts, fx, fy);\n if (container === undefined) {\n side = Math.max(baseSide, labelSideAt(1));\n } else if ((2 * labelCostPx) / container > LABEL_RESERVE_MAX_FRACTION) {\n labelsHidden = true;\n } else {\n side = Math.max(\n baseSide,\n (2 * (OUTER_RING_RADIUS + LABEL_RADIAL_PAD_UNITS)) /\n (1 - (2 * labelCostPx) / container)\n );\n }\n }\n }\n\n return {side, labelsHidden};\n}\n\n/**\n * Lower a top/bottom clip so the crop edge keeps `labelDropPx / scale` SVG\n * units past the ±90° horizontal line (where the arc end labels hang).\n * Returns the input object unchanged when the requested clips already fit.\n */\nfunction adjustClipsForLabelDrop(\n clips: RadialClips,\n side: number,\n scale: number,\n labelDropPx: number\n): RadialClips {\n const requiredUnits = labelDropPx / scale;\n const requiredClipPct = Math.max(0, (0.5 - requiredUnits / side) * 100);\n const bottom =\n clips.bottom > requiredClipPct ? requiredClipPct : clips.bottom;\n const top = clips.top > requiredClipPct ? requiredClipPct : clips.top;\n if (bottom === clips.bottom && top === clips.top) {\n return clips;\n }\n return {...clips, top, bottom};\n}\n\n/**\n * Compute the viewBox frame for a radial instrument, reserving width-aware\n * room for outside tick labels. See the module documentation for the model.\n */\nexport function computeRadialFrame(opts: RadialFrameOptions): RadialFrame {\n const labelWidthPx = opts.labelWidthPx ?? 0;\n const labelCostPx =\n labelWidthPx > 0\n ? LABEL_ANCHOR_OFFSET_PX + labelWidthPx + LABEL_EDGE_PAD_PX\n : 0;\n\n if (opts.zoomToFitArc && opts.areas && opts.areas.length > 0) {\n return computeZoomedFrame(opts, labelCostPx);\n }\n\n const baseSide = (RADIAL_VIEWBOX_BASE + opts.basePadding) * 2;\n let clips = opts.clips;\n let {side, labelsHidden} = solveSide(opts, labelCostPx, clips);\n\n // Label drop past the ±90° line: lower a top/bottom crop that would cut\n // into it. The clip change alters the container fit (fy grows), so run a\n // second pass; visually exact after two.\n const labelDropPx = opts.labelDropPx ?? 0;\n if (labelDropPx > 0 && labelCostPx > 0 && !labelsHidden && clips) {\n for (let i = 0; i < 2; i++) {\n const scale =\n pinnedScale(opts) ??\n containerScale(opts, buildViewBox(side, clips)) ??\n 1;\n const adjusted = adjustClipsForLabelDrop(\n clips!,\n side,\n scale,\n labelDropPx\n );\n if (adjusted === clips) {\n break;\n }\n clips = adjusted;\n const solved = solveSide(opts, labelCostPx, clips);\n side = solved.side;\n labelsHidden = solved.labelsHidden;\n }\n if (labelsHidden) {\n // The taller window pushed the reserve past the cap: labels are\n // stripped, so the drop is moot — restore the requested clips.\n clips = opts.clips;\n const solved = solveSide(opts, labelCostPx, clips);\n side = solved.side;\n labelsHidden = solved.labelsHidden;\n }\n }\n\n const frame = buildViewBox(side, clips);\n const pinned = pinnedScale(opts);\n const scale = pinned ?? containerScale(opts, frame) ?? 1;\n return withHostPx(\n frame,\n pinned !== undefined,\n scale,\n (side - baseSide) / 2,\n labelsHidden,\n clips !== opts.clips\n );\n}\n\nfunction computeZoomedFrame(\n opts: RadialFrameOptions,\n labelCostPx: number\n): RadialFrame {\n let reserve = 0;\n let labelsHidden = false;\n let frame: ZoomToFitArcFrame = zoomFrameAt(opts, opts.basePadding);\n\n if (labelCostPx > 0) {\n // Fixed-point on the extension: the reserve depends on the scale, and the\n // scale on the frame size. Converges monotonically; two passes suffice.\n for (let i = 0; i < 2; i++) {\n const passScale = zoomedScale(opts, frame);\n const reserveUnits = labelCostPx / passScale;\n if (\n (2 * reserveUnits) / Math.max(frame.width, frame.height) >\n LABEL_RESERVE_MAX_FRACTION\n ) {\n labelsHidden = true;\n reserve = 0;\n frame = zoomFrameAt(opts, opts.basePadding);\n break;\n }\n reserve = reserveUnits;\n frame = zoomFrameAt(opts, opts.basePadding + reserve);\n }\n }\n\n const pinned = pinnedZoomedScale(opts, frame);\n const scale = pinned ?? containerScale(opts, frame) ?? 1;\n return withHostPx(\n frame,\n pinned !== undefined,\n scale,\n labelsHidden ? 0 : reserve,\n labelsHidden\n );\n}\n\nfunction zoomFrameAt(\n opts: RadialFrameOptions,\n extension: number\n): ZoomToFitArcFrame {\n return computeZoomToFitArcFrame({\n areas: opts.areas!,\n outerRadius: OUTER_RING_RADIUS,\n innerRadius: opts.innerRadius ?? OUTER_RING_RADIUS,\n extension,\n targetSize: (RADIAL_VIEWBOX_BASE + extension) * 2,\n });\n}\n\n/** Pin scale for a zoomed frame, or undefined when `faceDiameter` is unusable. */\nfunction pinnedZoomedScale(\n opts: RadialFrameOptions,\n frame: ZoomToFitArcFrame\n): number | undefined {\n if (opts.faceDiameter === undefined) {\n return undefined;\n }\n const scale =\n opts.faceDiameter / (2 * (OUTER_RING_RADIUS + frame.radiusOffset));\n return Number.isFinite(scale) && scale > 0 ? scale : undefined;\n}\n\nfunction zoomedScale(\n opts: RadialFrameOptions,\n frame: ZoomToFitArcFrame\n): number {\n return pinnedZoomedScale(opts, frame) ?? containerScale(opts, frame) ?? 1;\n}\n"],"names":["scale"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
* - **Empty description (warning):** a `@slot`/`@fires` tag with only a name and
|
|
32
32
|
* no descriptive text. These become blank cells in the manifest / Storybook
|
|
33
33
|
* controls; warnings do not fail CI.
|
|
34
|
+
* - **Class-level `@property` tag (error):** properties must be documented
|
|
35
|
+
* inline above their field declarations (AGENTS.md §3.6). A tag in the class
|
|
36
|
+
* JSDoc block overrides the inline doc and can inject a ghost member for a
|
|
37
|
+
* non-existent property (issue #1043). A short allowlist covers documented
|
|
38
|
+
* exceptions (property-injecting mixins, one legacy chart-base tag). A
|
|
39
|
+
* class-level `@attr`/`@attribute` that documents a CSS-only attribute with
|
|
40
|
+
* no backing `@property` field is allowed and is not flagged.
|
|
34
41
|
*
|
|
35
42
|
* There is deliberately no phantom-`@fires` *error*: unlike slots (which must be
|
|
36
43
|
* a `<slot>` element in the component's own shadow DOM), a documented event may
|
|
@@ -304,6 +311,34 @@ async function run(): Promise<void> {
|
|
|
304
311
|
}
|
|
305
312
|
}
|
|
306
313
|
|
|
314
|
+
// Class-level @property / @attr JSDoc tags (AGENTS.md §3.6): properties must be
|
|
315
|
+
// documented inline above their field declarations. A class-level tag overrides
|
|
316
|
+
// the inline doc and can inject a ghost member for a property that does not
|
|
317
|
+
// exist (issue #1043). Allowlisted files are documented exceptions: mixins that
|
|
318
|
+
// inject properties into consumers, and one legacy `fill` tag on the chart base.
|
|
319
|
+
const PROPERTY_TAG_ALLOWLIST = [
|
|
320
|
+
'svghelpers/setpoint-mixin.ts',
|
|
321
|
+
'svghelpers/setpoint-bundle.ts',
|
|
322
|
+
'building-blocks/chart-line/chart-line-base.ts',
|
|
323
|
+
];
|
|
324
|
+
// Only `@property` is flagged. `@attr`/`@attribute` in a class block is the
|
|
325
|
+
// legitimate way to document a CSS-only attribute that has no backing
|
|
326
|
+
// `@property` field (e.g. slider's `hugcontainer`), so it is not an error.
|
|
327
|
+
// Matches both the multi-line (` * @property`) and single-line
|
|
328
|
+
// (`/** @property`) JSDoc block-comment forms.
|
|
329
|
+
const propTagRe = /^[ \t]*(?:\/\*\*|\*)[ \t]*@property\b/gm;
|
|
330
|
+
for (const rel of files.sort()) {
|
|
331
|
+
if (PROPERTY_TAG_ALLOWLIST.some((a) => rel.endsWith(a))) continue;
|
|
332
|
+
const source = fs.readFileSync(path.join(cwd, rel), 'utf8');
|
|
333
|
+
const matches = source.match(propTagRe);
|
|
334
|
+
if (matches && matches.length > 0) {
|
|
335
|
+
errors.push({
|
|
336
|
+
file: rel,
|
|
337
|
+
message: `has ${matches.length} class-level @property JSDoc tag(s) — document each property inline above its field declaration instead (AGENTS.md §3.6; these override inline docs and can create ghost manifest members)`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
307
342
|
console.log('Slot & event documentation audit');
|
|
308
343
|
console.log(`Scanned ${componentCount} registered components under src/`);
|
|
309
344
|
|
|
@@ -138,75 +138,77 @@ export type DonutChartDataItem = {
|
|
|
138
138
|
* </script>
|
|
139
139
|
* ```
|
|
140
140
|
*
|
|
141
|
-
* @property {Array<{label: string, value: number}>} data - Chart data segments (set via JavaScript)
|
|
142
|
-
* @property {string[]} colors - Custom segment colors (set via JavaScript) with fallback to theme palette
|
|
143
|
-
* @property {boolean} half - Whether to display as half-circle (180°) or full circle (360°), default: false
|
|
144
|
-
* @property {boolean} showOuterLabels - Show outer labels, default: false
|
|
145
|
-
* @property {boolean} showUnit - Whether to show unit in labels, default: false
|
|
146
|
-
* @property {string} outerLabelUnit - Unit string to append to outer labels, default: "%"
|
|
147
|
-
* @property {number} outerLabelMaxLength - Maximum character length for labels before trim (0 = no limit), default: 0
|
|
148
|
-
* @property {number} outerLabelDecimalPlaces - Number of decimal places in labels, default: 0
|
|
149
|
-
* @property {string} centerReadoutLabel - Text label shown below the center total value, default: "Total"
|
|
150
|
-
* @property {string} centerReadoutUnit - Unit string shown inline after the label in the center readout, default: "%"
|
|
151
|
-
* @property {number} max - Maximum value for calculating remaining empty sector, default: 100
|
|
152
|
-
* @property {number} thickness - Donut ring thickness in pixels, default: 24
|
|
153
|
-
* @property {boolean} showDebugOverlay - Show debug overlay for development, default: false
|
|
154
|
-
* @property {number} fixedHeight - Fixed height of the chart in pixels (mandatory, determines chart circumference), default: 320. The chart's circumference is always based on this fixed height to match other radial instruments.
|
|
155
|
-
* @property {boolean} legend - Whether to display the legend below the chart, default: false
|
|
156
141
|
* @beta
|
|
157
142
|
*/
|
|
158
143
|
@customElement('obc-donut-chart')
|
|
159
144
|
export class ObcDonutChart extends LitElement {
|
|
145
|
+
/** Chart data segments (set via JavaScript) */
|
|
160
146
|
@property({type: Array, attribute: false})
|
|
161
147
|
data: DonutChartDataItem[] = [];
|
|
162
148
|
|
|
149
|
+
/** Custom segment colors (set via JavaScript) with fallback to theme palette */
|
|
163
150
|
@property({type: Array, attribute: false})
|
|
164
151
|
colors: string[] = [];
|
|
165
152
|
|
|
166
153
|
@property({type: String})
|
|
167
154
|
priority: Priority = Priority.regular;
|
|
168
155
|
|
|
156
|
+
/** Whether to display as half-circle (180°) or full circle (360°), default: false */
|
|
169
157
|
@property({type: Boolean, reflect: true})
|
|
170
158
|
half = false;
|
|
171
159
|
|
|
160
|
+
/** Show outer labels, default: false */
|
|
172
161
|
@property({type: Boolean})
|
|
173
162
|
showOuterLabels = false;
|
|
174
163
|
|
|
164
|
+
/** Whether to show unit in labels, default: false */
|
|
175
165
|
@property({type: Boolean})
|
|
176
166
|
showUnit = false;
|
|
177
167
|
|
|
168
|
+
/** Unit string to append to outer labels, default: "%" */
|
|
178
169
|
@property({type: String})
|
|
179
170
|
outerLabelUnit = '%';
|
|
180
171
|
|
|
181
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Maximum character length for labels before trim (0 = no limit), default: 0
|
|
174
|
+
* @availableWhen showOuterLabels==true
|
|
175
|
+
*/
|
|
182
176
|
@property({
|
|
183
177
|
type: Number,
|
|
184
178
|
})
|
|
185
179
|
outerLabelMaxLength = 0;
|
|
186
180
|
|
|
181
|
+
/** Number of decimal places in labels, default: 0 */
|
|
187
182
|
@property({
|
|
188
183
|
type: Number,
|
|
189
184
|
})
|
|
190
185
|
outerLabelDecimalPlaces = 0;
|
|
191
186
|
|
|
187
|
+
/** Text label shown below the center total value, default: "Total" */
|
|
192
188
|
@property({type: String})
|
|
193
189
|
centerReadoutLabel = 'Total';
|
|
194
190
|
|
|
191
|
+
/** Unit string shown inline after the label in the center readout, default: "%" */
|
|
195
192
|
@property({type: String})
|
|
196
193
|
centerReadoutUnit = '%';
|
|
197
194
|
|
|
195
|
+
/** Maximum value for calculating remaining empty sector, default: 100 */
|
|
198
196
|
@property({type: Number})
|
|
199
197
|
max = 100;
|
|
200
198
|
|
|
199
|
+
/** Donut ring thickness in pixels, default: 24 */
|
|
201
200
|
@property({type: Number})
|
|
202
201
|
thickness = 24;
|
|
203
202
|
|
|
203
|
+
/** Whether to display the legend below the chart, default: false */
|
|
204
204
|
@property({type: Boolean, reflect: true})
|
|
205
205
|
legend = false;
|
|
206
206
|
|
|
207
|
+
/** Show debug overlay for development, default: false */
|
|
207
208
|
@property({type: Boolean, reflect: true})
|
|
208
209
|
showDebugOverlay = false;
|
|
209
210
|
|
|
211
|
+
/** Fixed height of the chart in pixels (determines chart circumference), default: 320. The chart's circumference is always based on this fixed height to match other radial instruments. */
|
|
210
212
|
@property({type: Number, reflect: true})
|
|
211
213
|
fixedHeight = 320;
|
|
212
214
|
|
|
@@ -156,59 +156,61 @@ export type PieChartDataItem = {
|
|
|
156
156
|
* </script>
|
|
157
157
|
* ```
|
|
158
158
|
*
|
|
159
|
-
* @property {Array<{label: string, value: number, children?: Array<{label: string, value: number}>}>} data - Chart data segments with optional children subsegments for sunburst mode (set via JavaScript)
|
|
160
|
-
* @property {string[]} colors - Custom segment colors (set via JavaScript) with fallback to theme palette
|
|
161
|
-
* @property {boolean} showOuterLabels - Show outer labels, default: false
|
|
162
|
-
* @property {boolean} showUnit - Whether to show unit in labels, default: false
|
|
163
|
-
* @property {boolean} sunburst - Enable sunburst mode with interactive children subsegments, default: false
|
|
164
|
-
* @property {string} outerLabelUnit - Unit string to append to outer labels, default: "%"
|
|
165
|
-
* @property {number} outerLabelMaxLength - Maximum character length for labels before trim (0 = no limit), default: 0
|
|
166
|
-
* @property {number} outerLabelDecimalPlaces - Number of decimal places in labels, default: 0
|
|
167
|
-
* @property {boolean} showDebugOverlay - Show debug overlay for development, default: false
|
|
168
|
-
* @property {number} fixedHeight - Fixed height of the chart in pixels (mandatory, determines chart circumference), default: 320. The chart's circumference is always based on this fixed height to match other radial instruments.
|
|
169
|
-
* @property {boolean} legend - Whether to display the legend below the chart, default: false
|
|
170
159
|
* @beta
|
|
171
160
|
*/
|
|
172
161
|
@customElement('obc-pie-chart')
|
|
173
162
|
export class ObcPieChart extends LitElement {
|
|
163
|
+
/** Chart data segments with optional children subsegments for sunburst mode (set via JavaScript). */
|
|
174
164
|
@property({type: Array, attribute: false})
|
|
175
165
|
data: PieChartDataItem[] = [];
|
|
176
166
|
|
|
167
|
+
/** Custom segment colors (set via JavaScript) with fallback to theme palette */
|
|
177
168
|
@property({type: Array, attribute: false})
|
|
178
169
|
colors: string[] = [];
|
|
179
170
|
|
|
180
171
|
@property({type: String})
|
|
181
172
|
priority: Priority = Priority.regular;
|
|
182
173
|
|
|
174
|
+
/** Show outer labels, default: false */
|
|
183
175
|
@property({type: Boolean})
|
|
184
176
|
showOuterLabels = false;
|
|
185
177
|
|
|
178
|
+
/** Whether to show unit in labels, default: false */
|
|
186
179
|
@property({type: Boolean})
|
|
187
180
|
showUnit = false;
|
|
188
181
|
|
|
182
|
+
/** Enable sunburst mode with interactive children subsegments, default: false */
|
|
189
183
|
@property({type: Boolean})
|
|
190
184
|
sunburst = false;
|
|
191
185
|
|
|
186
|
+
/** Unit string to append to outer labels, default: "%" */
|
|
192
187
|
@property({type: String})
|
|
193
188
|
outerLabelUnit = '%';
|
|
194
189
|
|
|
195
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Maximum character length for labels before trim (0 = no limit), default: 0
|
|
192
|
+
* @availableWhen showOuterLabels==true
|
|
193
|
+
*/
|
|
196
194
|
@property({
|
|
197
195
|
type: Number,
|
|
198
196
|
})
|
|
199
197
|
outerLabelMaxLength = 0;
|
|
200
198
|
|
|
199
|
+
/** Number of decimal places in labels, default: 0 */
|
|
201
200
|
@property({
|
|
202
201
|
type: Number,
|
|
203
202
|
})
|
|
204
203
|
outerLabelDecimalPlaces = 0;
|
|
205
204
|
|
|
205
|
+
/** Whether to display the legend below the chart, default: false */
|
|
206
206
|
@property({type: Boolean, reflect: true})
|
|
207
207
|
legend = false;
|
|
208
208
|
|
|
209
|
+
/** Show debug overlay for development, default: false */
|
|
209
210
|
@property({type: Boolean, reflect: true})
|
|
210
211
|
showDebugOverlay = false;
|
|
211
212
|
|
|
213
|
+
/** Fixed height of the chart in pixels (determines chart circumference), default: 320. The chart's circumference is always based on this fixed height to match other radial instruments. */
|
|
212
214
|
@property({type: Number, reflect: true})
|
|
213
215
|
fixedHeight = 320;
|
|
214
216
|
|