@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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Injector,
|
|
4
|
+
type Type,
|
|
5
|
+
computed,
|
|
6
|
+
input,
|
|
7
|
+
output,
|
|
8
|
+
signal,
|
|
9
|
+
viewChild,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import {
|
|
12
|
+
fireEvent,
|
|
13
|
+
render,
|
|
14
|
+
waitForUpdate,
|
|
15
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
16
|
+
import { defineChart, dot } from '@tanstack/charts/universal';
|
|
17
|
+
import { describe, expect, it } from 'vitest';
|
|
18
|
+
import { UiChart, tooltip } from './chart';
|
|
19
|
+
import {
|
|
20
|
+
type CartesianPresetOptions,
|
|
21
|
+
PRESET_CHART_BINDINGS,
|
|
22
|
+
UiCartesianPreset,
|
|
23
|
+
type XYDatum,
|
|
24
|
+
linearAxis,
|
|
25
|
+
minSpacing,
|
|
26
|
+
presetAxes,
|
|
27
|
+
} from './chart-presets';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The configured domain of an axis config's scale instance.
|
|
31
|
+
*/
|
|
32
|
+
const domainOf = (axis: { scale: unknown }): number[] =>
|
|
33
|
+
(axis.scale as { domain: () => number[] }).domain();
|
|
34
|
+
|
|
35
|
+
describe('linearAxis', () => {
|
|
36
|
+
const base = { tickCount: 5, grid: true };
|
|
37
|
+
|
|
38
|
+
it('fits the data and rounds out to nice values', () => {
|
|
39
|
+
expect(domainOf(linearAxis({ ...base, values: [3, 47] }))).toEqual([0, 50]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('keeps fixed ends exact and rounds only the free one', () => {
|
|
43
|
+
expect(
|
|
44
|
+
domainOf(linearAxis({ ...base, values: [3, 47], min: 1, max: 2 })),
|
|
45
|
+
).toEqual([1, 2]);
|
|
46
|
+
expect(domainOf(linearAxis({ ...base, values: [3, 47], min: 1 }))).toEqual([
|
|
47
|
+
1, 50,
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('includes extra values, such as a baseline', () => {
|
|
52
|
+
expect(
|
|
53
|
+
domainOf(linearAxis({ ...base, values: [5, 10], include: [0] })),
|
|
54
|
+
).toEqual([0, 10]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('gives flat and empty data a span around the value', () => {
|
|
58
|
+
expect(domainOf(linearAxis({ ...base, values: [10, 10] }))).toEqual([
|
|
59
|
+
4, 16,
|
|
60
|
+
]);
|
|
61
|
+
expect(domainOf(linearAxis({ ...base, values: [0] }))).toEqual([-1, 1]);
|
|
62
|
+
expect(domainOf(linearAxis({ ...base, values: [] }))).toEqual([0, 1]);
|
|
63
|
+
expect(domainOf(linearAxis({ ...base, values: [Number.NaN] }))).toEqual([
|
|
64
|
+
0, 1,
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('orders reversed fixed ends', () => {
|
|
69
|
+
expect(
|
|
70
|
+
domainOf(linearAxis({ ...base, values: [], min: 10, max: 0 })),
|
|
71
|
+
).toEqual([0, 10]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('widens by a margin, then by padding fractions', () => {
|
|
75
|
+
expect(
|
|
76
|
+
domainOf(linearAxis({ ...base, values: [], min: 0, max: 10, margin: 1 })),
|
|
77
|
+
).toEqual([-1, 11]);
|
|
78
|
+
// 10% each side of a 0–8 domain: the plot spans 10 units, inset 1 each side.
|
|
79
|
+
const [lo, hi] = domainOf(
|
|
80
|
+
linearAxis({ ...base, values: [], min: 0, max: 8, padding: [0.1, 0.1] }),
|
|
81
|
+
);
|
|
82
|
+
expect(lo).toBeCloseTo(-1);
|
|
83
|
+
expect(hi).toBeCloseTo(9);
|
|
84
|
+
// Padding that leaves no plot is ignored.
|
|
85
|
+
expect(
|
|
86
|
+
domainOf(
|
|
87
|
+
linearAxis({
|
|
88
|
+
...base,
|
|
89
|
+
values: [],
|
|
90
|
+
min: 0,
|
|
91
|
+
max: 8,
|
|
92
|
+
padding: [0.6, 0.6],
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
).toEqual([0, 8]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('configures grid, label and ticks', () => {
|
|
99
|
+
const format = (value: number) => `${value}%`;
|
|
100
|
+
const axis = linearAxis({
|
|
101
|
+
...base,
|
|
102
|
+
values: [0, 1],
|
|
103
|
+
label: 'Share',
|
|
104
|
+
format,
|
|
105
|
+
grid: false,
|
|
106
|
+
});
|
|
107
|
+
expect(axis.grid).toBe(false);
|
|
108
|
+
expect(axis.axis).toEqual({ label: 'Share', ticks: { count: 5, format } });
|
|
109
|
+
expect(
|
|
110
|
+
(
|
|
111
|
+
linearAxis({ ...base, values: [0, 1], label: '' }).axis as {
|
|
112
|
+
label?: string;
|
|
113
|
+
}
|
|
114
|
+
).label,
|
|
115
|
+
).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('minSpacing', () => {
|
|
120
|
+
it('finds the smallest gap between distinct values', () => {
|
|
121
|
+
expect(minSpacing([5, 1, 3, 3, 4])).toBe(1);
|
|
122
|
+
expect(minSpacing([0, 10])).toBe(10);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('defaults to 1 without a gap', () => {
|
|
126
|
+
expect(minSpacing([])).toBe(1);
|
|
127
|
+
expect(minSpacing([2])).toBe(1);
|
|
128
|
+
expect(minSpacing([2, 2])).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('presetAxes', () => {
|
|
133
|
+
it('maps the shared options onto both axes', () => {
|
|
134
|
+
const axes = presetAxes(
|
|
135
|
+
{
|
|
136
|
+
tickCount: 3,
|
|
137
|
+
showGrid: false,
|
|
138
|
+
showXGrid: true,
|
|
139
|
+
xAxisLabel: 'X',
|
|
140
|
+
yAxisLabel: 'Y',
|
|
141
|
+
padding: { left: 0.1, right: 0.1, top: 0.1, bottom: 0.1 },
|
|
142
|
+
xMin: 0,
|
|
143
|
+
xMax: 8,
|
|
144
|
+
yMin: 0,
|
|
145
|
+
yMax: 8,
|
|
146
|
+
},
|
|
147
|
+
{ x: [], y: [], xMargin: 0 },
|
|
148
|
+
);
|
|
149
|
+
expect(axes.x.grid).toBe(true);
|
|
150
|
+
expect(axes.y.grid).toBe(false);
|
|
151
|
+
expect(axes.x.axis).toMatchObject({ label: 'X', ticks: { count: 3 } });
|
|
152
|
+
expect(axes.y.axis).toMatchObject({ label: 'Y' });
|
|
153
|
+
expect(domainOf(axes.x)[0]).toBeCloseTo(-1);
|
|
154
|
+
expect(domainOf(axes.y)[1]).toBeCloseTo(9);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('defaults to five ticks, a y grid and no padding', () => {
|
|
158
|
+
const axes = presetAxes({}, { x: [0, 10], y: [0, 10], yInclude: [-5] });
|
|
159
|
+
expect(axes.x.grid).toBe(false);
|
|
160
|
+
expect(axes.y.grid).toBe(true);
|
|
161
|
+
expect(domainOf(axes.x)).toEqual([0, 10]);
|
|
162
|
+
expect(domainOf(axes.y)).toEqual([-10, 10]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The smallest preset: a dot per datum on the shared axes, built the way the
|
|
168
|
+
* library's presets (`<ui-line-chart>`, …) build theirs.
|
|
169
|
+
*/
|
|
170
|
+
@Component({
|
|
171
|
+
selector: 'test-dot-chart',
|
|
172
|
+
imports: [UiChart],
|
|
173
|
+
template: `
|
|
174
|
+
<ui-chart
|
|
175
|
+
#chart
|
|
176
|
+
[definition]="definition()"
|
|
177
|
+
${PRESET_CHART_BINDINGS}
|
|
178
|
+
(pointSelect)="pointTap.emit(datumOf($event))"
|
|
179
|
+
/>
|
|
180
|
+
`,
|
|
181
|
+
})
|
|
182
|
+
class DotChart extends UiCartesianPreset<XYDatum> {
|
|
183
|
+
readonly data = input.required<readonly XYDatum[]>();
|
|
184
|
+
/** The tapped datum, or `null` for a tap on empty space. */
|
|
185
|
+
readonly pointTap = output<XYDatum | null>();
|
|
186
|
+
|
|
187
|
+
protected readonly definition = computed(() => {
|
|
188
|
+
const options = this.commonOptions();
|
|
189
|
+
const data = this.data();
|
|
190
|
+
return defineChart({
|
|
191
|
+
marks: [dot(data, { x: 'x', y: 'y' })],
|
|
192
|
+
scales: presetAxes(options, {
|
|
193
|
+
x: data.map((d) => d.x),
|
|
194
|
+
y: data.map((d) => d.y),
|
|
195
|
+
}),
|
|
196
|
+
tooltip: options.tooltip === false ? false : tooltip,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Component({
|
|
202
|
+
imports: [DotChart],
|
|
203
|
+
template: `
|
|
204
|
+
<test-dot-chart
|
|
205
|
+
[data]="data"
|
|
206
|
+
[width]="300"
|
|
207
|
+
[height]="160"
|
|
208
|
+
label="Sales"
|
|
209
|
+
class="mt-2"
|
|
210
|
+
[xAxisLabel]="xAxisLabel()"
|
|
211
|
+
yAxisLabel="Units"
|
|
212
|
+
[zoomable]="true"
|
|
213
|
+
zoomAxes="x"
|
|
214
|
+
(pointTap)="tapped.push($event)"
|
|
215
|
+
/>
|
|
216
|
+
`,
|
|
217
|
+
})
|
|
218
|
+
class PresetHost {
|
|
219
|
+
readonly data: readonly XYDatum[] = [
|
|
220
|
+
{ x: 0, y: 100 },
|
|
221
|
+
{ x: 5, y: 300 },
|
|
222
|
+
{ x: 10, y: 500 },
|
|
223
|
+
];
|
|
224
|
+
readonly xAxisLabel = signal('Day');
|
|
225
|
+
/** Every `pointTap`, in order. */
|
|
226
|
+
readonly tapped: (XYDatum | null)[] = [];
|
|
227
|
+
readonly preset = viewChild.required(DotChart);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The chart's own view, which takes every touch.
|
|
232
|
+
*/
|
|
233
|
+
const chartRoot = (container: Element) =>
|
|
234
|
+
container.querySelector('[accessibility-label]')!;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Where the chart drew each dot, read from its marks layer.
|
|
238
|
+
*/
|
|
239
|
+
const dotsDrawn = (container: Element) =>
|
|
240
|
+
[
|
|
241
|
+
...container
|
|
242
|
+
.querySelectorAll('svg')[1]!
|
|
243
|
+
.getAttribute('content')!
|
|
244
|
+
.matchAll(/<circle cx="([\d.-]+)" cy="([\d.-]+)"/g),
|
|
245
|
+
].map(([, x, y]) => ({ x: Number(x), y: Number(y) }));
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Touches a spot and lifts the finger right there: a tap.
|
|
249
|
+
*/
|
|
250
|
+
const tap = async (root: Element, at: { x: number; y: number }) => {
|
|
251
|
+
fireEvent.touchstart(root, { touches: [at] });
|
|
252
|
+
fireEvent.touchend(root, { touches: [at] });
|
|
253
|
+
await waitForUpdate();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
describe('UiCartesianPreset', () => {
|
|
257
|
+
it('starts every preset from the same defaults', () => {
|
|
258
|
+
// Angular builds a preset through the base's factory, with the preset's
|
|
259
|
+
// own type, so no preset runs that factory's default: build the base alone.
|
|
260
|
+
const base = Injector.create({
|
|
261
|
+
providers: [
|
|
262
|
+
{
|
|
263
|
+
provide: UiCartesianPreset,
|
|
264
|
+
useClass: UiCartesianPreset as unknown as Type<
|
|
265
|
+
UiCartesianPreset<XYDatum>
|
|
266
|
+
>,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
}).get(UiCartesianPreset);
|
|
270
|
+
const options = (
|
|
271
|
+
base as unknown as { commonOptions(): CartesianPresetOptions }
|
|
272
|
+
).commonOptions();
|
|
273
|
+
expect(options).toEqual({
|
|
274
|
+
tickCount: 5,
|
|
275
|
+
showGrid: true,
|
|
276
|
+
showXGrid: false,
|
|
277
|
+
xAxisLabel: '',
|
|
278
|
+
yAxisLabel: '',
|
|
279
|
+
padding: {},
|
|
280
|
+
xMin: undefined,
|
|
281
|
+
xMax: undefined,
|
|
282
|
+
yMin: undefined,
|
|
283
|
+
yMax: undefined,
|
|
284
|
+
xTickFormat: undefined,
|
|
285
|
+
yTickFormat: undefined,
|
|
286
|
+
tooltip: true,
|
|
287
|
+
});
|
|
288
|
+
expect(base.height()).toBe(200);
|
|
289
|
+
expect(base.label()).toBe('Chart');
|
|
290
|
+
expect(base.zoomable()).toBe(false);
|
|
291
|
+
expect(base.zoomAxes()).toBe('xy');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('passes the shared inputs through to its chart', async () => {
|
|
295
|
+
const { container, componentRef, getByText } = await render(PresetHost);
|
|
296
|
+
const root = chartRoot(container);
|
|
297
|
+
expect(root.getAttribute('accessibility-label')).toBe('Sales');
|
|
298
|
+
expect(root.getAttribute('class')).toContain('mt-2');
|
|
299
|
+
expect(root.getAttribute('style')).toContain('height: 160px');
|
|
300
|
+
expect(getByText('Units')).toBeTruthy();
|
|
301
|
+
expect(getByText('Day')).toBeTruthy();
|
|
302
|
+
|
|
303
|
+
(componentRef.instance as PresetHost).xAxisLabel.set('Week');
|
|
304
|
+
await waitForUpdate();
|
|
305
|
+
expect(getByText('Week')).toBeTruthy();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('hands a tap over as the datum, or null off the points', async () => {
|
|
309
|
+
const { container, componentRef } = await render(PresetHost);
|
|
310
|
+
const host = componentRef.instance as PresetHost;
|
|
311
|
+
const root = chartRoot(container);
|
|
312
|
+
const middle = dotsDrawn(container)[1]!;
|
|
313
|
+
await tap(root, { x: middle.x + 1, y: middle.y + 1 });
|
|
314
|
+
await tap(root, { x: -100, y: -100 });
|
|
315
|
+
expect(host.tapped).toEqual([host.data[1], null]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('zooms its chart in and out, and resets it', async () => {
|
|
319
|
+
const { componentRef, queryByText } = await render(PresetHost);
|
|
320
|
+
const preset = (componentRef.instance as PresetHost).preset();
|
|
321
|
+
// The x axis spans 0–10; zoomed in around the middle, its last tick goes.
|
|
322
|
+
expect(queryByText('10')).not.toBeNull();
|
|
323
|
+
preset.zoomIn();
|
|
324
|
+
await waitForUpdate();
|
|
325
|
+
expect(queryByText('10')).toBeNull();
|
|
326
|
+
preset.zoomOut();
|
|
327
|
+
await waitForUpdate();
|
|
328
|
+
expect(queryByText('10')).not.toBeNull();
|
|
329
|
+
|
|
330
|
+
preset.zoomIn();
|
|
331
|
+
preset.resetZoom();
|
|
332
|
+
await waitForUpdate();
|
|
333
|
+
expect(queryByText('10')).not.toBeNull();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { Directive, input, viewChild } from '@angular/core';
|
|
2
|
+
import { scaleLinear } from '@tanstack/charts/scales/linear';
|
|
3
|
+
import type {
|
|
4
|
+
ChartPoint,
|
|
5
|
+
ChartPositionScaleOptions,
|
|
6
|
+
} from '@tanstack/charts/types';
|
|
7
|
+
|
|
8
|
+
import { UiChart } from './chart';
|
|
9
|
+
import type { ChartColorSchemeOption } from './chart-theme';
|
|
10
|
+
import type { ChartZoomAxes } from './chart-zoom';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shared pieces of the chart presets (`<ui-line-chart>`, `<ui-bar-chart>`, …).
|
|
14
|
+
* Each preset is a thin component that turns its inputs into a TanStack
|
|
15
|
+
* definition for `<ui-chart>`; this file holds the axis handling and the inputs
|
|
16
|
+
* they all share, so every preset behaves the same way.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** A point in x/y data space, the datum the line, area and scatter presets plot. */
|
|
20
|
+
export type XYDatum = {
|
|
21
|
+
readonly x: number;
|
|
22
|
+
readonly y: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Inner inset per side, as a **fraction** (`0`–`1`) of the plot's width or
|
|
27
|
+
* height, so the first and last marks don't sit on the frame. Fractions keep
|
|
28
|
+
* the inset proportional as the chart resizes. Omitted sides default to `0`.
|
|
29
|
+
*/
|
|
30
|
+
export type ChartPadding = {
|
|
31
|
+
readonly top?: number;
|
|
32
|
+
readonly right?: number;
|
|
33
|
+
readonly bottom?: number;
|
|
34
|
+
readonly left?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The axis, grid and tooltip options every cartesian preset takes. */
|
|
38
|
+
export type CartesianPresetOptions = {
|
|
39
|
+
readonly tickCount?: number;
|
|
40
|
+
readonly showGrid?: boolean;
|
|
41
|
+
readonly showXGrid?: boolean;
|
|
42
|
+
readonly xAxisLabel?: string;
|
|
43
|
+
readonly yAxisLabel?: string;
|
|
44
|
+
readonly padding?: ChartPadding;
|
|
45
|
+
readonly xMin?: number;
|
|
46
|
+
readonly xMax?: number;
|
|
47
|
+
readonly yMin?: number;
|
|
48
|
+
readonly yMax?: number;
|
|
49
|
+
readonly xTickFormat?: (value: number) => string;
|
|
50
|
+
readonly yTickFormat?: (value: number) => string;
|
|
51
|
+
/** `false` turns the tooltip off. */
|
|
52
|
+
readonly tooltip?: boolean;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The x and y axes for a preset's data, from the shared options: `x` and `y`
|
|
57
|
+
* are the values each axis fits, `xMargin` widens x by data units (half a bar),
|
|
58
|
+
* `yInclude` keeps values like a baseline in view.
|
|
59
|
+
*/
|
|
60
|
+
export const presetAxes = (
|
|
61
|
+
options: CartesianPresetOptions,
|
|
62
|
+
data: {
|
|
63
|
+
readonly x: readonly number[];
|
|
64
|
+
readonly y: readonly number[];
|
|
65
|
+
readonly xMargin?: number;
|
|
66
|
+
readonly yInclude?: readonly number[];
|
|
67
|
+
},
|
|
68
|
+
) => {
|
|
69
|
+
const { tickCount = 5, padding = {} } = options;
|
|
70
|
+
return {
|
|
71
|
+
x: linearAxis({
|
|
72
|
+
values: data.x,
|
|
73
|
+
min: options.xMin,
|
|
74
|
+
max: options.xMax,
|
|
75
|
+
margin: data.xMargin,
|
|
76
|
+
tickCount,
|
|
77
|
+
padding: [padding.left ?? 0, padding.right ?? 0],
|
|
78
|
+
format: options.xTickFormat,
|
|
79
|
+
label: options.xAxisLabel,
|
|
80
|
+
grid: options.showXGrid ?? false,
|
|
81
|
+
}),
|
|
82
|
+
y: linearAxis({
|
|
83
|
+
values: data.y,
|
|
84
|
+
include: data.yInclude,
|
|
85
|
+
min: options.yMin,
|
|
86
|
+
max: options.yMax,
|
|
87
|
+
tickCount,
|
|
88
|
+
padding: [padding.bottom ?? 0, padding.top ?? 0],
|
|
89
|
+
format: options.yTickFormat,
|
|
90
|
+
label: options.yAxisLabel,
|
|
91
|
+
grid: options.showGrid ?? true,
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type LinearAxisOptions = {
|
|
97
|
+
/** Data values on this axis, to fit the domain to. */
|
|
98
|
+
readonly values: readonly number[];
|
|
99
|
+
/** Fixed ends; a missing end fits the data. */
|
|
100
|
+
readonly min?: number;
|
|
101
|
+
readonly max?: number;
|
|
102
|
+
/** Values that must stay in view, such as an area's baseline. */
|
|
103
|
+
readonly include?: readonly number[];
|
|
104
|
+
readonly tickCount: number;
|
|
105
|
+
/** Inset at the low and high end, as fractions of the plot (see {@link ChartPadding}). */
|
|
106
|
+
readonly padding?: readonly [number, number];
|
|
107
|
+
/** Extra data units added beyond each end, e.g. half a bar so the edge bars fit. */
|
|
108
|
+
readonly margin?: number;
|
|
109
|
+
readonly format?: (value: number) => string;
|
|
110
|
+
readonly label?: string;
|
|
111
|
+
readonly grid: boolean;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A linear axis config: the domain fits the data (and `include`), is rounded
|
|
116
|
+
* out to nice tick values unless both ends are fixed, then widened by `margin`
|
|
117
|
+
* and `padding`. Flat or empty data gets a span around the value so the marks
|
|
118
|
+
* sit mid-plot instead of collapsing onto one edge.
|
|
119
|
+
*
|
|
120
|
+
* The domain is computed here, not left to TanStack's inference, because
|
|
121
|
+
* padding has to apply after rounding, and a zoomable axis needs a configured
|
|
122
|
+
* scale to carry its viewport.
|
|
123
|
+
*/
|
|
124
|
+
export const linearAxis = (
|
|
125
|
+
options: LinearAxisOptions,
|
|
126
|
+
): ChartPositionScaleOptions<number> => {
|
|
127
|
+
const values = [...options.values, ...(options.include ?? [])].filter(
|
|
128
|
+
Number.isFinite,
|
|
129
|
+
);
|
|
130
|
+
let lo = options.min ?? (values.length ? Math.min(...values) : 0);
|
|
131
|
+
let hi = options.max ?? (values.length ? Math.max(...values) : 1);
|
|
132
|
+
if (lo === hi) {
|
|
133
|
+
const pad = lo === 0 ? 1 : Math.abs(lo) * 0.5;
|
|
134
|
+
lo -= pad;
|
|
135
|
+
hi += pad;
|
|
136
|
+
}
|
|
137
|
+
if (lo > hi) [lo, hi] = [hi, lo];
|
|
138
|
+
if (options.min === undefined || options.max === undefined) {
|
|
139
|
+
const nice = scaleLinear()
|
|
140
|
+
.domain([lo, hi])
|
|
141
|
+
.nice(options.tickCount)
|
|
142
|
+
.domain();
|
|
143
|
+
lo = options.min ?? nice[0];
|
|
144
|
+
hi = options.max ?? nice[1];
|
|
145
|
+
}
|
|
146
|
+
const margin = options.margin ?? 0;
|
|
147
|
+
lo -= margin;
|
|
148
|
+
hi += margin;
|
|
149
|
+
const [padLo, padHi] = options.padding ?? [0, 0];
|
|
150
|
+
const keep = 1 - padLo - padHi;
|
|
151
|
+
if ((padLo || padHi) && keep > 0) {
|
|
152
|
+
const span = (hi - lo) / keep;
|
|
153
|
+
lo -= padLo * span;
|
|
154
|
+
hi = lo + span;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
scale: scaleLinear().domain([lo, hi]),
|
|
158
|
+
grid: options.grid,
|
|
159
|
+
axis: {
|
|
160
|
+
label: options.label || undefined,
|
|
161
|
+
ticks: { count: options.tickCount, format: options.format },
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Smallest gap between neighboring values, `1` for fewer than two. */
|
|
167
|
+
export const minSpacing = (values: readonly number[]): number => {
|
|
168
|
+
const sorted = [...new Set(values)].sort((a, b) => a - b);
|
|
169
|
+
let spacing = Infinity;
|
|
170
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
171
|
+
spacing = Math.min(spacing, sorted[i]! - sorted[i - 1]!);
|
|
172
|
+
}
|
|
173
|
+
return Number.isFinite(spacing) && spacing > 0 ? spacing : 1;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* `UiChart`, for `viewChild()`. A call rather than the class itself: for a
|
|
178
|
+
* class, Angular's test build keeps an arrow function that nothing calls,
|
|
179
|
+
* which an app's coverage report would show as uncovered.
|
|
180
|
+
*/
|
|
181
|
+
const chartType = () => UiChart;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The inputs and zoom API every cartesian preset shares. A preset extends this
|
|
185
|
+
* and adds its own data and mark inputs, and renders `<ui-chart #chart>`
|
|
186
|
+
* with the shared bindings.
|
|
187
|
+
*/
|
|
188
|
+
@Directive()
|
|
189
|
+
export abstract class UiCartesianPreset<TDatum> {
|
|
190
|
+
/** Fixed width in px. Omit to fill the container. */
|
|
191
|
+
readonly width = input<number>();
|
|
192
|
+
readonly height = input(200);
|
|
193
|
+
/** Target number of ticks per axis; the axis picks nice values near it. */
|
|
194
|
+
readonly tickCount = input(5);
|
|
195
|
+
/** Horizontal gridlines at the y ticks. */
|
|
196
|
+
readonly showGrid = input(true);
|
|
197
|
+
/** Vertical gridlines at the x ticks. */
|
|
198
|
+
readonly showXGrid = input(false);
|
|
199
|
+
readonly xAxisLabel = input('');
|
|
200
|
+
readonly yAxisLabel = input('');
|
|
201
|
+
readonly padding = input<ChartPadding>({});
|
|
202
|
+
readonly xMin = input<number>();
|
|
203
|
+
readonly xMax = input<number>();
|
|
204
|
+
readonly yMin = input<number>();
|
|
205
|
+
readonly yMax = input<number>();
|
|
206
|
+
readonly xTickFormat = input<(value: number) => string>();
|
|
207
|
+
readonly yTickFormat = input<(value: number) => string>();
|
|
208
|
+
/** Shows a tooltip for the touched point; a tap pins it. */
|
|
209
|
+
readonly tooltip = input(true);
|
|
210
|
+
/** What a screen reader announces for the chart. */
|
|
211
|
+
readonly label = input('Chart');
|
|
212
|
+
/** Light, dark or `'system'` colors; the chart theme's setting when unset. */
|
|
213
|
+
readonly colorScheme = input<Exclude<ChartColorSchemeOption, Function>>();
|
|
214
|
+
readonly zoomable = input(false);
|
|
215
|
+
readonly zoomAxes = input<ChartZoomAxes>('xy');
|
|
216
|
+
readonly showZoomControls = input(true);
|
|
217
|
+
readonly minZoom = input(1);
|
|
218
|
+
readonly maxZoom = input(8);
|
|
219
|
+
readonly zoomStep = input(1.4);
|
|
220
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
221
|
+
|
|
222
|
+
protected readonly chart = viewChild.required(chartType());
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The shared inputs, as the options every preset's definition takes.
|
|
226
|
+
*/
|
|
227
|
+
protected commonOptions(): CartesianPresetOptions {
|
|
228
|
+
return {
|
|
229
|
+
tickCount: this.tickCount(),
|
|
230
|
+
showGrid: this.showGrid(),
|
|
231
|
+
showXGrid: this.showXGrid(),
|
|
232
|
+
xAxisLabel: this.xAxisLabel(),
|
|
233
|
+
yAxisLabel: this.yAxisLabel(),
|
|
234
|
+
padding: this.padding(),
|
|
235
|
+
xMin: this.xMin(),
|
|
236
|
+
xMax: this.xMax(),
|
|
237
|
+
yMin: this.yMin(),
|
|
238
|
+
yMax: this.yMax(),
|
|
239
|
+
xTickFormat: this.xTickFormat(),
|
|
240
|
+
yTickFormat: this.yTickFormat(),
|
|
241
|
+
tooltip: this.tooltip(),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The tapped datum, unwrapped from TanStack's point.
|
|
247
|
+
*/
|
|
248
|
+
protected datumOf(point: ChartPoint | null): TDatum | null {
|
|
249
|
+
return point ? (point.datum as TDatum) : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
zoomIn(): void {
|
|
253
|
+
this.chart().zoomIn();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
zoomOut(): void {
|
|
257
|
+
this.chart().zoomOut();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
resetZoom(): void {
|
|
261
|
+
this.chart().resetZoom();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The `<ui-chart>` bindings a preset passes straight through. */
|
|
266
|
+
export const PRESET_CHART_BINDINGS = `
|
|
267
|
+
[width]="width()"
|
|
268
|
+
[height]="height()"
|
|
269
|
+
[label]="label()"
|
|
270
|
+
[colorScheme]="colorScheme()"
|
|
271
|
+
[class]="userClass()"
|
|
272
|
+
[zoomable]="zoomable()"
|
|
273
|
+
[zoomAxes]="zoomAxes()"
|
|
274
|
+
[showZoomControls]="showZoomControls()"
|
|
275
|
+
[minZoom]="minZoom()"
|
|
276
|
+
[maxZoom]="maxZoom()"
|
|
277
|
+
[zoomStep]="zoomStep()"
|
|
278
|
+
`;
|