@blotch/dolan 0.0.0-stage → 22.2.1
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 +22 -0
- package/dist/commands/add.js +132 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/add.test.d.ts +1 -0
- package/dist/commands/add.test.js +209 -0
- package/dist/commands/add.test.js.map +1 -0
- package/dist/commands/diff.d.ts +13 -0
- package/dist/commands/diff.js +155 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/diff.test.d.ts +1 -0
- package/dist/commands/diff.test.js +327 -0
- package/dist/commands/diff.test.js.map +1 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/doctor.js +248 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/doctor.test.js +301 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/eject.d.ts +11 -0
- package/dist/commands/eject.js +55 -0
- package/dist/commands/eject.js.map +1 -0
- package/dist/commands/eject.test.d.ts +1 -0
- package/dist/commands/eject.test.js +151 -0
- package/dist/commands/eject.test.js.map +1 -0
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.js +73 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/info.test.d.ts +1 -0
- package/dist/commands/info.test.js +143 -0
- package/dist/commands/info.test.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +154 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/list.d.ts +16 -0
- package/dist/commands/list.js +124 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/list.test.d.ts +1 -0
- package/dist/commands/list.test.js +285 -0
- package/dist/commands/list.test.js.map +1 -0
- package/dist/commands/outdated.d.ts +13 -0
- package/dist/commands/outdated.js +116 -0
- package/dist/commands/outdated.js.map +1 -0
- package/dist/commands/outdated.test.d.ts +1 -0
- package/dist/commands/outdated.test.js +208 -0
- package/dist/commands/outdated.test.js.map +1 -0
- package/dist/commands/remove.d.ts +18 -0
- package/dist/commands/remove.js +161 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/remove.test.d.ts +1 -0
- package/dist/commands/remove.test.js +361 -0
- package/dist/commands/remove.test.js.map +1 -0
- package/dist/commands/theme.d.ts +1 -0
- package/dist/commands/theme.js +66 -0
- package/dist/commands/theme.js.map +1 -0
- package/dist/commands/theme.test.d.ts +1 -0
- package/dist/commands/theme.test.js +133 -0
- package/dist/commands/theme.test.js.map +1 -0
- package/dist/commands/update.d.ts +31 -0
- package/dist/commands/update.js +601 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/update.test.d.ts +1 -0
- package/dist/commands/update.test.js +1233 -0
- package/dist/commands/update.test.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +48 -0
- package/dist/config.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/lockfile.d.ts +21 -0
- package/dist/lockfile.js +39 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +85 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.test.d.ts +1 -0
- package/dist/registry.test.js +95 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/test-utils.d.ts +16 -0
- package/dist/test-utils.js +65 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/ui/components/accordion/accordion.test.ts +194 -0
- package/dist/ui/components/accordion/accordion.ts +173 -0
- package/dist/ui/components/accordion/index.ts +6 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +211 -0
- package/dist/ui/components/action-sheet/action-sheet.ts +353 -0
- package/dist/ui/components/action-sheet/index.ts +6 -0
- package/dist/ui/components/alert/alert.test.ts +48 -0
- package/dist/ui/components/alert/alert.ts +104 -0
- package/dist/ui/components/alert/index.ts +1 -0
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +141 -0
- package/dist/ui/components/alert-dialog/alert-dialog.ts +243 -0
- package/dist/ui/components/alert-dialog/index.ts +7 -0
- package/dist/ui/components/area-chart/area-chart-render.test.ts +121 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +158 -0
- package/dist/ui/components/area-chart/area-chart.ts +349 -0
- package/dist/ui/components/area-chart/index.ts +6 -0
- package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +10 -0
- package/dist/ui/components/aspect-ratio/aspect-ratio.ts +28 -0
- package/dist/ui/components/aspect-ratio/index.ts +1 -0
- package/dist/ui/components/avatar/avatar.test.ts +69 -0
- package/dist/ui/components/avatar/avatar.ts +131 -0
- package/dist/ui/components/avatar/index.ts +1 -0
- package/dist/ui/components/badge/badge.test.ts +49 -0
- package/dist/ui/components/badge/badge.ts +97 -0
- package/dist/ui/components/badge/index.ts +1 -0
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +174 -0
- package/dist/ui/components/bar-chart/bar-chart.test.ts +73 -0
- package/dist/ui/components/bar-chart/bar-chart.ts +327 -0
- package/dist/ui/components/bar-chart/index.ts +6 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +163 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +339 -0
- package/dist/ui/components/bottom-sheet/index.ts +1 -0
- package/dist/ui/components/button/button-render.test.ts +113 -0
- package/dist/ui/components/button/button.test.ts +70 -0
- package/dist/ui/components/button/button.ts +170 -0
- package/dist/ui/components/button/index.ts +6 -0
- package/dist/ui/components/button-group/button-group-render.test.ts +146 -0
- package/dist/ui/components/button-group/button-group.test.ts +54 -0
- package/dist/ui/components/button-group/button-group.ts +209 -0
- package/dist/ui/components/button-group/index.ts +8 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +174 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +96 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +355 -0
- package/dist/ui/components/candlestick-chart/index.ts +7 -0
- package/dist/ui/components/card/card.test.ts +110 -0
- package/dist/ui/components/card/card.ts +162 -0
- package/dist/ui/components/card/index.ts +8 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +188 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +549 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +1140 -0
- package/dist/ui/components/cartesian-chart/index.ts +18 -0
- package/dist/ui/components/checkbox/checkbox.test.ts +59 -0
- package/dist/ui/components/checkbox/checkbox.ts +96 -0
- package/dist/ui/components/checkbox/index.ts +1 -0
- package/dist/ui/components/collapsible/collapsible.test.ts +112 -0
- package/dist/ui/components/collapsible/collapsible.ts +122 -0
- package/dist/ui/components/collapsible/index.ts +5 -0
- package/dist/ui/components/dialog/dialog.test.ts +157 -0
- package/dist/ui/components/dialog/dialog.ts +248 -0
- package/dist/ui/components/dialog/index.ts +7 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +60 -0
- package/dist/ui/components/empty-state/empty-state.ts +46 -0
- package/dist/ui/components/empty-state/index.ts +1 -0
- package/dist/ui/components/icon/icon.test.ts +52 -0
- package/dist/ui/components/icon/icon.ts +54 -0
- package/dist/ui/components/icon/icons.ts +569 -0
- package/dist/ui/components/icon/index.ts +2 -0
- package/dist/ui/components/input/index.ts +1 -0
- package/dist/ui/components/input/input.test.ts +134 -0
- package/dist/ui/components/input/input.ts +189 -0
- package/dist/ui/components/label/index.ts +1 -0
- package/dist/ui/components/label/label.test.ts +38 -0
- package/dist/ui/components/label/label.ts +24 -0
- package/dist/ui/components/line-chart/index.ts +6 -0
- package/dist/ui/components/line-chart/line-chart-render.test.ts +121 -0
- package/dist/ui/components/line-chart/line-chart.test.ts +81 -0
- package/dist/ui/components/line-chart/line-chart.ts +273 -0
- package/dist/ui/components/list/index.ts +1 -0
- package/dist/ui/components/list/list.test.ts +35 -0
- package/dist/ui/components/list/list.ts +71 -0
- package/dist/ui/components/nav-drawer/index.ts +8 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +267 -0
- package/dist/ui/components/nav-drawer/nav-drawer.ts +504 -0
- package/dist/ui/components/pagination/index.ts +1 -0
- package/dist/ui/components/pagination/pagination.test.ts +96 -0
- package/dist/ui/components/pagination/pagination.ts +147 -0
- package/dist/ui/components/progress/index.ts +1 -0
- package/dist/ui/components/progress/progress.test.ts +71 -0
- package/dist/ui/components/progress/progress.ts +118 -0
- package/dist/ui/components/radio-group/index.ts +1 -0
- package/dist/ui/components/radio-group/radio-group.test.ts +115 -0
- package/dist/ui/components/radio-group/radio-group.ts +129 -0
- package/dist/ui/components/scatter-chart/index.ts +6 -0
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +157 -0
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -0
- package/dist/ui/components/scatter-chart/scatter-chart.ts +277 -0
- package/dist/ui/components/scroll-area/index.ts +1 -0
- package/dist/ui/components/scroll-area/scroll-area.test.ts +40 -0
- package/dist/ui/components/scroll-area/scroll-area.ts +73 -0
- package/dist/ui/components/select/index.ts +1 -0
- package/dist/ui/components/select/select.test.ts +107 -0
- package/dist/ui/components/select/select.ts +146 -0
- package/dist/ui/components/separator/index.ts +1 -0
- package/dist/ui/components/separator/separator.test.ts +26 -0
- package/dist/ui/components/separator/separator.ts +45 -0
- package/dist/ui/components/sheet/index.ts +7 -0
- package/dist/ui/components/sheet/sheet.test.ts +44 -0
- package/dist/ui/components/sheet/sheet.ts +114 -0
- package/dist/ui/components/skeleton/index.ts +1 -0
- package/dist/ui/components/skeleton/skeleton.test.ts +13 -0
- package/dist/ui/components/skeleton/skeleton.ts +45 -0
- package/dist/ui/components/spinner/index.ts +1 -0
- package/dist/ui/components/spinner/spinner.test.ts +39 -0
- package/dist/ui/components/spinner/spinner.ts +83 -0
- package/dist/ui/components/switch/index.ts +1 -0
- package/dist/ui/components/switch/switch.test.ts +63 -0
- package/dist/ui/components/switch/switch.ts +98 -0
- package/dist/ui/components/tabs/index.ts +1 -0
- package/dist/ui/components/tabs/tabs.test.ts +199 -0
- package/dist/ui/components/tabs/tabs.ts +317 -0
- package/dist/ui/components/textarea/index.ts +1 -0
- package/dist/ui/components/textarea/textarea.test.ts +138 -0
- package/dist/ui/components/textarea/textarea.ts +221 -0
- package/dist/ui/components/toast/index.ts +9 -0
- package/dist/ui/components/toast/toast-state.test.ts +54 -0
- package/dist/ui/components/toast/toast-state.ts +46 -0
- package/dist/ui/components/toast/toast.test.ts +342 -0
- package/dist/ui/components/toast/toast.ts +527 -0
- package/dist/ui/components/toggle/index.ts +7 -0
- package/dist/ui/components/toggle/toggle.test.ts +186 -0
- package/dist/ui/components/toggle/toggle.ts +218 -0
- package/dist/ui/components/typography/index.ts +5 -0
- package/dist/ui/components/typography/typography.test.ts +55 -0
- package/dist/ui/components/typography/typography.ts +44 -0
- package/dist/ui/theme/dark.css +41 -0
- package/dist/ui/theme/default.css +56 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +124 -0
- package/dist/ui/theme/tailwind-plugin.ts +151 -0
- package/dist/ui/utils/animate.test.ts +324 -0
- package/dist/ui/utils/animate.ts +544 -0
- package/dist/ui/utils/cn.test.ts +27 -0
- package/dist/ui/utils/cn.ts +6 -0
- package/dist/utils/analyze.d.ts +32 -0
- package/dist/utils/analyze.js +109 -0
- package/dist/utils/analyze.js.map +1 -0
- package/dist/utils/analyze.test.d.ts +1 -0
- package/dist/utils/analyze.test.js +146 -0
- package/dist/utils/analyze.test.js.map +1 -0
- package/dist/utils/animate.d.ts +210 -0
- package/dist/utils/animate.js +423 -0
- package/dist/utils/animate.js.map +1 -0
- package/dist/utils/animate.test.d.ts +1 -0
- package/dist/utils/animate.test.js +365 -0
- package/dist/utils/animate.test.js.map +1 -0
- package/dist/utils/banner.d.ts +1 -0
- package/dist/utils/banner.js +15 -0
- package/dist/utils/banner.js.map +1 -0
- package/dist/utils/banner.test.d.ts +1 -0
- package/dist/utils/banner.test.js +29 -0
- package/dist/utils/banner.test.js.map +1 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +6 -0
- package/dist/utils/cn.js.map +1 -0
- package/dist/utils/cn.test.d.ts +1 -0
- package/dist/utils/cn.test.js +21 -0
- package/dist/utils/cn.test.js.map +1 -0
- package/dist/utils/format.d.ts +31 -0
- package/dist/utils/format.js +95 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/format.test.d.ts +1 -0
- package/dist/utils/format.test.js +71 -0
- package/dist/utils/format.test.js.map +1 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.js +16 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.test.d.ts +1 -0
- package/dist/utils/logger.test.js +36 -0
- package/dist/utils/logger.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +12 -0
- package/dist/utils/resolve-paths.js +29 -0
- package/dist/utils/resolve-paths.js.map +1 -0
- package/dist/utils/resolve-paths.test.d.ts +1 -0
- package/dist/utils/resolve-paths.test.js +52 -0
- package/dist/utils/resolve-paths.test.js.map +1 -0
- package/package.json +56 -4
- package/README.md +0 -3
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
4
|
+
import { UiCartesianChart } from '../cartesian-chart/cartesian-chart';
|
|
5
|
+
import {
|
|
6
|
+
UiCandlestickChart,
|
|
7
|
+
UiCandlestickSeries,
|
|
8
|
+
type CandlestickPoint,
|
|
9
|
+
} from './candlestick-chart';
|
|
10
|
+
|
|
11
|
+
const period = (
|
|
12
|
+
x: number,
|
|
13
|
+
o: number,
|
|
14
|
+
h: number,
|
|
15
|
+
l: number,
|
|
16
|
+
c: number,
|
|
17
|
+
): CandlestickPoint => ({ x, open: o, high: h, low: l, close: c });
|
|
18
|
+
|
|
19
|
+
describe('UiCandlestickChart', () => {
|
|
20
|
+
it('renders with default configuration', async () => {
|
|
21
|
+
const { container } = await render(UiCandlestickChart);
|
|
22
|
+
expect(container).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('derives the y-axis from the lowest low and highest high', async () => {
|
|
26
|
+
const { componentRef } = await render(UiCandlestickChart);
|
|
27
|
+
const inst = componentRef.instance as UiCandlestickChart;
|
|
28
|
+
setInputSignal(inst.data, [
|
|
29
|
+
period(0, 10, 15, 5, 12),
|
|
30
|
+
period(1, 12, 20, 8, 18),
|
|
31
|
+
period(2, 18, 22, 14, 16),
|
|
32
|
+
]);
|
|
33
|
+
await waitForUpdate();
|
|
34
|
+
const cast = inst as unknown as {
|
|
35
|
+
xAxis: () => { domain: readonly [number, number] };
|
|
36
|
+
yAxis: () => { domain: readonly [number, number] };
|
|
37
|
+
effectivePadding: () => { left?: number; right?: number };
|
|
38
|
+
};
|
|
39
|
+
expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(0);
|
|
40
|
+
expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(2);
|
|
41
|
+
expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(5);
|
|
42
|
+
expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(22);
|
|
43
|
+
// 3 candles → half = 0.5 / 2 = 0.25
|
|
44
|
+
expect(cast.effectivePadding().left).toBeCloseTo(0.25);
|
|
45
|
+
expect(cast.effectivePadding().right).toBeCloseTo(0.25);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('honors explicit xMin / xMax / yMin / yMax overrides', async () => {
|
|
49
|
+
const { componentRef } = await render(UiCandlestickChart);
|
|
50
|
+
const inst = componentRef.instance as UiCandlestickChart;
|
|
51
|
+
setInputSignal(inst.data, [period(0, 10, 15, 5, 12)]);
|
|
52
|
+
setInputSignal(inst.xMin, -5);
|
|
53
|
+
setInputSignal(inst.xMax, 20);
|
|
54
|
+
setInputSignal(inst.yMin, 0);
|
|
55
|
+
setInputSignal(inst.yMax, 100);
|
|
56
|
+
await waitForUpdate();
|
|
57
|
+
const cast = inst as unknown as {
|
|
58
|
+
xAxis: () => { domain: readonly [number, number] };
|
|
59
|
+
yAxis: () => { domain: readonly [number, number] };
|
|
60
|
+
};
|
|
61
|
+
expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(-5);
|
|
62
|
+
expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(20);
|
|
63
|
+
expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(0);
|
|
64
|
+
expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(100);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('single-datum series drops the half-candle padding', async () => {
|
|
68
|
+
const { componentRef } = await render(UiCandlestickChart);
|
|
69
|
+
const inst = componentRef.instance as UiCandlestickChart;
|
|
70
|
+
setInputSignal(inst.data, [period(0, 10, 15, 5, 12)]);
|
|
71
|
+
await waitForUpdate();
|
|
72
|
+
const cast = inst as unknown as {
|
|
73
|
+
effectivePadding: () => { left?: number; right?: number };
|
|
74
|
+
};
|
|
75
|
+
expect(cast.effectivePadding().left).toBe(0);
|
|
76
|
+
expect(cast.effectivePadding().right).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('user padding overrides the auto half-candle inset per side', async () => {
|
|
80
|
+
const { componentRef } = await render(UiCandlestickChart);
|
|
81
|
+
const inst = componentRef.instance as UiCandlestickChart;
|
|
82
|
+
setInputSignal(inst.data, [
|
|
83
|
+
period(0, 10, 15, 5, 12),
|
|
84
|
+
period(1, 12, 20, 8, 18),
|
|
85
|
+
]);
|
|
86
|
+
setInputSignal(inst.padding, { right: 0.05 });
|
|
87
|
+
await waitForUpdate();
|
|
88
|
+
const cast = inst as unknown as {
|
|
89
|
+
effectivePadding: () => { left?: number; right?: number };
|
|
90
|
+
};
|
|
91
|
+
expect(cast.effectivePadding().right).toBeCloseTo(0.05);
|
|
92
|
+
expect(cast.effectivePadding().left).toBeCloseTo(0.5);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('exposes the default tick formatter used by the child cartesian chart', async () => {
|
|
96
|
+
const { componentRef } = await render(UiCandlestickChart);
|
|
97
|
+
const inst = componentRef.instance as UiCandlestickChart;
|
|
98
|
+
expect(inst.xTickFormat()(3.14159)).toBe('3.14');
|
|
99
|
+
expect(inst.yTickFormat()(1.005)).toBe('1');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('UiCandlestickSeries', () => {
|
|
104
|
+
const renderSeries = async () => {
|
|
105
|
+
const parentResult = await render(UiCartesianChart);
|
|
106
|
+
const parent = parentResult.componentRef.instance as UiCartesianChart;
|
|
107
|
+
setInputSignal(parent.xDomain, [0, 10]);
|
|
108
|
+
setInputSignal(parent.yDomain, [0, 100]);
|
|
109
|
+
await waitForUpdate();
|
|
110
|
+
const seriesResult = await render(UiCandlestickSeries, {
|
|
111
|
+
providers: [{ provide: UiCartesianChart, useValue: parent }],
|
|
112
|
+
});
|
|
113
|
+
const series = seriesResult.componentRef.instance as UiCandlestickSeries;
|
|
114
|
+
setInputSignal(series.data, [
|
|
115
|
+
// up candle (close >= open)
|
|
116
|
+
period(0, 10, 20, 5, 15),
|
|
117
|
+
// down candle (close < open)
|
|
118
|
+
period(5, 60, 70, 50, 55),
|
|
119
|
+
// doji (close === open)
|
|
120
|
+
period(10, 40, 50, 30, 40),
|
|
121
|
+
]);
|
|
122
|
+
await waitForUpdate();
|
|
123
|
+
return { parent, series };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
it('renders a wick + body for each period, tinted by direction', async () => {
|
|
127
|
+
const { series } = await renderSeries();
|
|
128
|
+
const cast = series as unknown as {
|
|
129
|
+
candles: () => {
|
|
130
|
+
wickStyle: string;
|
|
131
|
+
bodyStyle: string;
|
|
132
|
+
point: CandlestickPoint;
|
|
133
|
+
}[];
|
|
134
|
+
};
|
|
135
|
+
const candles = cast.candles();
|
|
136
|
+
expect(candles.length).toBe(3);
|
|
137
|
+
// First: up candle → uses upColor (green rgba)
|
|
138
|
+
expect(candles[0].bodyStyle).toContain('rgba(34, 197, 94, 1)');
|
|
139
|
+
// Second: down candle → uses downColor (red rgba)
|
|
140
|
+
expect(candles[1].bodyStyle).toContain('rgba(239, 68, 68, 1)');
|
|
141
|
+
// Third: doji (open === close) is `up: true` (>= comparison)
|
|
142
|
+
expect(candles[2].bodyStyle).toContain('rgba(34, 197, 94, 1)');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('emits candleTap with the source point when a body is tapped', async () => {
|
|
146
|
+
const { series } = await renderSeries();
|
|
147
|
+
const cb = vi.fn();
|
|
148
|
+
series.candleTap.subscribe(cb);
|
|
149
|
+
const p = period(1, 2, 3, 1, 2);
|
|
150
|
+
(
|
|
151
|
+
series as unknown as { onCandleTap: (point: CandlestickPoint) => void }
|
|
152
|
+
).onCandleTap(p);
|
|
153
|
+
expect(cb).toHaveBeenCalledWith(p);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('single-datum series uses the fallback body width', async () => {
|
|
157
|
+
const parentResult = await render(UiCartesianChart);
|
|
158
|
+
const parent = parentResult.componentRef.instance as UiCartesianChart;
|
|
159
|
+
setInputSignal(parent.xDomain, [0, 10]);
|
|
160
|
+
setInputSignal(parent.yDomain, [0, 100]);
|
|
161
|
+
await waitForUpdate();
|
|
162
|
+
const seriesResult = await render(UiCandlestickSeries, {
|
|
163
|
+
providers: [{ provide: UiCartesianChart, useValue: parent }],
|
|
164
|
+
});
|
|
165
|
+
const series = seriesResult.componentRef.instance as UiCandlestickSeries;
|
|
166
|
+
setInputSignal(series.data, [period(5, 10, 15, 5, 12)]);
|
|
167
|
+
await waitForUpdate();
|
|
168
|
+
const cast = series as unknown as {
|
|
169
|
+
candles: () => { bodyStyle: string }[];
|
|
170
|
+
};
|
|
171
|
+
// fallback body width = 8px
|
|
172
|
+
expect(cast.candles()[0].bodyStyle).toContain('width: 8.00px');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock Angular (and the transitively-loaded cartesian-chart Angular usage) so
|
|
4
|
+
// importing the module needs no framework runtime. Only the exported pure
|
|
5
|
+
// `computeCandles` is exercised; the components are never built.
|
|
6
|
+
vi.mock('@angular/core', () => ({
|
|
7
|
+
Component: () => () => {},
|
|
8
|
+
ViewEncapsulation: { None: 0 },
|
|
9
|
+
computed: () => () => {},
|
|
10
|
+
inject: () => ({}),
|
|
11
|
+
input: () => () => {},
|
|
12
|
+
output: () => () => {},
|
|
13
|
+
}));
|
|
14
|
+
|
|
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 { computeCandles } = await import('./candlestick-chart');
|
|
25
|
+
|
|
26
|
+
// A rising candle centered at cx=50. Screen y grows downward, so the projected
|
|
27
|
+
// highY (top of the wick) is the smallest pixel value and lowY the largest.
|
|
28
|
+
// close is above open (up), so closeY < openY in pixels.
|
|
29
|
+
const upCandle = {
|
|
30
|
+
cx: 50,
|
|
31
|
+
highY: 10, // high price → top of plot
|
|
32
|
+
lowY: 90, // low price → bottom of plot
|
|
33
|
+
openY: 70,
|
|
34
|
+
closeY: 30,
|
|
35
|
+
up: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe('computeCandles', () => {
|
|
39
|
+
it('draws the wick spanning high to low, centered on cx', () => {
|
|
40
|
+
const [shape] = computeCandles([upCandle], 12);
|
|
41
|
+
expect(shape.wick.top).toBe(10); // min(highY, lowY)
|
|
42
|
+
expect(shape.wick.height).toBe(80); // |lowY - highY|
|
|
43
|
+
expect(shape.wick.width).toBe(1); // default wickWidth
|
|
44
|
+
// Centered: left = cx - wickWidth/2.
|
|
45
|
+
expect(shape.wick.left).toBe(50 - 0.5);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('draws the body between open and close, centered on cx', () => {
|
|
49
|
+
const [shape] = computeCandles([upCandle], 12);
|
|
50
|
+
expect(shape.body.top).toBe(30); // min(openY, closeY) — the higher price
|
|
51
|
+
expect(shape.body.height).toBe(40); // |closeY - openY|
|
|
52
|
+
expect(shape.body.width).toBe(12);
|
|
53
|
+
expect(shape.body.left).toBe(50 - 6); // cx - bodyWidth/2
|
|
54
|
+
expect(shape.up).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('marks a falling candle as down and still tops the body at the higher price', () => {
|
|
58
|
+
// close below open: closeY (pixels) is BELOW openY, i.e. larger.
|
|
59
|
+
const downCandle = {
|
|
60
|
+
cx: 20,
|
|
61
|
+
highY: 5,
|
|
62
|
+
lowY: 95,
|
|
63
|
+
openY: 30,
|
|
64
|
+
closeY: 60,
|
|
65
|
+
up: false,
|
|
66
|
+
};
|
|
67
|
+
const [shape] = computeCandles([downCandle], 10);
|
|
68
|
+
expect(shape.up).toBe(false);
|
|
69
|
+
expect(shape.body.top).toBe(30); // min(openY, closeY) = openY here
|
|
70
|
+
expect(shape.body.height).toBe(30);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('clamps a doji (open === close) to the minimum body height', () => {
|
|
74
|
+
const doji = {
|
|
75
|
+
cx: 0,
|
|
76
|
+
highY: 0,
|
|
77
|
+
lowY: 100,
|
|
78
|
+
openY: 50,
|
|
79
|
+
closeY: 50,
|
|
80
|
+
up: true,
|
|
81
|
+
};
|
|
82
|
+
// Zero-height body would be invisible; the floor keeps it a thin line.
|
|
83
|
+
expect(computeCandles([doji], 12)[0].body.height).toBe(1);
|
|
84
|
+
expect(computeCandles([doji], 12, 1, 3)[0].body.height).toBe(3);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('honors a custom wick width', () => {
|
|
88
|
+
const [shape] = computeCandles([upCandle], 12, 3);
|
|
89
|
+
expect(shape.wick.width).toBe(3);
|
|
90
|
+
expect(shape.wick.left).toBe(50 - 1.5);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns an empty array for no candles', () => {
|
|
94
|
+
expect(computeCandles([], 12)).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
computed,
|
|
5
|
+
inject,
|
|
6
|
+
input,
|
|
7
|
+
output,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ChartPadding,
|
|
13
|
+
UiCartesianChart,
|
|
14
|
+
niceScale,
|
|
15
|
+
} from '../cartesian-chart/cartesian-chart';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One OHLC period: the `open`, `high`, `low`, and `close` prices at position
|
|
23
|
+
* `x`. `x` is a plain number (a period index), not a `Date` — Lynx has no `Intl`
|
|
24
|
+
* to format dates, so the whole chart system keeps x numeric (see {@link
|
|
25
|
+
* ChartPoint}). Format the axis with `xTickFormat` if you need labels.
|
|
26
|
+
*/
|
|
27
|
+
export type CandlestickPoint = {
|
|
28
|
+
x: number;
|
|
29
|
+
open: number;
|
|
30
|
+
high: number;
|
|
31
|
+
low: number;
|
|
32
|
+
close: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** One candle after projection into plot-local pixel space. */
|
|
36
|
+
type ProjectedCandle = {
|
|
37
|
+
/** Center x of the candle, in px. */
|
|
38
|
+
cx: number;
|
|
39
|
+
openY: number;
|
|
40
|
+
highY: number;
|
|
41
|
+
lowY: number;
|
|
42
|
+
closeY: number;
|
|
43
|
+
/** `true` when the period closed at or above its open (a rising candle). */
|
|
44
|
+
up: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** An axis-aligned rectangle in plot-local pixel space. */
|
|
48
|
+
type Rect = { left: number; top: number; width: number; height: number };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Render geometry for one candle: a thin vertical **wick** (high→low) and a
|
|
52
|
+
* **body** (open↔close), both plain axis-aligned rectangles. `up` is echoed from
|
|
53
|
+
* the input so the caller can pick the fill color.
|
|
54
|
+
*/
|
|
55
|
+
export type CandleShape = {
|
|
56
|
+
wick: Rect;
|
|
57
|
+
body: Rect;
|
|
58
|
+
up: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Pure geometry (exported so it can be unit-tested without Angular)
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Turns projected candles — already in plot-local pixel space — into wick/body
|
|
67
|
+
* rectangles ready to render as `<view>`s. Unlike the line and area series, a
|
|
68
|
+
* candlestick needs no rotation or fill rasterization: each mark is a plain
|
|
69
|
+
* axis-aligned box, so this is a straight geometric mapping.
|
|
70
|
+
*
|
|
71
|
+
* Screen y grows *downward*, so a higher price projects to a *smaller* y. Hence
|
|
72
|
+
* the wick's top is `min(highY, lowY)` and the body's top is `min(openY,
|
|
73
|
+
* closeY)` — taking the pixel min picks the visually-higher (larger-value) edge.
|
|
74
|
+
*
|
|
75
|
+
* A doji (open === close) would give a zero-height body that renders invisibly,
|
|
76
|
+
* so the body height is clamped to `minBodyHeight` (a thin line, like a bar).
|
|
77
|
+
* Wick and body are both centered on the candle's `cx`.
|
|
78
|
+
*
|
|
79
|
+
* @param bodyWidth width of the body rectangle in px (the wick stays `wickWidth`).
|
|
80
|
+
* @param minBodyHeight floor for the body height so a doji is still visible.
|
|
81
|
+
*/
|
|
82
|
+
export const computeCandles = (
|
|
83
|
+
candles: readonly ProjectedCandle[],
|
|
84
|
+
bodyWidth: number,
|
|
85
|
+
wickWidth = 1,
|
|
86
|
+
minBodyHeight = 1,
|
|
87
|
+
): CandleShape[] =>
|
|
88
|
+
candles.map((c) => {
|
|
89
|
+
const bodyTop = Math.min(c.openY, c.closeY);
|
|
90
|
+
const bodyHeight = Math.max(Math.abs(c.closeY - c.openY), minBodyHeight);
|
|
91
|
+
const wickTop = Math.min(c.highY, c.lowY);
|
|
92
|
+
const wickHeight = Math.abs(c.lowY - c.highY);
|
|
93
|
+
return {
|
|
94
|
+
wick: {
|
|
95
|
+
left: c.cx - wickWidth / 2,
|
|
96
|
+
top: wickTop,
|
|
97
|
+
width: wickWidth,
|
|
98
|
+
height: wickHeight,
|
|
99
|
+
},
|
|
100
|
+
body: {
|
|
101
|
+
left: c.cx - bodyWidth / 2,
|
|
102
|
+
top: bodyTop,
|
|
103
|
+
width: bodyWidth,
|
|
104
|
+
height: bodyHeight,
|
|
105
|
+
},
|
|
106
|
+
up: c.up,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Rendering helpers (mirror the equivalents in line-chart.ts / area-chart.ts —
|
|
112
|
+
// kept local so this module stays self-contained when scaffolded by `dolan add`)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Formats a pixel value for an inline style, avoiding scientific notation.
|
|
117
|
+
*/
|
|
118
|
+
const px = (n: number): string => `${n.toFixed(2)}px`;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Default axis label formatter — plain JS only (no `Intl`, absent in Lynx).
|
|
122
|
+
*/
|
|
123
|
+
const defaultTickFormat = (value: number): string =>
|
|
124
|
+
String(Math.round(value * 100) / 100);
|
|
125
|
+
|
|
126
|
+
// Fallback body width (px) when there is only a single candle, so there is no
|
|
127
|
+
// neighbor to derive a spacing from. Matches the React source's `font(8)`.
|
|
128
|
+
const SINGLE_CANDLE_BODY_WIDTH = 8;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A single candlestick series drawn inside a `<ui-cartesian-chart>`. Reads the
|
|
132
|
+
* shared scales from the parent chart via DI and renders, per period, a wick
|
|
133
|
+
* `<view>` (high→low) and a body `<view>` (open↔close), colored by direction.
|
|
134
|
+
*
|
|
135
|
+
* ## Why the host is `position: absolute; width/height: 100%`
|
|
136
|
+
* Identical to `UiLineSeries`: Lynx resolves an absolute child against its
|
|
137
|
+
* **direct parent** (there is no `position: static`), so the host itself must be
|
|
138
|
+
* the plot-sized containing block. Every wick and body is a direct child of it.
|
|
139
|
+
*
|
|
140
|
+
* ## Differentiating up vs down
|
|
141
|
+
* On a color screen, the fill color carries direction — up candles use
|
|
142
|
+
* `upColor`, down candles `downColor`. (The React original also supported SVG
|
|
143
|
+
* *fill patterns* for e-ink displays where color can't be relied on; Lynx has
|
|
144
|
+
* no SVG patterns, so that is dropped in favor of solid color.)
|
|
145
|
+
*/
|
|
146
|
+
@Component({
|
|
147
|
+
selector: 'ui-candlestick-series',
|
|
148
|
+
standalone: true,
|
|
149
|
+
imports: [LYNX_ELEMENTS],
|
|
150
|
+
encapsulation: ViewEncapsulation.None,
|
|
151
|
+
host: {
|
|
152
|
+
style:
|
|
153
|
+
'position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;',
|
|
154
|
+
'[class]': 'userClass()',
|
|
155
|
+
},
|
|
156
|
+
template: `
|
|
157
|
+
<!-- Wick first so the body paints on top of it (Lynx paint order follows
|
|
158
|
+
source order; z-index is unnecessary). The body is the tap target. -->
|
|
159
|
+
@for (candle of candles(); track $index) {
|
|
160
|
+
<view [style]="candle.wickStyle"></view>
|
|
161
|
+
<view
|
|
162
|
+
[style]="candle.bodyStyle"
|
|
163
|
+
(bindtap)="onCandleTap(candle.point)"
|
|
164
|
+
></view>
|
|
165
|
+
}
|
|
166
|
+
`,
|
|
167
|
+
})
|
|
168
|
+
export class UiCandlestickSeries {
|
|
169
|
+
readonly #chart = inject(UiCartesianChart);
|
|
170
|
+
|
|
171
|
+
readonly data = input.required<CandlestickPoint[]>();
|
|
172
|
+
/** Fill for rising candles (close ≥ open). Fixed data hue, not theme chrome. */
|
|
173
|
+
readonly upColor = input('rgba(34, 197, 94, 1)');
|
|
174
|
+
/** Fill for falling candles (close < open). */
|
|
175
|
+
readonly downColor = input('rgba(239, 68, 68, 1)');
|
|
176
|
+
/** Width of the high–low wick in px. */
|
|
177
|
+
readonly wickWidth = input(1);
|
|
178
|
+
/** Body width as a fraction of the spacing between adjacent candles. 0.6
|
|
179
|
+
* leaves a ~40% gap between bodies, the conventional candlestick look. */
|
|
180
|
+
readonly bodyWidthRatio = input(0.6);
|
|
181
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
182
|
+
|
|
183
|
+
readonly candleTap = output<CandlestickPoint>();
|
|
184
|
+
|
|
185
|
+
// Project each OHLC point into plot-local pixel space via the shared scales.
|
|
186
|
+
// Reading the chart's scale signals here makes this recompute whenever the
|
|
187
|
+
// domain or plot size changes.
|
|
188
|
+
readonly #projected = computed(() => {
|
|
189
|
+
const scaleX = this.#chart.xScale();
|
|
190
|
+
const scaleY = this.#chart.yScale();
|
|
191
|
+
return this.data().map((point) => ({
|
|
192
|
+
cx: scaleX(point.x),
|
|
193
|
+
openY: scaleY(point.open),
|
|
194
|
+
highY: scaleY(point.high),
|
|
195
|
+
lowY: scaleY(point.low),
|
|
196
|
+
closeY: scaleY(point.close),
|
|
197
|
+
up: point.close >= point.open,
|
|
198
|
+
point,
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Body width = a fraction of the pixel spacing between candle centers, so
|
|
203
|
+
// bodies scale with the plot and never overlap. With one candle there is no
|
|
204
|
+
// spacing to measure, so fall back to a fixed width.
|
|
205
|
+
readonly #bodyWidth = computed(() => {
|
|
206
|
+
const projected = this.#projected();
|
|
207
|
+
const n = projected.length;
|
|
208
|
+
if (n < 2) return SINGLE_CANDLE_BODY_WIDTH;
|
|
209
|
+
const span = Math.abs(projected[n - 1].cx - projected[0].cx);
|
|
210
|
+
return (span / (n - 1)) * this.bodyWidthRatio();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
protected readonly candles = computed(() => {
|
|
214
|
+
const projected = this.#projected();
|
|
215
|
+
const up = this.upColor();
|
|
216
|
+
const down = this.downColor();
|
|
217
|
+
// computeCandles works purely from the projected pixel fields; zip its output
|
|
218
|
+
// back with each source point so the body's tap can emit the original data.
|
|
219
|
+
return computeCandles(projected, this.#bodyWidth(), this.wickWidth()).map(
|
|
220
|
+
(shape, i) => {
|
|
221
|
+
const fill = shape.up ? up : down;
|
|
222
|
+
return {
|
|
223
|
+
point: projected[i].point,
|
|
224
|
+
wickStyle:
|
|
225
|
+
`position: absolute; left: ${px(shape.wick.left)}; top: ${px(shape.wick.top)}; ` +
|
|
226
|
+
`width: ${px(shape.wick.width)}; height: ${px(shape.wick.height)}; ` +
|
|
227
|
+
`background-color: ${fill};`,
|
|
228
|
+
bodyStyle:
|
|
229
|
+
`position: absolute; left: ${px(shape.body.left)}; top: ${px(shape.body.top)}; ` +
|
|
230
|
+
`width: ${px(shape.body.width)}; height: ${px(shape.body.height)}; ` +
|
|
231
|
+
`background-color: ${fill};`,
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
protected onCandleTap(point: CandlestickPoint): void {
|
|
238
|
+
this.candleTap.emit(point);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A ready-to-use candlestick (OHLC) chart for the common single-series case.
|
|
244
|
+
* Composes a `<ui-cartesian-chart>` with one `<ui-candlestick-series>` and
|
|
245
|
+
* auto-derives rounded axis domains + ticks from the data. For a moving-average
|
|
246
|
+
* line on top, or full control over the axes, use `<ui-cartesian-chart>` with a
|
|
247
|
+
* `<ui-candlestick-series>` (and optionally a `<ui-line-series>`) directly.
|
|
248
|
+
*/
|
|
249
|
+
@Component({
|
|
250
|
+
selector: 'ui-candlestick-chart',
|
|
251
|
+
standalone: true,
|
|
252
|
+
imports: [LYNX_ELEMENTS, UiCartesianChart, UiCandlestickSeries],
|
|
253
|
+
encapsulation: ViewEncapsulation.None,
|
|
254
|
+
template: `
|
|
255
|
+
<ui-cartesian-chart
|
|
256
|
+
[xDomain]="xAxis().domain"
|
|
257
|
+
[yDomain]="yAxis().domain"
|
|
258
|
+
[xTicks]="xAxis().ticks"
|
|
259
|
+
[yTicks]="yAxis().ticks"
|
|
260
|
+
[width]="width()"
|
|
261
|
+
[height]="height()"
|
|
262
|
+
[tickCount]="tickCount()"
|
|
263
|
+
[showXGrid]="showXGrid()"
|
|
264
|
+
[xAxisLabel]="xAxisLabel()"
|
|
265
|
+
[yAxisLabel]="yAxisLabel()"
|
|
266
|
+
[padding]="effectivePadding()"
|
|
267
|
+
[xTickFormat]="xTickFormat()"
|
|
268
|
+
[yTickFormat]="yTickFormat()"
|
|
269
|
+
[zoomable]="zoomable()"
|
|
270
|
+
[zoomAxes]="zoomAxes()"
|
|
271
|
+
[showZoomControls]="showZoomControls()"
|
|
272
|
+
[minZoom]="minZoom()"
|
|
273
|
+
[maxZoom]="maxZoom()"
|
|
274
|
+
[zoomStep]="zoomStep()"
|
|
275
|
+
[class]="userClass()"
|
|
276
|
+
>
|
|
277
|
+
<ui-candlestick-series
|
|
278
|
+
[data]="data()"
|
|
279
|
+
[upColor]="upColor()"
|
|
280
|
+
[downColor]="downColor()"
|
|
281
|
+
[wickWidth]="wickWidth()"
|
|
282
|
+
[bodyWidthRatio]="bodyWidthRatio()"
|
|
283
|
+
(candleTap)="candleTap.emit($event)"
|
|
284
|
+
/>
|
|
285
|
+
</ui-cartesian-chart>
|
|
286
|
+
`,
|
|
287
|
+
})
|
|
288
|
+
export class UiCandlestickChart {
|
|
289
|
+
readonly data = input.required<CandlestickPoint[]>();
|
|
290
|
+
readonly upColor = input('rgba(34, 197, 94, 1)');
|
|
291
|
+
readonly downColor = input('rgba(239, 68, 68, 1)');
|
|
292
|
+
readonly width = input(320);
|
|
293
|
+
readonly height = input(200);
|
|
294
|
+
readonly tickCount = input(5);
|
|
295
|
+
readonly wickWidth = input(1);
|
|
296
|
+
readonly bodyWidthRatio = input(0.6);
|
|
297
|
+
/** Draw vertical gridlines at each x tick. */
|
|
298
|
+
readonly showXGrid = input(false);
|
|
299
|
+
/** Title for the x-axis (centered below the tick labels). */
|
|
300
|
+
readonly xAxisLabel = input<string>('');
|
|
301
|
+
/** Title for the y-axis (rotated in the left gutter). */
|
|
302
|
+
readonly yAxisLabel = input<string>('');
|
|
303
|
+
/** Inner inset of the plot area — merged over the auto half-candle padding
|
|
304
|
+
* (below), so any side you set wins. See {@link ChartPadding}. */
|
|
305
|
+
readonly padding = input<ChartPadding>({});
|
|
306
|
+
/** Force the x-axis lower/upper bound instead of deriving it from the data.
|
|
307
|
+
* Omitted bounds fall back to the data extremes. */
|
|
308
|
+
readonly xMin = input<number | undefined>(undefined);
|
|
309
|
+
readonly xMax = input<number | undefined>(undefined);
|
|
310
|
+
/** Force the y-axis lower/upper bound (e.g. to compare two charts on one scale). */
|
|
311
|
+
readonly yMin = input<number | undefined>(undefined);
|
|
312
|
+
readonly yMax = input<number | undefined>(undefined);
|
|
313
|
+
readonly xTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
314
|
+
readonly yTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
315
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
316
|
+
|
|
317
|
+
// Pan/zoom — forwarded to the underlying <ui-cartesian-chart>. Off by default
|
|
318
|
+
// so a candlestick chart stays static unless opted in.
|
|
319
|
+
readonly zoomable = input(false);
|
|
320
|
+
readonly zoomAxes = input<'x' | 'y' | 'xy'>('xy');
|
|
321
|
+
readonly showZoomControls = input(true);
|
|
322
|
+
readonly minZoom = input(1);
|
|
323
|
+
readonly maxZoom = input(8);
|
|
324
|
+
readonly zoomStep = input(1.4);
|
|
325
|
+
|
|
326
|
+
readonly candleTap = output<CandlestickPoint>();
|
|
327
|
+
|
|
328
|
+
// Auto-compute rounded axes from the data extremes, letting an explicit
|
|
329
|
+
// min/max override either end. `Math.min/max(...[])` yield ±Infinity for empty
|
|
330
|
+
// data, which `niceScale` handles by padding.
|
|
331
|
+
protected readonly xAxis = computed(() => {
|
|
332
|
+
const xs = this.data().map((p) => p.x);
|
|
333
|
+
const lo = this.xMin() ?? Math.min(...xs);
|
|
334
|
+
const hi = this.xMax() ?? Math.max(...xs);
|
|
335
|
+
return niceScale(lo, hi, this.tickCount());
|
|
336
|
+
});
|
|
337
|
+
// The y-extent spans every wick: the lowest low to the highest high. Open and
|
|
338
|
+
// close always sit inside that range, so they need no separate consideration.
|
|
339
|
+
protected readonly yAxis = computed(() => {
|
|
340
|
+
const lows = this.data().map((p) => p.low);
|
|
341
|
+
const highs = this.data().map((p) => p.high);
|
|
342
|
+
const lo = this.yMin() ?? Math.min(...lows);
|
|
343
|
+
const hi = this.yMax() ?? Math.max(...highs);
|
|
344
|
+
return niceScale(lo, hi, this.tickCount());
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// A candle body is centered on its x, so the first and last bodies would spill
|
|
348
|
+
// over the plot edges. Insetting the scales by half a candle's spacing keeps
|
|
349
|
+
// them fully inside. User `padding` is spread last, so an explicit side wins.
|
|
350
|
+
protected readonly effectivePadding = computed<ChartPadding>(() => {
|
|
351
|
+
const n = this.data().length;
|
|
352
|
+
const half = n > 1 ? 0.5 / (n - 1) : 0;
|
|
353
|
+
return { left: half, right: half, ...this.padding() };
|
|
354
|
+
});
|
|
355
|
+
}
|