@blotch/dolan 22.2.2 → 22.2.4
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/dist/commands/add.d.ts +6 -2
- package/dist/commands/add.js +41 -11
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +162 -9
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -1
- package/dist/commands/diff.js +38 -25
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/diff.test.js +72 -11
- package/dist/commands/diff.test.js.map +1 -1
- package/dist/commands/doctor.js +228 -34
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +367 -10
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/eject.js +7 -0
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +39 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/info.js +3 -2
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts +37 -1
- package/dist/commands/init.js +169 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +392 -27
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-packages.d.ts +6 -0
- package/dist/commands/install-packages.js +20 -0
- package/dist/commands/install-packages.js.map +1 -0
- package/dist/commands/install-packages.test.d.ts +1 -0
- package/dist/commands/install-packages.test.js +55 -0
- package/dist/commands/install-packages.test.js.map +1 -0
- package/dist/commands/list.js +21 -14
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/list.test.js +62 -5
- package/dist/commands/list.test.js.map +1 -1
- package/dist/commands/outdated.js +11 -10
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/outdated.test.js +20 -5
- package/dist/commands/outdated.test.js.map +1 -1
- package/dist/commands/remove.d.ts +4 -0
- package/dist/commands/remove.js +17 -4
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/remove.test.js +53 -2
- package/dist/commands/remove.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.js +174 -58
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +587 -33
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/lockfile.d.ts +7 -0
- package/dist/lockfile.js +36 -0
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +25 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +78 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +119 -38
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +5 -0
- package/dist/test-utils.js +6 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +14 -0
- package/dist/testing/set-input-signal.js +21 -0
- package/dist/testing/set-input-signal.js.map +1 -0
- package/dist/testing/set-input-signal.test.d.ts +1 -0
- package/dist/testing/set-input-signal.test.js +20 -0
- package/dist/testing/set-input-signal.test.js.map +1 -0
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
- package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
- package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
- package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
- package/dist/ui/components/area-chart/area-chart.ts +100 -315
- package/dist/ui/components/area-chart/index.ts +2 -3
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +12 -14
- package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
- package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
- package/dist/ui/components/bar-chart/index.ts +3 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +434 -49
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +484 -2
- package/dist/ui/components/button-group/button-group.ts +380 -68
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
- package/dist/ui/components/candlestick-chart/index.ts +3 -4
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-focus.test.ts +132 -0
- package/dist/ui/components/chart/chart-focus.ts +115 -0
- package/dist/ui/components/chart/chart-presets.test.ts +335 -0
- package/dist/ui/components/chart/chart-presets.ts +278 -0
- package/dist/ui/components/chart/chart-scene.test.ts +589 -0
- package/dist/ui/components/chart/chart-scene.ts +483 -0
- package/dist/ui/components/chart/chart-theme.test.ts +85 -0
- package/dist/ui/components/chart/chart-theme.ts +210 -0
- package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
- package/dist/ui/components/chart/chart-zoom.ts +162 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +934 -0
- package/dist/ui/components/chart/index.ts +24 -0
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +23 -23
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +19 -14
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +213 -108
- package/dist/ui/components/dialog/dialog.ts +72 -35
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +133 -0
- package/dist/ui/components/dock/dock.test.ts +749 -0
- package/dist/ui/components/dock/dock.ts +960 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +338 -0
- package/dist/ui/components/glass/glass.ts +360 -0
- package/dist/ui/components/glass/index.ts +18 -0
- package/dist/ui/components/icon/icon-theme.test.ts +169 -0
- package/dist/ui/components/icon/icon-theme.ts +227 -0
- package/dist/ui/components/icon/icon.test.ts +85 -25
- package/dist/ui/components/icon/icon.ts +38 -21
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +15 -12
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/line-chart/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +119 -21
- package/dist/ui/components/list/list.ts +21 -15
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +632 -0
- package/dist/ui/components/menu/menu-popup.ts +544 -0
- package/dist/ui/components/menu/menu.test.ts +429 -0
- package/dist/ui/components/menu/menu.ts +415 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
- package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
- package/dist/ui/components/pagination/pagination.test.ts +177 -65
- package/dist/ui/components/pagination/pagination.ts +55 -8
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +52 -54
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +24 -24
- package/dist/ui/components/scatter-chart/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +17 -11
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +64 -11
- package/dist/ui/components/sheet/sheet.ts +9 -1
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/spinner/spinner.test.ts +67 -22
- package/dist/ui/components/spinner/spinner.ts +48 -41
- package/dist/ui/components/switch/switch.test.ts +116 -46
- package/dist/ui/components/switch/switch.ts +8 -13
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/tabs.test.ts +215 -157
- package/dist/ui/components/tabs/tabs.ts +110 -118
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +14 -12
- package/dist/ui/components/toast/toast.test.ts +436 -315
- package/dist/ui/components/toast/toast.ts +62 -55
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- package/dist/ui/theme/chart-colors.test.ts +35 -0
- package/dist/ui/theme/dark.css +46 -2
- package/dist/ui/theme/default.css +63 -7
- package/dist/ui/theme/icon-colors.test.ts +35 -0
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +98 -25
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +14 -0
- package/dist/utils/analyze.js +15 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +29 -0
- package/dist/utils/installed.js +43 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +57 -0
- package/dist/utils/installed.test.js.map +1 -0
- package/dist/utils/interactive.d.ts +9 -0
- package/dist/utils/interactive.js +10 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/interactive.test.d.ts +1 -0
- package/dist/utils/interactive.test.js +24 -0
- package/dist/utils/interactive.test.js.map +1 -0
- package/dist/utils/packages.d.ts +36 -0
- package/dist/utils/packages.js +70 -0
- package/dist/utils/packages.js.map +1 -0
- package/dist/utils/packages.test.d.ts +1 -0
- package/dist/utils/packages.test.js +89 -0
- package/dist/utils/packages.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +17 -1
- package/dist/utils/resolve-paths.js +21 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +38 -2
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +25 -0
- package/dist/utils/tailwind.js +51 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +71 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +13 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
- package/dist/ui/components/button/button-render.test.ts +0 -113
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
|
@@ -70,36 +70,36 @@ export class UiRadioGroupItem {
|
|
|
70
70
|
readonly itemValue = input.required<string>({ alias: 'value' });
|
|
71
71
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
72
72
|
|
|
73
|
-
readonly dotRef = viewChild<ElementRef>('dot');
|
|
73
|
+
readonly dotRef = viewChild.required<ElementRef>('dot');
|
|
74
74
|
#dotAnim?: AnimationHandle;
|
|
75
|
-
#
|
|
75
|
+
#mounted = false;
|
|
76
76
|
|
|
77
77
|
protected readonly isSelected = computed(
|
|
78
78
|
() => this.#group.value() === this.itemValue(),
|
|
79
79
|
);
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
81
|
+
/**
|
|
82
|
+
* Pops the dot in when the item is selected and out when it's deselected.
|
|
83
|
+
* A field rather than a constructor: Angular's test build keeps a function
|
|
84
|
+
* for a constructor that nothing calls, which an app's coverage report
|
|
85
|
+
* would show as uncovered.
|
|
86
|
+
*
|
|
87
|
+
* The first run is the mount, skipped so a pre-selected item doesn't
|
|
88
|
+
* animate in. Every later run is a change of selection: `isSelected` is a
|
|
89
|
+
* computed, which only notifies when its value changes, and the dot is
|
|
90
|
+
* always in the template, so its query never changes.
|
|
91
|
+
*/
|
|
92
|
+
protected readonly dotAnimation = effect(() => {
|
|
93
|
+
const selected = this.isSelected();
|
|
94
|
+
const el = this.dotRef().nativeElement;
|
|
95
|
+
if (!this.#mounted) {
|
|
96
|
+
this.#mounted = true;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.#dotAnim?.cancel();
|
|
101
|
+
this.#dotAnim = selected ? popIn(el) : popOut(el);
|
|
102
|
+
});
|
|
103
103
|
|
|
104
104
|
protected readonly itemClass = computed(() =>
|
|
105
105
|
cn(
|
|
@@ -1,35 +1,140 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
7
|
+
import { createChartRuntime } from '@tanstack/charts/runtime';
|
|
8
|
+
import type {
|
|
9
|
+
ChartDefinition,
|
|
10
|
+
SceneDot,
|
|
11
|
+
SceneNode,
|
|
12
|
+
} from '@tanstack/charts/types';
|
|
1
13
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
import type { XYDatum } from '../chart/chart-presets';
|
|
15
|
+
import { UiScatterChart, scatterChartDefinition } from './scatter-chart';
|
|
16
|
+
|
|
17
|
+
const data = [
|
|
18
|
+
{ x: 150, y: 50 },
|
|
19
|
+
{ x: 160, y: 65 },
|
|
20
|
+
{ x: 170, y: 70 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every dot in a definition's scene.
|
|
25
|
+
*/
|
|
26
|
+
const dots = (definition: ChartDefinition<any, any, any>) => {
|
|
27
|
+
const scene = createChartRuntime().render(definition, {
|
|
28
|
+
width: 300,
|
|
29
|
+
height: 200,
|
|
30
|
+
});
|
|
31
|
+
const found: SceneDot[] = [];
|
|
32
|
+
const walk = (nodes: readonly SceneNode[]) => {
|
|
33
|
+
for (const node of nodes) {
|
|
34
|
+
if (node.kind === 'group') {
|
|
35
|
+
if (!node.focus) walk(node.children);
|
|
36
|
+
} else if (node.kind === 'dot') found.push(node);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
walk(scene.nodes);
|
|
40
|
+
return { scene, found };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('scatterChartDefinition', () => {
|
|
44
|
+
it('draws a dot per point, inset from the frame', () => {
|
|
45
|
+
const { scene, found } = dots(scatterChartDefinition({ data }));
|
|
46
|
+
expect(found).toHaveLength(3);
|
|
47
|
+
expect(found[0]!.radius).toBe(4);
|
|
48
|
+
expect(found[0]!.style?.fill).toBe('var(--primary)');
|
|
49
|
+
// The 4% inset widens the x domain past the data.
|
|
50
|
+
expect(scene.scales['x']!.domain[0]).toBeLessThan(150);
|
|
10
51
|
});
|
|
11
52
|
|
|
12
|
-
it('
|
|
13
|
-
const
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
]);
|
|
17
|
-
expect(small.size).toBe(4);
|
|
18
|
-
expect(small.left).toBe(8);
|
|
19
|
-
expect(big.size).toBe(18);
|
|
20
|
-
expect(big.left).toBe(31); // 40 - 9
|
|
53
|
+
it('sizes points individually for a bubble chart', () => {
|
|
54
|
+
const { found } = dots(
|
|
55
|
+
scatterChartDefinition({ data, sizes: [2, 8], radius: 5 }),
|
|
56
|
+
);
|
|
57
|
+
expect(found.map((dot) => dot.radius)).toEqual([2, 8, 5]);
|
|
21
58
|
});
|
|
22
59
|
|
|
23
|
-
it('
|
|
24
|
-
const
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
it('cycles per-point colors and keeps explicit padding', () => {
|
|
61
|
+
const { scene, found } = dots(
|
|
62
|
+
scatterChartDefinition({
|
|
63
|
+
data,
|
|
64
|
+
colors: ['red', 'blue'],
|
|
65
|
+
xMin: 150,
|
|
66
|
+
xMax: 170,
|
|
67
|
+
padding: { left: 0, right: 0, top: 0, bottom: 0 },
|
|
68
|
+
tooltip: false,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
expect(found.map((dot) => dot.style?.fill)).toEqual(['red', 'blue', 'red']);
|
|
72
|
+
expect(scene.scales['x']!.domain).toEqual([150, 170]);
|
|
30
73
|
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
@Component({
|
|
77
|
+
imports: [UiScatterChart],
|
|
78
|
+
template: `
|
|
79
|
+
<ui-scatter-chart
|
|
80
|
+
[data]="data"
|
|
81
|
+
[width]="300"
|
|
82
|
+
[radius]="radius()"
|
|
83
|
+
(pointTap)="tapped.push($event)"
|
|
84
|
+
/>
|
|
85
|
+
`,
|
|
86
|
+
})
|
|
87
|
+
class ScatterChartHost {
|
|
88
|
+
readonly data = data;
|
|
89
|
+
readonly radius = signal(7);
|
|
90
|
+
/** Every `pointTap`, in order. */
|
|
91
|
+
readonly tapped: XYDatum[] = [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The SVG the chart's marks layer draws.
|
|
96
|
+
*/
|
|
97
|
+
const marksSvg = (container: Element) =>
|
|
98
|
+
container.querySelectorAll('svg')[1]!.getAttribute('content')!;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Where the chart drew each dot, and how big.
|
|
102
|
+
*/
|
|
103
|
+
const dotsDrawn = (container: Element) =>
|
|
104
|
+
[
|
|
105
|
+
...marksSvg(container).matchAll(
|
|
106
|
+
/<circle cx="([\d.-]+)" cy="([\d.-]+)" r="([\d.-]+)"/g,
|
|
107
|
+
),
|
|
108
|
+
].map(([, x, y, r]) => ({ x: Number(x), y: Number(y), r: Number(r) }));
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Touches a spot of the chart and lifts the finger right there: a tap.
|
|
112
|
+
*/
|
|
113
|
+
const tap = async (container: Element, at: { x: number; y: number }) => {
|
|
114
|
+
const root = container.querySelector('[accessibility-label]')!;
|
|
115
|
+
fireEvent.touchstart(root, { touches: [at] });
|
|
116
|
+
fireEvent.touchend(root, { touches: [at] });
|
|
117
|
+
await waitForUpdate();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
describe('UiScatterChart', () => {
|
|
121
|
+
it('draws a dot per datum with its inputs', async () => {
|
|
122
|
+
const { componentRef, container } = await render(ScatterChartHost);
|
|
123
|
+
expect(dotsDrawn(container).map((dot) => dot.r)).toEqual([7, 7, 7]);
|
|
124
|
+
|
|
125
|
+
(componentRef.instance as ScatterChartHost).radius.set(3);
|
|
126
|
+
await waitForUpdate();
|
|
127
|
+
expect(dotsDrawn(container).map((dot) => dot.r)).toEqual([3, 3, 3]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("emits a tapped point's datum, and nothing for a tap off the points", async () => {
|
|
131
|
+
const { componentRef, container } = await render(ScatterChartHost);
|
|
132
|
+
const host = componentRef.instance as ScatterChartHost;
|
|
133
|
+
const last = dotsDrawn(container)[2]!;
|
|
134
|
+
await tap(container, { x: last.x, y: last.y });
|
|
135
|
+
expect(host.tapped).toEqual([data[2]]);
|
|
31
136
|
|
|
32
|
-
|
|
33
|
-
expect(
|
|
137
|
+
await tap(container, { x: -100, y: -100 });
|
|
138
|
+
expect(host.tapped).toEqual([data[2]]);
|
|
34
139
|
});
|
|
35
140
|
});
|
|
@@ -2,276 +2,130 @@ import {
|
|
|
2
2
|
Component,
|
|
3
3
|
ViewEncapsulation,
|
|
4
4
|
computed,
|
|
5
|
-
inject,
|
|
6
5
|
input,
|
|
7
6
|
output,
|
|
8
7
|
} from '@angular/core';
|
|
9
|
-
import {
|
|
8
|
+
import { defineChart, dot } from '@tanstack/charts/universal';
|
|
9
|
+
import type { ChartPoint } from '@tanstack/charts/types';
|
|
10
10
|
|
|
11
|
+
import { UiChart, tooltip } from '../chart/chart';
|
|
11
12
|
import {
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/** Center y of the dot, in px. */
|
|
31
|
-
cy: number;
|
|
32
|
-
/** Dot radius in px (varies per point for a bubble chart). */
|
|
33
|
-
r: number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/** A square (rendered as a circle via border-radius) in plot-local pixel space. */
|
|
37
|
-
export type DotShape = { left: number; top: number; size: number };
|
|
38
|
-
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// Pure geometry (exported so it can be unit-tested without Angular)
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Turns projected points — already in plot-local pixel space — into square boxes
|
|
45
|
-
* centered on each point, ready to render as circular `<view>`s (the caller sets
|
|
46
|
-
* `border-radius` to half the size). A scatter mark is the simplest chart mark
|
|
47
|
-
* of all: one box per datum, positioned so the point sits at its center.
|
|
48
|
-
*
|
|
49
|
-
* Each dot's radius comes from the point itself, so a per-point radius (a bubble
|
|
50
|
-
* chart) and a uniform radius (a plain scatter) share this one mapping.
|
|
51
|
-
*/
|
|
52
|
-
export const computeDots = (dots: readonly ProjectedDot[]): DotShape[] =>
|
|
53
|
-
dots.map((d) => ({ left: d.cx - d.r, top: d.cy - d.r, size: d.r * 2 }));
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Rendering helpers (mirror the equivalents in line-chart.ts / bar-chart.ts —
|
|
57
|
-
// kept local so this module stays self-contained when scaffolded by `dolan add`)
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
13
|
+
type CartesianPresetOptions,
|
|
14
|
+
PRESET_CHART_BINDINGS,
|
|
15
|
+
UiCartesianPreset,
|
|
16
|
+
type XYDatum,
|
|
17
|
+
presetAxes,
|
|
18
|
+
} from '../chart/chart-presets';
|
|
19
|
+
|
|
20
|
+
export type ScatterChartOptions = {
|
|
21
|
+
readonly data: readonly XYDatum[];
|
|
22
|
+
/** Any CSS color; theme tokens like `var(--primary)` follow dark mode. */
|
|
23
|
+
readonly color?: string;
|
|
24
|
+
/** One color per point, cycling; overrides `color` when not empty. */
|
|
25
|
+
readonly colors?: readonly string[];
|
|
26
|
+
/** Dot radius in px. */
|
|
27
|
+
readonly radius?: number;
|
|
28
|
+
/** One radius per point (a bubble chart); points past its end use `radius`. */
|
|
29
|
+
readonly sizes?: readonly number[];
|
|
30
|
+
} & CartesianPresetOptions;
|
|
59
31
|
|
|
60
32
|
/**
|
|
61
|
-
*
|
|
33
|
+
* Scatter plots inset every side by 4% unless told otherwise, so dots at the
|
|
34
|
+
* extremes aren't cut in half by the frame.
|
|
62
35
|
*/
|
|
63
|
-
const px = (n: number): string => `${n.toFixed(2)}px`;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Default axis label formatter — plain JS only (no `Intl`, absent in Lynx).
|
|
67
|
-
*/
|
|
68
|
-
const defaultTickFormat = (value: number): string =>
|
|
69
|
-
String(Math.round(value * 100) / 100);
|
|
70
|
-
|
|
71
|
-
// Default inner inset (fraction of the plot) for the wrapper. Unlike the line
|
|
72
|
-
// chart, a scatter's marks are dots at the data extremes, so a small inset on
|
|
73
|
-
// every side keeps the edge dots from being clipped by the frame.
|
|
74
36
|
const SCATTER_INSET = 0.04;
|
|
75
37
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
readonly sizes = input<number[]>([]);
|
|
121
|
-
readonly userClass = input<string>('', { alias: 'class' });
|
|
122
|
-
|
|
123
|
-
readonly pointTap = output<ChartPoint>();
|
|
124
|
-
|
|
125
|
-
// Project each datum into plot-local pixel space via the shared scales, picking
|
|
126
|
-
// up a per-point radius when `sizes` supplies one. Reading the chart's scale
|
|
127
|
-
// signals here makes this recompute whenever the domain or plot size changes.
|
|
128
|
-
readonly #projected = computed(() => {
|
|
129
|
-
const scaleX = this.#chart.xScale();
|
|
130
|
-
const scaleY = this.#chart.yScale();
|
|
131
|
-
const sizes = this.sizes();
|
|
132
|
-
const r = this.radius();
|
|
133
|
-
return this.data().map((point, i) => ({
|
|
134
|
-
cx: scaleX(point.x),
|
|
135
|
-
cy: scaleY(point.y),
|
|
136
|
-
r: sizes[i] ?? r,
|
|
137
|
-
point,
|
|
138
|
-
}));
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
protected readonly dots = computed(() => {
|
|
142
|
-
const projected = this.#projected();
|
|
143
|
-
const fallback = this.color();
|
|
144
|
-
const palette = this.colors();
|
|
145
|
-
// computeDots works purely from the projected pixel fields; zip its output
|
|
146
|
-
// back with each source point so a dot's tap can emit the original datum.
|
|
147
|
-
// A circle is a square with border-radius of half its size.
|
|
148
|
-
return computeDots(projected).map((shape, i) => {
|
|
149
|
-
const fill = palette.length ? palette[i % palette.length] : fallback;
|
|
150
|
-
return {
|
|
151
|
-
point: projected[i].point,
|
|
152
|
-
style:
|
|
153
|
-
`position: absolute; left: ${px(shape.left)}; top: ${px(shape.top)}; ` +
|
|
154
|
-
`width: ${px(shape.size)}; height: ${px(shape.size)}; ` +
|
|
155
|
-
`background-color: ${fill}; border-radius: ${px(shape.size / 2)};`,
|
|
156
|
-
};
|
|
157
|
-
});
|
|
38
|
+
/** The definition `<ui-scatter-chart>` renders. */
|
|
39
|
+
export const scatterChartDefinition = (options: ScatterChartOptions) => {
|
|
40
|
+
const {
|
|
41
|
+
data,
|
|
42
|
+
color = 'var(--primary)',
|
|
43
|
+
colors = [],
|
|
44
|
+
radius = 4,
|
|
45
|
+
sizes = [],
|
|
46
|
+
} = options;
|
|
47
|
+
const padding = options.padding ?? {};
|
|
48
|
+
return defineChart({
|
|
49
|
+
marks: [
|
|
50
|
+
dot(data, {
|
|
51
|
+
x: 'x',
|
|
52
|
+
y: 'y',
|
|
53
|
+
r: sizes.length ? (_d, { index }) => sizes[index] ?? radius : radius,
|
|
54
|
+
// dot's fill is a single color, so per-point colors go through the
|
|
55
|
+
// color scale, keyed by point index.
|
|
56
|
+
...(colors.length
|
|
57
|
+
? { color: (_d: XYDatum, { index }: { index: number }) => index }
|
|
58
|
+
: { fill: color }),
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
scales: presetAxes(
|
|
62
|
+
{
|
|
63
|
+
...options,
|
|
64
|
+
padding: {
|
|
65
|
+
top: padding.top ?? SCATTER_INSET,
|
|
66
|
+
right: padding.right ?? SCATTER_INSET,
|
|
67
|
+
bottom: padding.bottom ?? SCATTER_INSET,
|
|
68
|
+
left: padding.left ?? SCATTER_INSET,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{ x: data.map((d) => d.x), y: data.map((d) => d.y) },
|
|
72
|
+
),
|
|
73
|
+
...(colors.length
|
|
74
|
+
? {
|
|
75
|
+
color: {
|
|
76
|
+
domain: data.map((_d, index) => index),
|
|
77
|
+
range: data.map((_d, index) => colors[index % colors.length]!),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
81
|
+
tooltip: options.tooltip === false ? false : tooltip,
|
|
158
82
|
});
|
|
159
|
-
|
|
160
|
-
protected onPointTap(point: ChartPoint): void {
|
|
161
|
-
this.pointTap.emit(point);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
83
|
+
};
|
|
164
84
|
|
|
165
85
|
/**
|
|
166
|
-
* A
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
86
|
+
* A scatter plot, or a bubble chart with `sizes`. For a trend line or several
|
|
87
|
+
* groups with a legend, build a definition with `dot` (and `linearRegressionY`)
|
|
88
|
+
* and render it with `<ui-chart>`.
|
|
89
|
+
*
|
|
90
|
+
* ```html
|
|
91
|
+
* <ui-scatter-chart [data]="heightWeight" xAxisLabel="Height" yAxisLabel="Weight" />
|
|
92
|
+
* ```
|
|
171
93
|
*/
|
|
172
94
|
@Component({
|
|
173
95
|
selector: 'ui-scatter-chart',
|
|
174
|
-
|
|
175
|
-
imports: [LYNX_ELEMENTS, UiCartesianChart, UiScatterSeries],
|
|
96
|
+
imports: [UiChart],
|
|
176
97
|
encapsulation: ViewEncapsulation.None,
|
|
177
98
|
template: `
|
|
178
|
-
<ui-
|
|
179
|
-
[
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
[width]="width()"
|
|
184
|
-
[height]="height()"
|
|
185
|
-
[tickCount]="tickCount()"
|
|
186
|
-
[showXGrid]="showXGrid()"
|
|
187
|
-
[xAxisLabel]="xAxisLabel()"
|
|
188
|
-
[yAxisLabel]="yAxisLabel()"
|
|
189
|
-
[padding]="effectivePadding()"
|
|
190
|
-
[xTickFormat]="xTickFormat()"
|
|
191
|
-
[yTickFormat]="yTickFormat()"
|
|
192
|
-
[zoomable]="zoomable()"
|
|
193
|
-
[zoomAxes]="zoomAxes()"
|
|
194
|
-
[showZoomControls]="showZoomControls()"
|
|
195
|
-
[minZoom]="minZoom()"
|
|
196
|
-
[maxZoom]="maxZoom()"
|
|
197
|
-
[zoomStep]="zoomStep()"
|
|
198
|
-
[class]="userClass()"
|
|
199
|
-
>
|
|
200
|
-
<ui-scatter-series
|
|
201
|
-
[data]="data()"
|
|
202
|
-
[color]="color()"
|
|
203
|
-
[colors]="colors()"
|
|
204
|
-
[radius]="radius()"
|
|
205
|
-
[sizes]="sizes()"
|
|
206
|
-
(pointTap)="pointTap.emit($event)"
|
|
207
|
-
/>
|
|
208
|
-
</ui-cartesian-chart>
|
|
99
|
+
<ui-chart
|
|
100
|
+
[definition]="definition()"
|
|
101
|
+
${PRESET_CHART_BINDINGS}
|
|
102
|
+
(pointSelect)="onSelect($event)"
|
|
103
|
+
/>
|
|
209
104
|
`,
|
|
210
105
|
})
|
|
211
|
-
export class UiScatterChart {
|
|
212
|
-
readonly data = input.required<
|
|
106
|
+
export class UiScatterChart extends UiCartesianPreset<XYDatum> {
|
|
107
|
+
readonly data = input.required<readonly XYDatum[]>();
|
|
213
108
|
readonly color = input('var(--primary)');
|
|
214
|
-
readonly colors = input<string[]>([]);
|
|
109
|
+
readonly colors = input<readonly string[]>([]);
|
|
215
110
|
readonly radius = input(4);
|
|
216
|
-
readonly sizes = input<number[]>([]);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
readonly
|
|
220
|
-
|
|
221
|
-
readonly
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
readonly xTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
237
|
-
readonly yTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
238
|
-
readonly userClass = input<string>('', { alias: 'class' });
|
|
239
|
-
|
|
240
|
-
// Pan/zoom — forwarded to the underlying <ui-cartesian-chart>. Off by default.
|
|
241
|
-
readonly zoomable = input(false);
|
|
242
|
-
readonly zoomAxes = input<'x' | 'y' | 'xy'>('xy');
|
|
243
|
-
readonly showZoomControls = input(true);
|
|
244
|
-
readonly minZoom = input(1);
|
|
245
|
-
readonly maxZoom = input(8);
|
|
246
|
-
readonly zoomStep = input(1.4);
|
|
247
|
-
|
|
248
|
-
readonly pointTap = output<ChartPoint>();
|
|
249
|
-
|
|
250
|
-
// Auto-compute rounded axes from the data extremes, letting an explicit
|
|
251
|
-
// min/max override either end. Unlike the bar/area charts there is no baseline
|
|
252
|
-
// to fold in — both axes simply fit the data. `Math.min/max(...[])` yield
|
|
253
|
-
// ±Infinity for empty data, which `niceScale` handles by padding.
|
|
254
|
-
protected readonly xAxis = computed(() => {
|
|
255
|
-
const xs = this.data().map((p) => p.x);
|
|
256
|
-
const lo = this.xMin() ?? Math.min(...xs);
|
|
257
|
-
const hi = this.xMax() ?? Math.max(...xs);
|
|
258
|
-
return niceScale(lo, hi, this.tickCount());
|
|
259
|
-
});
|
|
260
|
-
protected readonly yAxis = computed(() => {
|
|
261
|
-
const ys = this.data().map((p) => p.y);
|
|
262
|
-
const lo = this.yMin() ?? Math.min(...ys);
|
|
263
|
-
const hi = this.yMax() ?? Math.max(...ys);
|
|
264
|
-
return niceScale(lo, hi, this.tickCount());
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Dots sit at the data extremes, so without an inset the edge dots would be
|
|
268
|
-
// clipped by the frame. Default a small inset on every side; user `padding` is
|
|
269
|
-
// spread last, so an explicit side wins.
|
|
270
|
-
protected readonly effectivePadding = computed<ChartPadding>(() => ({
|
|
271
|
-
top: SCATTER_INSET,
|
|
272
|
-
right: SCATTER_INSET,
|
|
273
|
-
bottom: SCATTER_INSET,
|
|
274
|
-
left: SCATTER_INSET,
|
|
275
|
-
...this.padding(),
|
|
276
|
-
}));
|
|
111
|
+
readonly sizes = input<readonly number[]>([]);
|
|
112
|
+
|
|
113
|
+
/** The tapped point's datum. */
|
|
114
|
+
readonly pointTap = output<XYDatum>();
|
|
115
|
+
|
|
116
|
+
protected readonly definition = computed(() =>
|
|
117
|
+
scatterChartDefinition({
|
|
118
|
+
...this.commonOptions(),
|
|
119
|
+
data: this.data(),
|
|
120
|
+
color: this.color(),
|
|
121
|
+
colors: this.colors(),
|
|
122
|
+
radius: this.radius(),
|
|
123
|
+
sizes: this.sizes(),
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
protected onSelect(point: ChartPoint | null): void {
|
|
128
|
+
const datum = this.datumOf(point);
|
|
129
|
+
if (datum) this.pointTap.emit(datum);
|
|
130
|
+
}
|
|
277
131
|
}
|