@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.110 → 2.0.0-next.111

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 (52) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +2297 -1506
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +562 -0
  4. package/dist/automation/indicator-battery/indicator-battery.css.js +52 -0
  5. package/dist/automation/indicator-battery/indicator-battery.css.js.map +1 -0
  6. package/dist/automation/indicator-battery/indicator-battery.d.ts +65 -0
  7. package/dist/automation/indicator-battery/indicator-battery.d.ts.map +1 -0
  8. package/dist/automation/indicator-battery/indicator-battery.js +119 -0
  9. package/dist/automation/indicator-battery/indicator-battery.js.map +1 -0
  10. package/dist/automation/indicator-engine/indicator-engine.css.js +45 -0
  11. package/dist/automation/indicator-engine/indicator-engine.css.js.map +1 -0
  12. package/dist/automation/indicator-engine/indicator-engine.d.ts +34 -0
  13. package/dist/automation/indicator-engine/indicator-engine.d.ts.map +1 -0
  14. package/dist/automation/indicator-engine/indicator-engine.js +73 -0
  15. package/dist/automation/indicator-engine/indicator-engine.js.map +1 -0
  16. package/dist/automation/indicator-generator/indicator-generator-geometry.d.ts +30 -0
  17. package/dist/automation/indicator-generator/indicator-generator-geometry.d.ts.map +1 -0
  18. package/dist/automation/indicator-generator/indicator-generator-geometry.js +61 -0
  19. package/dist/automation/indicator-generator/indicator-generator-geometry.js.map +1 -0
  20. package/dist/automation/indicator-generator/indicator-generator.css.js +121 -0
  21. package/dist/automation/indicator-generator/indicator-generator.css.js.map +1 -0
  22. package/dist/automation/indicator-generator/indicator-generator.d.ts +57 -0
  23. package/dist/automation/indicator-generator/indicator-generator.d.ts.map +1 -0
  24. package/dist/automation/indicator-generator/indicator-generator.js +136 -0
  25. package/dist/automation/indicator-generator/indicator-generator.js.map +1 -0
  26. package/dist/automation/indicator-shared/linear-indicator.d.ts +46 -0
  27. package/dist/automation/indicator-shared/linear-indicator.d.ts.map +1 -0
  28. package/dist/automation/indicator-shared/linear-indicator.js +175 -0
  29. package/dist/automation/indicator-shared/linear-indicator.js.map +1 -0
  30. package/dist/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.css.js +48 -0
  31. package/dist/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.css.js.map +1 -0
  32. package/dist/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.d.ts +65 -0
  33. package/dist/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.d.ts.map +1 -0
  34. package/dist/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.js +105 -0
  35. package/dist/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/automation/indicator-battery/indicator-battery.css +42 -0
  38. package/src/automation/indicator-battery/indicator-battery.stories.ts +94 -0
  39. package/src/automation/indicator-battery/indicator-battery.ts +149 -0
  40. package/src/automation/indicator-engine/indicator-engine.css +35 -0
  41. package/src/automation/indicator-engine/indicator-engine.stories.ts +48 -0
  42. package/src/automation/indicator-engine/indicator-engine.ts +78 -0
  43. package/src/automation/indicator-generator/indicator-generator-geometry.spec.ts +62 -0
  44. package/src/automation/indicator-generator/indicator-generator-geometry.ts +63 -0
  45. package/src/automation/indicator-generator/indicator-generator.css +111 -0
  46. package/src/automation/indicator-generator/indicator-generator.stories.ts +103 -0
  47. package/src/automation/indicator-generator/indicator-generator.ts +184 -0
  48. package/src/automation/indicator-shared/linear-indicator.spec.ts +90 -0
  49. package/src/automation/indicator-shared/linear-indicator.ts +230 -0
  50. package/src/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.css +38 -0
  51. package/src/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.stories.ts +94 -0
  52. package/src/automation/indicator-tank-atmospheric/indicator-tank-atmospheric.ts +138 -0
@@ -0,0 +1,111 @@
1
+ :host {
2
+ display: block;
3
+ position: relative;
4
+ width: 48px;
5
+ height: 48px;
6
+ }
7
+
8
+ :host([variant="double-bar"]) {
9
+ width: 56px;
10
+ height: 56px;
11
+ }
12
+
13
+ svg {
14
+ display: block;
15
+ width: 100%;
16
+ height: 100%;
17
+ }
18
+
19
+ .silhouette {
20
+ fill: var(--instrument-frame-primary-color);
21
+ stroke: var(--border-silhouette-color);
22
+ }
23
+
24
+ .silhouette-ring {
25
+ fill: none;
26
+ stroke: var(--border-silhouette-color);
27
+ }
28
+
29
+ .disc {
30
+ fill: var(--instrument-frame-primary-color);
31
+ stroke: var(--instrument-frame-tertiary-color);
32
+ }
33
+
34
+ :host([value="off"]) .disc {
35
+ fill: var(--automation-button-off-background-color);
36
+ stroke: var(--automation-button-off-border-color);
37
+ }
38
+
39
+ :host([value="filled"]) .disc {
40
+ stroke: var(--element-neutral-color);
41
+ }
42
+
43
+ :host([value="filled"][variant="double-bar"]) .disc {
44
+ fill: var(--element-neutral-color);
45
+ stroke: var(--element-neutral-color);
46
+ }
47
+
48
+ :host([value="enhanced"]) .disc {
49
+ fill: var(--element-neutral-enhanced-color);
50
+ stroke: var(--base-blue-600);
51
+ }
52
+
53
+ :host([value="categorical"]) .disc {
54
+ fill: var(--base-teal-400);
55
+ stroke: var(--base-teal-500);
56
+ }
57
+
58
+ .outline {
59
+ fill: none;
60
+ stroke: var(--border-silhouette-color);
61
+ }
62
+
63
+ .ring-track {
64
+ fill: none;
65
+ stroke: var(--border-divider-color);
66
+ }
67
+
68
+ .ring-progress {
69
+ fill: none;
70
+ stroke: var(--element-neutral-color);
71
+ }
72
+
73
+ :host([value="enhanced"]) .ring-progress {
74
+ stroke: var(--element-neutral-enhanced-color);
75
+ }
76
+
77
+ :host([value="categorical"]) .ring-progress {
78
+ stroke: var(--base-teal-400);
79
+ }
80
+
81
+ .icon-wrapper {
82
+ position: absolute;
83
+ inset: 12px;
84
+ color: var(--element-neutral-color);
85
+ pointer-events: none;
86
+ }
87
+
88
+ :host([variant="double-bar"]) .icon-wrapper {
89
+ inset: 16px;
90
+ }
91
+
92
+ :host([value="off"]) .icon-wrapper {
93
+ color: var(--instrument-frame-tertiary-color);
94
+ }
95
+
96
+ :host([value="filled"]) .icon-wrapper,
97
+ :host([value="enhanced"]) .icon-wrapper,
98
+ :host([value="categorical"]) .icon-wrapper {
99
+ color: var(--instrument-frame-primary-color);
100
+ }
101
+
102
+ .icon-wrapper ::slotted(*),
103
+ .default-icon {
104
+ display: block;
105
+ width: 100%;
106
+ height: 100%;
107
+ }
108
+
109
+ .default-icon path {
110
+ fill: currentColor;
111
+ }
@@ -0,0 +1,103 @@
1
+ import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
+ import {html} from 'lit';
3
+ import {
4
+ ObcIndicatorGenerator,
5
+ IndicatorGeneratorVariant,
6
+ IndicatorGeneratorValue,
7
+ } from './indicator-generator.js';
8
+ import './indicator-generator.js';
9
+
10
+ const meta = {
11
+ title: 'Automation/Indicators/Indicator Generator',
12
+ component: 'obc-indicator-generator',
13
+ tags: ['autodocs', '6.0'],
14
+ args: {
15
+ variant: IndicatorGeneratorVariant.button,
16
+ value: IndicatorGeneratorValue.off,
17
+ level: 65,
18
+ secondaryLevel: 75,
19
+ },
20
+ argTypes: {
21
+ variant: {
22
+ control: 'radio',
23
+ options: Object.values(IndicatorGeneratorVariant),
24
+ },
25
+ value: {
26
+ control: 'radio',
27
+ options: Object.values(IndicatorGeneratorValue),
28
+ },
29
+ level: {control: {type: 'range', min: 0, max: 100}},
30
+ secondaryLevel: {control: {type: 'range', min: 0, max: 100}},
31
+ },
32
+ render: (args) => html`
33
+ <obc-indicator-generator
34
+ .variant=${args.variant}
35
+ .value=${args.value}
36
+ .level=${args.level}
37
+ .secondaryLevel=${args.secondaryLevel}
38
+ ></obc-indicator-generator>
39
+ `,
40
+ } satisfies Meta<Partial<ObcIndicatorGenerator>>;
41
+
42
+ export default meta;
43
+ type Story = StoryObj<Partial<ObcIndicatorGenerator>>;
44
+
45
+ export const Default: Story = {};
46
+
47
+ export const ButtonRegular: Story = {
48
+ args: {value: IndicatorGeneratorValue.regular},
49
+ };
50
+
51
+ export const ButtonEnhanced: Story = {
52
+ args: {value: IndicatorGeneratorValue.enhanced},
53
+ };
54
+
55
+ export const ButtonCategorical: Story = {
56
+ args: {value: IndicatorGeneratorValue.categorical},
57
+ };
58
+
59
+ export const BarRegular: Story = {
60
+ args: {
61
+ variant: IndicatorGeneratorVariant.bar,
62
+ value: IndicatorGeneratorValue.regular,
63
+ },
64
+ };
65
+
66
+ export const BarEnhanced: Story = {
67
+ args: {
68
+ variant: IndicatorGeneratorVariant.bar,
69
+ value: IndicatorGeneratorValue.enhanced,
70
+ },
71
+ };
72
+
73
+ export const BarCategorical: Story = {
74
+ args: {
75
+ variant: IndicatorGeneratorVariant.bar,
76
+ value: IndicatorGeneratorValue.categorical,
77
+ },
78
+ };
79
+
80
+ export const ButtonFilled: Story = {
81
+ args: {value: IndicatorGeneratorValue.filled},
82
+ };
83
+
84
+ export const BarFilled: Story = {
85
+ args: {
86
+ variant: IndicatorGeneratorVariant.bar,
87
+ value: IndicatorGeneratorValue.filled,
88
+ },
89
+ };
90
+
91
+ export const DoubleBar: Story = {
92
+ args: {
93
+ variant: IndicatorGeneratorVariant.doubleBar,
94
+ value: IndicatorGeneratorValue.regular,
95
+ },
96
+ };
97
+
98
+ export const DoubleBarFilled: Story = {
99
+ args: {
100
+ variant: IndicatorGeneratorVariant.doubleBar,
101
+ value: IndicatorGeneratorValue.filled,
102
+ },
103
+ };
@@ -0,0 +1,184 @@
1
+ import {LitElement, html, svg, unsafeCSS, nothing} from 'lit';
2
+ import {property} from 'lit/decorators.js';
3
+ import componentStyle from './indicator-generator.css?inline';
4
+ import {customElement} from '../../decorator.js';
5
+ import {
6
+ BUTTON_SILHOUETTE_RADIUS,
7
+ DISC_RADIUS,
8
+ FILLED_DISC_RADIUS,
9
+ BAR_SILHOUETTE_RADIUS,
10
+ BAR_SILHOUETTE_STROKE_WIDTH,
11
+ BAR_OUTLINE_RADIUS,
12
+ DOUBLE_BAR_OUTLINE_RADIUS,
13
+ OUTLINE_STROKE_WIDTH,
14
+ BAR_TRACK_RADIUS,
15
+ BAR_TRACK_STROKE_WIDTH,
16
+ DOUBLE_BAR_OUTER_RADIUS,
17
+ DOUBLE_BAR_OUTER_STROKE_WIDTH,
18
+ DOUBLE_BAR_INNER_RADIUS,
19
+ DOUBLE_BAR_INNER_STROKE_WIDTH,
20
+ arcPath,
21
+ progressSweep,
22
+ } from './indicator-generator-geometry.js';
23
+
24
+ export enum IndicatorGeneratorVariant {
25
+ button = 'button',
26
+ bar = 'bar',
27
+ doubleBar = 'double-bar',
28
+ }
29
+
30
+ export enum IndicatorGeneratorValue {
31
+ off = 'off',
32
+ regular = 'regular',
33
+ filled = 'filled',
34
+ enhanced = 'enhanced',
35
+ categorical = 'categorical',
36
+ }
37
+
38
+ const ICON_G_PATH =
39
+ 'M6.1686 5.216H10.6326V11.088C10.0353 11.2907 9.41127 11.4507 8.7606 11.568C8.1206 11.6853 7.38993 11.744 6.5686 11.744C5.43793 11.744 4.4726 11.52 3.6726 11.072C2.88327 10.6133 2.2806 9.94667 1.8646 9.072C1.4486 8.19733 1.2406 7.12533 1.2406 5.856C1.2406 4.66133 1.46993 3.62667 1.9286 2.752C2.39793 1.87733 3.07527 1.2 3.9606 0.72C4.84593 0.24 5.9286 0 7.2086 0C7.8166 0 8.41393 0.0640001 9.0006 0.192C9.58727 0.32 10.1206 0.490667 10.6006 0.704L9.8326 2.56C9.46993 2.37867 9.05927 2.22933 8.6006 2.112C8.1526 1.99467 7.68327 1.936 7.1926 1.936C6.4566 1.936 5.82193 2.10133 5.2886 2.432C4.76593 2.752 4.35527 3.21067 4.0566 3.808C3.7686 4.39467 3.6246 5.09333 3.6246 5.904C3.6246 6.66133 3.73127 7.33867 3.9446 7.936C4.15793 8.52267 4.49393 8.98133 4.9526 9.312C5.41127 9.64267 6.01393 9.808 6.7606 9.808C7.00593 9.808 7.21927 9.80267 7.4006 9.792C7.5926 9.77067 7.76327 9.74933 7.9126 9.728C8.0726 9.696 8.22727 9.66933 8.3766 9.648V7.152H6.1686V5.216Z';
40
+
41
+ const ICON_TILDE_PATH =
42
+ 'M4.20117 14.0059C5.30264 14.0679 6.22793 14.5801 7.02344 15.2207C7.64824 15.7239 8.18172 15.9676 8.7041 15.9971C9.21748 16.026 9.88939 15.8574 10.8096 15.1904L11.6191 14.6035L12.793 16.2227L11.9834 16.8096C10.8203 17.6526 9.7024 18.0567 8.5918 17.9941C7.49033 17.9321 6.56504 17.4199 5.76953 16.7793C5.14473 16.2761 4.61125 16.0324 4.08887 16.0029C3.57549 15.974 2.90358 16.1426 1.9834 16.8096L1.17383 17.3965L0 15.7773L0.80957 15.1904C1.97263 14.3473 3.09057 13.9433 4.20117 14.0059Z';
43
+
44
+ /**
45
+ * Compact generator status indicator (48×48; 56×56 as double-bar).
46
+ *
47
+ * ## Features / Variants
48
+ * - `variant`: `button` (plain disc), `bar` (disc with a circular progress
49
+ * ring) or `double-bar` (disc with two concentric progress rings)
50
+ * - `value`: visual scheme — `off`, `regular`, `filled`, `enhanced` or
51
+ * `categorical`; `off` hides the progress rings
52
+ * - `level`: 0–100 (%), sweep of the (outer) progress ring, clockwise from
53
+ * 12 o'clock
54
+ * - `secondaryLevel`: 0–100 (%), sweep of the inner ring (double-bar only)
55
+ *
56
+ * ## Usage Guidelines
57
+ * Use as an at-a-glance generator load indicator in dashboards and lists.
58
+ * For schematic (P&ID) placement with button behavior, use the electric
59
+ * generator automation devices instead.
60
+ *
61
+ * ## Slots
62
+ * | Slot | Condition | Purpose |
63
+ * | ------ | --------- | ------------------------------------------------ |
64
+ * | `icon` | always | Center icon, defaults to the generator G~ glyph |
65
+ *
66
+ * @slot icon - Center icon, defaults to the generator G~ glyph
67
+ */
68
+ @customElement('obc-indicator-generator')
69
+ export class ObcIndicatorGenerator extends LitElement {
70
+ @property({type: String, reflect: true}) variant: IndicatorGeneratorVariant =
71
+ IndicatorGeneratorVariant.button;
72
+ @property({type: String, reflect: true}) value: IndicatorGeneratorValue =
73
+ IndicatorGeneratorValue.off;
74
+ /** Outer progress ring sweep, 0-100 (%) */
75
+ @property({type: Number}) level = 0;
76
+ /** Inner progress ring sweep, 0-100 (%), double-bar only */
77
+ @property({type: Number}) secondaryLevel = 0;
78
+
79
+ private get center(): number {
80
+ return this.variant === IndicatorGeneratorVariant.doubleBar ? 28 : 24;
81
+ }
82
+
83
+ private get discRadius(): number {
84
+ if (this.value === IndicatorGeneratorValue.filled) {
85
+ return FILLED_DISC_RADIUS;
86
+ }
87
+ if (
88
+ this.variant === IndicatorGeneratorVariant.button ||
89
+ this.value === IndicatorGeneratorValue.off ||
90
+ this.value === IndicatorGeneratorValue.regular
91
+ ) {
92
+ return DISC_RADIUS;
93
+ }
94
+ return FILLED_DISC_RADIUS;
95
+ }
96
+
97
+ private get showOutline(): boolean {
98
+ if (this.variant === IndicatorGeneratorVariant.bar) {
99
+ return [
100
+ IndicatorGeneratorValue.filled,
101
+ IndicatorGeneratorValue.enhanced,
102
+ IndicatorGeneratorValue.categorical,
103
+ ].includes(this.value);
104
+ }
105
+ if (this.variant === IndicatorGeneratorVariant.doubleBar) {
106
+ return this.value !== IndicatorGeneratorValue.off;
107
+ }
108
+ return false;
109
+ }
110
+
111
+ private renderRing(radius: number, strokeWidth: number, level: number) {
112
+ const c = this.center;
113
+ const sweep = progressSweep(level);
114
+ return svg`
115
+ <circle class="ring-track" cx=${c} cy=${c} r=${radius} stroke-width=${strokeWidth} />
116
+ ${
117
+ sweep >= 360
118
+ ? svg`<circle class="ring-progress" cx=${c} cy=${c} r=${radius} stroke-width=${strokeWidth} />`
119
+ : sweep > 0
120
+ ? svg`<path class="ring-progress" d=${arcPath(c, c, radius, 0, sweep)} stroke-width=${strokeWidth} />`
121
+ : nothing
122
+ }
123
+ `;
124
+ }
125
+
126
+ override render() {
127
+ const c = this.center;
128
+ const doubleBar = this.variant === IndicatorGeneratorVariant.doubleBar;
129
+ const bar = this.variant === IndicatorGeneratorVariant.bar;
130
+ const showRings = this.value !== IndicatorGeneratorValue.off;
131
+ const size = doubleBar ? 56 : 48;
132
+ return html`
133
+ <svg viewBox="0 0 ${size} ${size}" aria-hidden="true">
134
+ ${this.variant === IndicatorGeneratorVariant.button
135
+ ? svg`<circle class="silhouette" cx=${c} cy=${c} r=${BUTTON_SILHOUETTE_RADIUS} />`
136
+ : nothing}
137
+ ${bar
138
+ ? svg`<circle class="silhouette-ring" cx=${c} cy=${c} r=${BAR_SILHOUETTE_RADIUS} stroke-width=${BAR_SILHOUETTE_STROKE_WIDTH} />`
139
+ : nothing}
140
+ <circle class="disc" cx=${c} cy=${c} r=${this.discRadius} />
141
+ ${this.showOutline
142
+ ? svg`<circle
143
+ class="outline"
144
+ cx=${c}
145
+ cy=${c}
146
+ r=${doubleBar ? DOUBLE_BAR_OUTLINE_RADIUS : BAR_OUTLINE_RADIUS}
147
+ stroke-width=${OUTLINE_STROKE_WIDTH}
148
+ />`
149
+ : nothing}
150
+ ${bar && showRings
151
+ ? this.renderRing(
152
+ BAR_TRACK_RADIUS,
153
+ BAR_TRACK_STROKE_WIDTH,
154
+ this.level
155
+ )
156
+ : nothing}
157
+ ${doubleBar && showRings
158
+ ? svg`
159
+ ${this.renderRing(DOUBLE_BAR_OUTER_RADIUS, DOUBLE_BAR_OUTER_STROKE_WIDTH, this.level)}
160
+ ${this.renderRing(DOUBLE_BAR_INNER_RADIUS, DOUBLE_BAR_INNER_STROKE_WIDTH, this.secondaryLevel)}
161
+ `
162
+ : nothing}
163
+ </svg>
164
+ <div class="icon-wrapper">
165
+ <slot name="icon">
166
+ <svg class="default-icon" viewBox="0 0 24 24">
167
+ <g transform="translate(5.604 3)">
168
+ <path d=${ICON_G_PATH} />
169
+ <path d=${ICON_TILDE_PATH} />
170
+ </g>
171
+ </svg>
172
+ </slot>
173
+ </div>
174
+ `;
175
+ }
176
+
177
+ static override styles = unsafeCSS(componentStyle);
178
+ }
179
+
180
+ declare global {
181
+ interface HTMLElementTagNameMap {
182
+ 'obc-indicator-generator': ObcIndicatorGenerator;
183
+ }
184
+ }
@@ -0,0 +1,90 @@
1
+ import {describe, it, expect} from 'vitest';
2
+ import {
3
+ clampPercent,
4
+ expandRect,
5
+ fillHeight,
6
+ trendPolylinePoints,
7
+ roundedLeftRectPath,
8
+ } from './linear-indicator.js';
9
+
10
+ describe('clampPercent', () => {
11
+ it('clamps below 0 to 0', () => {
12
+ expect(clampPercent(-5)).toBe(0);
13
+ });
14
+
15
+ it('clamps above 100 to 100', () => {
16
+ expect(clampPercent(140)).toBe(100);
17
+ });
18
+
19
+ it('maps NaN to 0', () => {
20
+ expect(clampPercent(Number.NaN)).toBe(0);
21
+ });
22
+
23
+ it('passes through in-range values', () => {
24
+ expect(clampPercent(39.29)).toBeCloseTo(39.29);
25
+ });
26
+ });
27
+
28
+ describe('expandRect', () => {
29
+ it('grows the rect by the amount on every side', () => {
30
+ expect(expandRect({x: 9, y: 11, width: 30, height: 26}, 1)).toEqual({
31
+ x: 8,
32
+ y: 10,
33
+ width: 32,
34
+ height: 28,
35
+ });
36
+ });
37
+ });
38
+
39
+ describe('fillHeight', () => {
40
+ const rect = {x: 8, y: 10, width: 32, height: 28};
41
+
42
+ it('is 0 at level 0', () => {
43
+ expect(fillHeight(rect, 0)).toBe(0);
44
+ });
45
+
46
+ it('is full height at 100', () => {
47
+ expect(fillHeight(rect, 100)).toBe(28);
48
+ });
49
+
50
+ it('scales linearly', () => {
51
+ expect(fillHeight(rect, 50)).toBeCloseTo(14);
52
+ });
53
+ });
54
+
55
+ describe('trendPolylinePoints', () => {
56
+ const rect = {x: 10, y: 8, width: 28, height: 32};
57
+
58
+ it('returns empty string for fewer than 2 points', () => {
59
+ expect(trendPolylinePoints(rect, [], 23)).toBe('');
60
+ expect(trendPolylinePoints(rect, [50], 23)).toBe('');
61
+ });
62
+
63
+ it('spans graphWidth and maps values top-down', () => {
64
+ const points = trendPolylinePoints(rect, [0, 100], 23).split(' ');
65
+ expect(points[0]).toBe('10,40');
66
+ expect(points[1]).toBe('33,8');
67
+ });
68
+
69
+ it('clamps out-of-range data', () => {
70
+ const points = trendPolylinePoints(rect, [-20, 150], 23).split(' ');
71
+ expect(points[0]).toBe('10,40');
72
+ expect(points[1]).toBe('33,8');
73
+ });
74
+ });
75
+
76
+ describe('roundedLeftRectPath', () => {
77
+ const rect = {x: 10, y: 8, width: 28, height: 32};
78
+
79
+ it('rounds only the two left corners', () => {
80
+ const d = roundedLeftRectPath(rect, 23, 2);
81
+ expect(d.startsWith('M 12 8')).toBe(true);
82
+ expect((d.match(/A/g) ?? []).length).toBe(2);
83
+ });
84
+
85
+ it('spans the given width with square right corners', () => {
86
+ const d = roundedLeftRectPath(rect, 23, 2);
87
+ expect(d).toContain('H 33');
88
+ expect(d.endsWith('Z')).toBe(true);
89
+ });
90
+ });