@blotch/dolan 22.2.1 → 22.2.3

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 (156) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +32 -10
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +129 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +41 -27
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +93 -9
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +149 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +285 -26
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/update.d.ts +6 -0
  46. package/dist/commands/update.js +59 -24
  47. package/dist/commands/update.js.map +1 -1
  48. package/dist/commands/update.test.js +273 -31
  49. package/dist/commands/update.test.js.map +1 -1
  50. package/dist/index.js +10 -3
  51. package/dist/index.js.map +1 -1
  52. package/dist/registry.d.ts +8 -0
  53. package/dist/registry.js +32 -24
  54. package/dist/registry.js.map +1 -1
  55. package/dist/registry.test.js +76 -37
  56. package/dist/registry.test.js.map +1 -1
  57. package/dist/testing/set-input-signal.d.ts +13 -0
  58. package/dist/testing/set-input-signal.js +20 -0
  59. package/dist/testing/set-input-signal.js.map +1 -0
  60. package/dist/testing/set-input-signal.test.d.ts +1 -0
  61. package/dist/testing/set-input-signal.test.js +20 -0
  62. package/dist/testing/set-input-signal.test.js.map +1 -0
  63. package/dist/ui/components/action-sheet/action-sheet.test.ts +48 -1
  64. package/dist/ui/components/action-sheet/action-sheet.ts +37 -9
  65. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +49 -2
  66. package/dist/ui/components/alert-dialog/alert-dialog.ts +43 -12
  67. package/dist/ui/components/area-chart/area-chart-render.test.ts +52 -110
  68. package/dist/ui/components/area-chart/area-chart.test.ts +59 -146
  69. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  70. package/dist/ui/components/area-chart/index.ts +2 -3
  71. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +47 -163
  72. package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -58
  73. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  74. package/dist/ui/components/bar-chart/index.ts +3 -3
  75. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +191 -9
  76. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +136 -20
  77. package/dist/ui/components/button/button-render.test.ts +19 -0
  78. package/dist/ui/components/button/button.test.ts +2 -15
  79. package/dist/ui/components/button/button.ts +18 -2
  80. package/dist/ui/components/button-group/button-group.test.ts +2 -21
  81. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +48 -163
  82. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +107 -85
  83. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  84. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  85. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  86. package/dist/ui/components/chart/chart-focus.ts +115 -0
  87. package/dist/ui/components/chart/chart-presets.test.ts +139 -0
  88. package/dist/ui/components/chart/chart-presets.ts +271 -0
  89. package/dist/ui/components/chart/chart-render.test.ts +730 -0
  90. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  91. package/dist/ui/components/chart/chart-scene.ts +483 -0
  92. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  93. package/dist/ui/components/chart/chart-theme.ts +198 -0
  94. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  95. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  96. package/dist/ui/components/chart/chart.ts +921 -0
  97. package/dist/ui/components/chart/index.ts +24 -0
  98. package/dist/ui/components/dialog/dialog.test.ts +49 -2
  99. package/dist/ui/components/dialog/dialog.ts +43 -12
  100. package/dist/ui/components/icon/icon-theme.test.ts +133 -0
  101. package/dist/ui/components/icon/icon-theme.ts +204 -0
  102. package/dist/ui/components/icon/icon.test.ts +51 -2
  103. package/dist/ui/components/icon/icon.ts +38 -21
  104. package/dist/ui/components/line-chart/index.ts +2 -3
  105. package/dist/ui/components/line-chart/line-chart-render.test.ts +69 -107
  106. package/dist/ui/components/line-chart/line-chart.test.ts +63 -67
  107. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  108. package/dist/ui/components/list/list.test.ts +69 -2
  109. package/dist/ui/components/list/list.ts +21 -15
  110. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +80 -1
  111. package/dist/ui/components/nav-drawer/nav-drawer.ts +66 -11
  112. package/dist/ui/components/pagination/pagination.test.ts +62 -0
  113. package/dist/ui/components/pagination/pagination.ts +55 -8
  114. package/dist/ui/components/scatter-chart/index.ts +2 -3
  115. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +51 -144
  116. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -47
  117. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  118. package/dist/ui/components/scroll-area/scroll-area.test.ts +37 -2
  119. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  120. package/dist/ui/components/spinner/spinner.test.ts +27 -0
  121. package/dist/ui/components/spinner/spinner.ts +33 -23
  122. package/dist/ui/components/toast/toast.test.ts +37 -0
  123. package/dist/ui/components/toast/toast.ts +20 -4
  124. package/dist/ui/theme/chart-colors.test.ts +30 -0
  125. package/dist/ui/theme/dark.css +8 -0
  126. package/dist/ui/theme/default.css +8 -0
  127. package/dist/ui/theme/icon-colors.test.ts +30 -0
  128. package/dist/ui/theme/tailwind-plugin.ts +10 -1
  129. package/dist/utils/installed.d.ts +17 -0
  130. package/dist/utils/installed.js +27 -0
  131. package/dist/utils/installed.js.map +1 -0
  132. package/dist/utils/installed.test.d.ts +1 -0
  133. package/dist/utils/installed.test.js +41 -0
  134. package/dist/utils/installed.test.js.map +1 -0
  135. package/dist/utils/interactive.d.ts +9 -0
  136. package/dist/utils/interactive.js +10 -0
  137. package/dist/utils/interactive.js.map +1 -0
  138. package/dist/utils/interactive.test.d.ts +1 -0
  139. package/dist/utils/interactive.test.js +24 -0
  140. package/dist/utils/interactive.test.js.map +1 -0
  141. package/dist/utils/packages.d.ts +36 -0
  142. package/dist/utils/packages.js +70 -0
  143. package/dist/utils/packages.js.map +1 -0
  144. package/dist/utils/packages.test.d.ts +1 -0
  145. package/dist/utils/packages.test.js +89 -0
  146. package/dist/utils/packages.test.js.map +1 -0
  147. package/dist/utils/resolve-paths.d.ts +17 -1
  148. package/dist/utils/resolve-paths.js +23 -3
  149. package/dist/utils/resolve-paths.js.map +1 -1
  150. package/dist/utils/resolve-paths.test.js +34 -2
  151. package/dist/utils/resolve-paths.test.js.map +1 -1
  152. package/package.json +5 -1
  153. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  154. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -549
  155. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  156. package/dist/ui/components/cartesian-chart/index.ts +0 -18
@@ -1,6 +1,8 @@
1
1
  import { Component, ViewEncapsulation, input } from '@angular/core';
2
2
  import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
3
3
 
4
+ import { cn } from '../../utils/cn';
5
+ import { injectIconColor } from './icon-theme';
4
6
  import { type IconName, ICONS } from './icons';
5
7
 
6
8
  const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
@@ -16,39 +18,54 @@ const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
16
18
  imports: [LYNX_ELEMENTS],
17
19
  encapsulation: ViewEncapsulation.None,
18
20
  template: `
19
- <svg [attr.content]="svgContent()" [attr.style]="sizeStyle()" />
21
+ <svg
22
+ [attr.content]="svgContent()"
23
+ [attr.style]="sizeStyle()"
24
+ [class]="svgClass()"
25
+ />
20
26
  `,
21
27
  })
22
28
  export class UiIcon {
23
29
  readonly name = input.required<IconName>();
24
30
  readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');
31
+ /**
32
+ * A literal color or a theme token (`var(--primary)`). Without one, the icon
33
+ * takes the color its container sets (see `UI_ICON_COLOR`), then the theme's
34
+ * foreground.
35
+ */
25
36
  readonly color = input<string | undefined>(undefined);
37
+ readonly userClass = input<string>('', { alias: 'class' });
38
+
39
+ readonly #resolveColor = injectIconColor();
26
40
 
27
41
  protected svgContent(): string {
28
- let svg = ICONS[this.name()];
29
- const c = this.color();
30
- if (c) {
31
- // Lynx SVG elements don't propagate the CSS `color` property into SVG
32
- // stroke attributes, so we substitute the literal `currentColor` in the
33
- // markup string rather than relying on `currentColor` CSS propagation.
34
- //
35
- // Uses split().join() instead of String.prototype.replaceAll(). replaceAll
36
- // is ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target;
37
- // because it's a runtime method (not syntax) the build's down-leveling does
38
- // not polyfill it, so it simply doesn't exist there. Calling it throws
39
- // "main-thread.js exception: not a function" from inside Angular change
40
- // detection, which aborts the whole tick before Lynx flushes the element
41
- // tree — so NO icon paints (not just a colored one), and the error count
42
- // climbs on every render/interaction as each tick re-runs and re-throws.
43
- // split/join is ES2019-safe and is preferred over `replace(/.../g)` here
44
- // because the search term is a plain literal and needs no regex escaping.
45
- svg = svg.split('currentColor').join(c);
46
- }
47
- return svg;
42
+ // Lynx SVG elements don't propagate the CSS `color` property into SVG
43
+ // stroke attributes, so we substitute the literal `currentColor` in the
44
+ // markup string rather than relying on `currentColor` CSS propagation.
45
+ // The color is resolved first: the SVG parser can't read `var(--x)` either
46
+ // (see icon-theme.ts).
47
+ //
48
+ // Uses split().join() instead of String.prototype.replaceAll(). replaceAll
49
+ // is ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target;
50
+ // because it's a runtime method (not syntax) the build's down-leveling does
51
+ // not polyfill it, so it simply doesn't exist there. Calling it throws
52
+ // "main-thread.js exception: not a function" from inside Angular change
53
+ // detection, which aborts the whole tick before Lynx flushes the element
54
+ // tree — so NO icon paints (not just a colored one), and the error count
55
+ // climbs on every render/interaction as each tick re-runs and re-throws.
56
+ // split/join is ES2019-safe and is preferred over `replace(/.../g)` here
57
+ // because the search term is a plain literal and needs no regex escaping.
58
+ return ICONS[this.name()]
59
+ .split('currentColor')
60
+ .join(this.#resolveColor(this.color()));
48
61
  }
49
62
 
50
63
  protected sizeStyle(): string {
51
64
  const px = SIZE_MAP[this.size()];
52
65
  return `width: ${px}px; height: ${px}px;`;
53
66
  }
67
+
68
+ protected svgClass(): string {
69
+ return cn(this.userClass());
70
+ }
54
71
  }
@@ -1,6 +1,5 @@
1
1
  export {
2
+ type LineChartOptions,
2
3
  UiLineChart,
3
- UiLineSeries,
4
- computeLineSegments,
5
- type LineSegment,
4
+ lineChartDefinition,
6
5
  } from './line-chart';
@@ -1,121 +1,83 @@
1
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
1
+ import { render } from '@blotch/angular-lynx-testing-library';
2
+ import { createChartRuntime } from '@tanstack/charts/runtime';
3
+ import type {
4
+ ChartDefinition,
5
+ ChartPoint,
6
+ SceneNode,
7
+ } from '@tanstack/charts/types';
2
8
  import { describe, expect, it, vi } from 'vitest';
3
9
  import { setInputSignal } from '../../../test-utils/set-input-signal';
4
- import { UiCartesianChart } from '../cartesian-chart/cartesian-chart';
5
- import { UiLineChart, UiLineSeries } from './line-chart';
10
+ import { UiLineChart } from './line-chart';
6
11
 
7
- describe('UiLineChart', () => {
8
- it('renders with default configuration', async () => {
9
- const { container } = await render(UiLineChart);
10
- expect(container).toBeTruthy();
11
- });
12
-
13
- it('computes axis domains from data', async () => {
14
- const { componentRef } = await render(UiLineChart);
15
- const inst = componentRef.instance as UiLineChart;
16
- setInputSignal(inst.data, [
17
- { x: 0, y: 0 },
18
- { x: 5, y: 10 },
19
- { x: 10, y: 5 },
20
- ]);
21
- await waitForUpdate();
22
- const cast = inst as unknown as {
23
- xAxis: () => { domain: readonly [number, number]; ticks: number[] };
24
- yAxis: () => { domain: readonly [number, number]; ticks: number[] };
25
- };
26
- expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(0);
27
- expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(10);
28
- expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(0);
29
- expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(10);
30
- });
12
+ const data = [
13
+ { x: 0, y: 10 },
14
+ { x: 5, y: 30 },
15
+ { x: 10, y: 20 },
16
+ ];
31
17
 
32
- it('honors explicit xMin / xMax / yMin / yMax overrides', async () => {
33
- const { componentRef } = await render(UiLineChart);
34
- const inst = componentRef.instance as UiLineChart;
35
- setInputSignal(inst.data, [
36
- { x: 0, y: 0 },
37
- { x: 10, y: 10 },
38
- ]);
39
- setInputSignal(inst.xMin, -5);
40
- setInputSignal(inst.xMax, 20);
41
- setInputSignal(inst.yMin, -1);
42
- setInputSignal(inst.yMax, 15);
43
- await waitForUpdate();
44
- const cast = inst as unknown as {
45
- xAxis: () => { domain: readonly [number, number] };
46
- yAxis: () => { domain: readonly [number, number] };
47
- };
48
- expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(-5);
49
- expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(20);
50
- expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(-1);
51
- expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(15);
52
- });
53
-
54
- it('exposes the default tick formatter used by the child cartesian chart', async () => {
55
- const { componentRef } = await render(UiLineChart);
56
- const inst = componentRef.instance as UiLineChart;
57
- // JIT doesn't wire the child cartesian chart's [xTickFormat] input, so the
58
- // default formatter is never invoked through the template. Invoke it
59
- // directly to cover its two-decimal rounding branch.
60
- expect(inst.xTickFormat()(3.14159)).toBe('3.14');
61
- expect(inst.yTickFormat()(1.005)).toBe('1');
18
+ /**
19
+ * Every drawn shape in a definition's scene, labels excluded.
20
+ */
21
+ const shapes = (definition: ChartDefinition<any, any, any>) => {
22
+ const scene = createChartRuntime().render(definition, {
23
+ width: 300,
24
+ height: 200,
62
25
  });
63
- });
64
-
65
- describe('UiLineSeries', () => {
66
- const renderSeries = async () => {
67
- const parentResult = await render(UiCartesianChart);
68
- const parent = parentResult.componentRef.instance as UiCartesianChart;
69
- setInputSignal(parent.xDomain, [0, 10]);
70
- setInputSignal(parent.yDomain, [0, 10]);
71
- await waitForUpdate();
72
- const seriesResult = await render(UiLineSeries, {
73
- providers: [{ provide: UiCartesianChart, useValue: parent }],
74
- });
75
- const series = seriesResult.componentRef.instance as UiLineSeries;
76
- setInputSignal(series.data, [
77
- { x: 0, y: 0 },
78
- { x: 5, y: 5 },
79
- { x: 10, y: 10 },
80
- ]);
81
- await waitForUpdate();
82
- return { parent, series, seriesResult };
26
+ const found: SceneNode[] = [];
27
+ const walk = (nodes: readonly SceneNode[]) => {
28
+ for (const node of nodes) {
29
+ if (node.kind === 'group') {
30
+ if (!node.focus) walk(node.children);
31
+ } else if (node.kind !== 'label') found.push(node);
32
+ }
83
33
  };
34
+ walk(scene.nodes);
35
+ return { scene, found };
36
+ };
84
37
 
85
- it('renders segments and dots for the projected data', async () => {
86
- const { series } = await renderSeries();
87
- const cast = series as unknown as {
88
- segments: () => { style: string }[];
89
- dots: () => { style: string }[];
90
- };
91
- expect(cast.segments().length).toBeGreaterThan(0);
92
- expect(cast.dots().length).toBe(3);
93
- });
38
+ describe('UiLineChart', () => {
39
+ type Internals = {
40
+ definition: () => ChartDefinition<any, any, any>;
41
+ onSelect(point: ChartPoint | null): void;
42
+ chart: unknown;
43
+ };
94
44
 
95
- it('hides dots when showDots is false', async () => {
96
- const { series } = await renderSeries();
97
- setInputSignal(series.showDots, false);
98
- await waitForUpdate();
99
- const cast = series as unknown as { dots: () => { style: string }[] };
100
- expect(cast.dots()).toEqual([]);
45
+ it('builds its definition from its inputs', async () => {
46
+ const { componentRef } = await render(UiLineChart);
47
+ const chart = componentRef.instance as UiLineChart;
48
+ setInputSignal(chart.data, data);
49
+ setInputSignal(chart.showDots, false);
50
+ setInputSignal(chart.yAxisLabel, 'Revenue');
51
+ const { found, scene } = shapes(
52
+ (chart as unknown as Internals).definition(),
53
+ );
54
+ expect(found.some((n) => n.kind === 'dot')).toBe(false);
55
+ expect(scene.nodes).toBeDefined();
101
56
  });
102
57
 
103
- it('emits pointTap when onPointTap is invoked', async () => {
104
- const { series } = await renderSeries();
105
- const cb = vi.fn();
106
- series.pointTap.subscribe(cb);
107
- (series as unknown as { onPointTap: (p: unknown) => void }).onPointTap({
108
- x: 1,
109
- y: 2,
110
- });
111
- expect(cb).toHaveBeenCalledWith({ x: 1, y: 2 });
58
+ it('emits the tapped datum', async () => {
59
+ const { componentRef } = await render(UiLineChart);
60
+ const chart = componentRef.instance as UiLineChart;
61
+ const tapped = vi.fn();
62
+ chart.pointTap.subscribe(tapped);
63
+ const internals = chart as unknown as Internals;
64
+ internals.onSelect({ datum: data[1] } as ChartPoint);
65
+ internals.onSelect(null);
66
+ expect(tapped.mock.calls).toEqual([[data[1]]]);
112
67
  });
113
68
 
114
- it('smooth mode resamples through sampleSmoothLine', async () => {
115
- const { series } = await renderSeries();
116
- setInputSignal(series.smooth, true);
117
- await waitForUpdate();
118
- const cast = series as unknown as { segments: () => { style: string }[] };
119
- expect(cast.segments().length).toBeGreaterThan(2);
69
+ it('forwards the zoom API to its chart', async () => {
70
+ const { componentRef } = await render(UiLineChart);
71
+ const chart = componentRef.instance as UiLineChart;
72
+ const inner = { zoomIn: vi.fn(), zoomOut: vi.fn(), resetZoom: vi.fn() };
73
+ // Signal queries aren't wired under plain Vitest (JIT), so stand in for
74
+ // the inner <ui-chart>.
75
+ (chart as unknown as Internals).chart = () => inner;
76
+ chart.zoomIn();
77
+ chart.zoomOut();
78
+ chart.resetZoom();
79
+ expect(inner.zoomIn).toHaveBeenCalled();
80
+ expect(inner.zoomOut).toHaveBeenCalled();
81
+ expect(inner.resetZoom).toHaveBeenCalled();
120
82
  });
121
83
  });
@@ -1,81 +1,77 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { createChartRuntime } from '@tanstack/charts/runtime';
2
+ import type { ChartDefinition, SceneNode } from '@tanstack/charts/types';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { lineChartDefinition } from './line-chart';
2
5
 
3
- // Mock Angular (and the sibling cartesian-chart's Angular usage, which loads
4
- // transitively) so importing the module needs no framework runtime. Only the
5
- // exported pure `computeLineSegments` is exercised; components are never built.
6
- vi.mock('@angular/core', () => ({
7
- Component: () => () => {},
8
- ViewEncapsulation: { None: 0 },
9
- computed: () => () => {},
10
- inject: () => ({}),
11
- input: () => () => {},
12
- output: () => () => {},
13
- }));
6
+ const data = [
7
+ { x: 0, y: 10 },
8
+ { x: 5, y: 30 },
9
+ { x: 10, y: 20 },
10
+ ];
14
11
 
15
- vi.mock('@blotch/angular-lynx', () => ({
16
- LYNX_ELEMENTS: [],
17
- // cartesian-chart (imported transitively) registers these in its decorator.
18
- LynxGestureDetector: class {},
19
- PanGesture: class {},
20
- PinchGesture: class {},
21
- Gesture: { Simultaneous: () => ({}) },
22
- }));
23
-
24
- const { computeLineSegments } = await import('./line-chart');
25
-
26
- describe('computeLineSegments', () => {
27
- it('produces a horizontal segment (angle 0)', () => {
28
- const segments = computeLineSegments([
29
- { x: 0, y: 10 },
30
- { x: 100, y: 10 },
31
- ]);
32
- expect(segments).toHaveLength(1);
33
- expect(segments[0].x).toBe(0);
34
- expect(segments[0].y).toBe(10);
35
- expect(segments[0].length).toBeCloseTo(100);
36
- expect(segments[0].angle).toBeCloseTo(0);
12
+ /**
13
+ * Every drawn shape in a definition's scene, labels excluded.
14
+ */
15
+ const shapes = (definition: ChartDefinition<any, any, any>) => {
16
+ const scene = createChartRuntime().render(definition, {
17
+ width: 300,
18
+ height: 200,
37
19
  });
20
+ const found: SceneNode[] = [];
21
+ const walk = (nodes: readonly SceneNode[]) => {
22
+ for (const node of nodes) {
23
+ if (node.kind === 'group') {
24
+ if (!node.focus) walk(node.children);
25
+ } else if (node.kind !== 'label') found.push(node);
26
+ }
27
+ };
28
+ walk(scene.nodes);
29
+ return { scene, found };
30
+ };
38
31
 
39
- it('measures a vertical segment as 90 degrees (screen y-down)', () => {
40
- const segments = computeLineSegments([
41
- { x: 0, y: 0 },
42
- { x: 0, y: 50 },
43
- ]);
44
- expect(segments[0].length).toBeCloseTo(50);
45
- expect(segments[0].angle).toBeCloseTo(90);
32
+ describe('lineChartDefinition', () => {
33
+ it('draws the line and a dot per point', () => {
34
+ const { found } = shapes(lineChartDefinition({ data }));
35
+ expect(found.filter((n) => n.kind === 'polyline')).toHaveLength(1);
36
+ expect(found.filter((n) => n.kind === 'dot')).toHaveLength(3);
37
+ const line = found.find((n) => n.kind === 'polyline')!;
38
+ expect(line.style).toMatchObject({
39
+ stroke: 'var(--primary)',
40
+ strokeWidth: 2,
41
+ });
46
42
  });
47
43
 
48
- it('measures a 45-degree diagonal', () => {
49
- const segments = computeLineSegments([
50
- { x: 0, y: 0 },
51
- { x: 10, y: 10 },
52
- ]);
53
- expect(segments[0].length).toBeCloseTo(Math.sqrt(200));
54
- expect(segments[0].angle).toBeCloseTo(45);
44
+ it('can leave the dots out', () => {
45
+ for (const options of [{ showDots: false }, { dotRadius: 0 }]) {
46
+ const { found } = shapes(lineChartDefinition({ data, ...options }));
47
+ expect(found.some((n) => n.kind === 'dot')).toBe(false);
48
+ }
55
49
  });
56
50
 
57
- it('emits one segment per consecutive pair', () => {
58
- const segments = computeLineSegments([
59
- { x: 0, y: 0 },
60
- { x: 10, y: 5 },
61
- { x: 20, y: 0 },
62
- ]);
63
- expect(segments).toHaveLength(2);
51
+ it('draws a curve through the points when smooth', () => {
52
+ const { found } = shapes(
53
+ lineChartDefinition({ data, smooth: true, color: 'red' }),
54
+ );
55
+ const line = found.find((n) => n.kind === 'polyline') as { path?: string };
56
+ expect(line.path).toContain('C');
64
57
  });
65
58
 
66
- it('skips zero-length (duplicate) points', () => {
67
- const segments = computeLineSegments([
68
- { x: 5, y: 5 },
69
- { x: 5, y: 5 },
70
- { x: 10, y: 5 },
71
- ]);
72
- expect(segments).toHaveLength(1);
73
- expect(segments[0].x).toBe(5);
74
- expect(segments[0].length).toBeCloseTo(5);
59
+ it('fits the axes to the data and honors the shared options', () => {
60
+ const { scene } = shapes(
61
+ lineChartDefinition({
62
+ data,
63
+ yMin: 0,
64
+ yMax: 40,
65
+ showGrid: false,
66
+ tooltip: false,
67
+ }),
68
+ );
69
+ expect(scene.scales['y']!.domain).toEqual([0, 40]);
70
+ expect(scene.scales['x']!.domain).toEqual([0, 10]);
75
71
  });
76
72
 
77
- it('returns no segments for fewer than two points', () => {
78
- expect(computeLineSegments([])).toEqual([]);
79
- expect(computeLineSegments([{ x: 1, y: 1 }])).toEqual([]);
73
+ it('includes the tooltip unless turned off', () => {
74
+ expect(lineChartDefinition({ data }).tooltip).toBeTruthy();
75
+ expect(lineChartDefinition({ data, tooltip: false }).tooltip).toBe(false);
80
76
  });
81
77
  });