@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,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
effect,
|
|
7
|
+
input,
|
|
8
|
+
viewChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
11
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
12
|
+
|
|
13
|
+
import { popIn } from '../../utils/animate';
|
|
14
|
+
import { cn } from '../../utils/cn';
|
|
15
|
+
|
|
16
|
+
export const badgeVariants = cva(
|
|
17
|
+
// `self-start` keeps the badge hugging its content. Lynx views default to `linear`
|
|
18
|
+
// layout, where a child with no explicit width stretches to fill the parent's cross
|
|
19
|
+
// axis — so without it a badge renders full-width. (Web shadcn uses `inline-flex` for
|
|
20
|
+
// the same shrink-to-fit; Lynx has no inline display, so align-self is the lever.)
|
|
21
|
+
'items-center self-start rounded-full px-2.5 py-0.5 flex',
|
|
22
|
+
{
|
|
23
|
+
variants: {
|
|
24
|
+
variant: {
|
|
25
|
+
default: 'bg-primary',
|
|
26
|
+
secondary: 'bg-secondary',
|
|
27
|
+
destructive: 'bg-destructive',
|
|
28
|
+
outline: 'border border-border bg-transparent',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
variant: 'default',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const badgeTextVariants = cva('text-xs font-semibold', {
|
|
38
|
+
variants: {
|
|
39
|
+
variant: {
|
|
40
|
+
default: 'text-primary-foreground',
|
|
41
|
+
secondary: 'text-secondary-foreground',
|
|
42
|
+
destructive: 'text-destructive-foreground',
|
|
43
|
+
outline: 'text-foreground',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
defaultVariants: {
|
|
47
|
+
variant: 'default',
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type BadgeVariant = NonNullable<
|
|
52
|
+
VariantProps<typeof badgeVariants>['variant']
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
@Component({
|
|
56
|
+
selector: 'ui-badge',
|
|
57
|
+
standalone: true,
|
|
58
|
+
imports: [LYNX_ELEMENTS],
|
|
59
|
+
encapsulation: ViewEncapsulation.None,
|
|
60
|
+
template: `
|
|
61
|
+
<view #container [class]="containerClass()">
|
|
62
|
+
<text [class]="labelClass()"><ng-content /></text>
|
|
63
|
+
</view>
|
|
64
|
+
`,
|
|
65
|
+
})
|
|
66
|
+
export class UiBadge {
|
|
67
|
+
readonly variant = input<BadgeVariant>('default');
|
|
68
|
+
/**
|
|
69
|
+
* When true, badge pops in with a spring animation on first render
|
|
70
|
+
*/
|
|
71
|
+
readonly animated = input(true);
|
|
72
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
73
|
+
|
|
74
|
+
readonly containerRef = viewChild<ElementRef>('container');
|
|
75
|
+
|
|
76
|
+
constructor() {
|
|
77
|
+
effect(() => {
|
|
78
|
+
const el = this.containerRef()?.nativeElement;
|
|
79
|
+
// popIn calls el.animate() which is inert in jsdom (the offscreen web
|
|
80
|
+
// renderer path — see animate.ts's transitionTransform comment). The
|
|
81
|
+
// effect's rendering-adjacent work is not exercisable from a JIT test,
|
|
82
|
+
// so ignore the animation branch for coverage.
|
|
83
|
+
/* v8 ignore next 3 */
|
|
84
|
+
if (el && this.animated()) {
|
|
85
|
+
popIn(el, { duration: 200 });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected readonly containerClass = computed(() =>
|
|
91
|
+
cn(badgeVariants({ variant: this.variant() }), this.userClass()),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
protected readonly labelClass = computed(() =>
|
|
95
|
+
cn(badgeTextVariants({ variant: this.variant() })),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiBadge, badgeVariants, type BadgeVariant } from './badge';
|
|
@@ -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 { UiBarChart, UiBarSeries } from './bar-chart';
|
|
6
|
+
|
|
7
|
+
describe('UiBarChart', () => {
|
|
8
|
+
it('renders with default configuration', async () => {
|
|
9
|
+
const { container } = await render(UiBarChart);
|
|
10
|
+
expect(container).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('folds the baseline into the y-axis domain', async () => {
|
|
14
|
+
const { componentRef } = await render(UiBarChart);
|
|
15
|
+
const inst = componentRef.instance as UiBarChart;
|
|
16
|
+
setInputSignal(inst.data, [
|
|
17
|
+
{ x: 0, y: 5 },
|
|
18
|
+
{ x: 1, y: 10 },
|
|
19
|
+
{ x: 2, y: 8 },
|
|
20
|
+
]);
|
|
21
|
+
setInputSignal(inst.baseline, 0);
|
|
22
|
+
await waitForUpdate();
|
|
23
|
+
const cast = inst as unknown as {
|
|
24
|
+
xAxis: () => { domain: readonly [number, number] };
|
|
25
|
+
yAxis: () => { domain: readonly [number, number] };
|
|
26
|
+
effectivePadding: () => { left?: number; right?: number };
|
|
27
|
+
};
|
|
28
|
+
expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(0);
|
|
29
|
+
expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(10);
|
|
30
|
+
// 3 bars → half-bar inset = 0.5 / (3-1) = 0.25
|
|
31
|
+
expect(cast.effectivePadding().left).toBeCloseTo(0.25);
|
|
32
|
+
expect(cast.effectivePadding().right).toBeCloseTo(0.25);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('honors explicit xMin / xMax / yMin / yMax overrides', async () => {
|
|
36
|
+
const { componentRef } = await render(UiBarChart);
|
|
37
|
+
const inst = componentRef.instance as UiBarChart;
|
|
38
|
+
setInputSignal(inst.data, [
|
|
39
|
+
{ x: 0, y: 0 },
|
|
40
|
+
{ x: 10, y: 10 },
|
|
41
|
+
]);
|
|
42
|
+
setInputSignal(inst.xMin, -5);
|
|
43
|
+
setInputSignal(inst.xMax, 20);
|
|
44
|
+
setInputSignal(inst.yMin, -1);
|
|
45
|
+
setInputSignal(inst.yMax, 15);
|
|
46
|
+
await waitForUpdate();
|
|
47
|
+
const cast = inst as unknown as {
|
|
48
|
+
xAxis: () => { domain: readonly [number, number] };
|
|
49
|
+
yAxis: () => { domain: readonly [number, number] };
|
|
50
|
+
};
|
|
51
|
+
expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(-5);
|
|
52
|
+
expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(20);
|
|
53
|
+
expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(-1);
|
|
54
|
+
expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(15);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('single-datum series drops the half-bar padding', async () => {
|
|
58
|
+
const { componentRef } = await render(UiBarChart);
|
|
59
|
+
const inst = componentRef.instance as UiBarChart;
|
|
60
|
+
setInputSignal(inst.data, [{ x: 0, y: 5 }]);
|
|
61
|
+
await waitForUpdate();
|
|
62
|
+
const cast = inst as unknown as {
|
|
63
|
+
effectivePadding: () => { left?: number; right?: number };
|
|
64
|
+
};
|
|
65
|
+
// n === 1 → half === 0
|
|
66
|
+
expect(cast.effectivePadding().left).toBe(0);
|
|
67
|
+
expect(cast.effectivePadding().right).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('exposes the default tick formatter used by the child cartesian chart', async () => {
|
|
71
|
+
const { componentRef } = await render(UiBarChart);
|
|
72
|
+
const inst = componentRef.instance as UiBarChart;
|
|
73
|
+
expect(inst.xTickFormat()(3.14159)).toBe('3.14');
|
|
74
|
+
expect(inst.yTickFormat()(1.005)).toBe('1');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('user padding overrides the auto half-bar inset per side', async () => {
|
|
78
|
+
const { componentRef } = await render(UiBarChart);
|
|
79
|
+
const inst = componentRef.instance as UiBarChart;
|
|
80
|
+
setInputSignal(inst.data, [
|
|
81
|
+
{ x: 0, y: 5 },
|
|
82
|
+
{ x: 1, y: 10 },
|
|
83
|
+
]);
|
|
84
|
+
setInputSignal(inst.padding, { left: 0.1 });
|
|
85
|
+
await waitForUpdate();
|
|
86
|
+
const cast = inst as unknown as {
|
|
87
|
+
effectivePadding: () => { left?: number; right?: number };
|
|
88
|
+
};
|
|
89
|
+
// left explicitly set → wins; right still auto = 0.5
|
|
90
|
+
expect(cast.effectivePadding().left).toBeCloseTo(0.1);
|
|
91
|
+
expect(cast.effectivePadding().right).toBeCloseTo(0.5);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('UiBarSeries', () => {
|
|
96
|
+
const renderSeries = async () => {
|
|
97
|
+
const parentResult = await render(UiCartesianChart);
|
|
98
|
+
const parent = parentResult.componentRef.instance as UiCartesianChart;
|
|
99
|
+
setInputSignal(parent.xDomain, [0, 10]);
|
|
100
|
+
setInputSignal(parent.yDomain, [0, 10]);
|
|
101
|
+
await waitForUpdate();
|
|
102
|
+
const seriesResult = await render(UiBarSeries, {
|
|
103
|
+
providers: [{ provide: UiCartesianChart, useValue: parent }],
|
|
104
|
+
});
|
|
105
|
+
const series = seriesResult.componentRef.instance as UiBarSeries;
|
|
106
|
+
setInputSignal(series.data, [
|
|
107
|
+
{ x: 0, y: 2 },
|
|
108
|
+
{ x: 5, y: 8 },
|
|
109
|
+
{ x: 10, y: 5 },
|
|
110
|
+
]);
|
|
111
|
+
await waitForUpdate();
|
|
112
|
+
return { parent, series };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
it('renders a rectangle per datum with the fallback color', async () => {
|
|
116
|
+
const { series } = await renderSeries();
|
|
117
|
+
const cast = series as unknown as {
|
|
118
|
+
bars: () => { style: string; point: { x: number; y: number } }[];
|
|
119
|
+
};
|
|
120
|
+
const bars = cast.bars();
|
|
121
|
+
expect(bars.length).toBe(3);
|
|
122
|
+
for (const b of bars) {
|
|
123
|
+
expect(b.style).toContain('background-color: var(--primary)');
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('cycles through the per-bar palette when colors are provided', async () => {
|
|
128
|
+
const { series } = await renderSeries();
|
|
129
|
+
setInputSignal(series.colors, ['#f00', '#0f0']);
|
|
130
|
+
await waitForUpdate();
|
|
131
|
+
const cast = series as unknown as { bars: () => { style: string }[] };
|
|
132
|
+
const bars = cast.bars();
|
|
133
|
+
expect(bars[0].style).toContain('#f00');
|
|
134
|
+
expect(bars[1].style).toContain('#0f0');
|
|
135
|
+
// wraps around
|
|
136
|
+
expect(bars[2].style).toContain('#f00');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('emits barTap with the source point when a bar is tapped', async () => {
|
|
140
|
+
const { series } = await renderSeries();
|
|
141
|
+
const cb = vi.fn();
|
|
142
|
+
series.barTap.subscribe(cb);
|
|
143
|
+
(
|
|
144
|
+
series as unknown as { onBarTap: (p: { x: number; y: number }) => void }
|
|
145
|
+
).onBarTap({ x: 1, y: 2 });
|
|
146
|
+
expect(cb).toHaveBeenCalledWith({ x: 1, y: 2 });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('single-datum series uses the fallback bar width', async () => {
|
|
150
|
+
const parentResult = await render(UiCartesianChart);
|
|
151
|
+
const parent = parentResult.componentRef.instance as UiCartesianChart;
|
|
152
|
+
setInputSignal(parent.xDomain, [0, 10]);
|
|
153
|
+
setInputSignal(parent.yDomain, [0, 10]);
|
|
154
|
+
await waitForUpdate();
|
|
155
|
+
const seriesResult = await render(UiBarSeries, {
|
|
156
|
+
providers: [{ provide: UiCartesianChart, useValue: parent }],
|
|
157
|
+
});
|
|
158
|
+
const series = seriesResult.componentRef.instance as UiBarSeries;
|
|
159
|
+
setInputSignal(series.data, [{ x: 5, y: 5 }]);
|
|
160
|
+
await waitForUpdate();
|
|
161
|
+
const cast = series as unknown as { bars: () => { style: string }[] };
|
|
162
|
+
const [bar] = cast.bars();
|
|
163
|
+
// fallback = 24px
|
|
164
|
+
expect(bar.style).toContain('width: 24.00px');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('clamps the baseline into the plot when it lies outside the domain', async () => {
|
|
168
|
+
const { series } = await renderSeries();
|
|
169
|
+
setInputSignal(series.baseline, 999);
|
|
170
|
+
await waitForUpdate();
|
|
171
|
+
const cast = series as unknown as { bars: () => { style: string }[] };
|
|
172
|
+
expect(cast.bars().length).toBe(3);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
// `computeBars` 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 { computeBars } = await import('./bar-chart');
|
|
25
|
+
|
|
26
|
+
// Screen y grows downward, so a value ABOVE the baseline has a SMALLER pixel y
|
|
27
|
+
// than baseY. Baseline pixel y = 100 in these cases (bottom of the plot).
|
|
28
|
+
describe('computeBars', () => {
|
|
29
|
+
it('draws a positive bar from the baseline up to the value, centered on cx', () => {
|
|
30
|
+
// value projects to y=30 (above the baseline at y=100).
|
|
31
|
+
const [bar] = computeBars([{ cx: 50, valueY: 30 }], 100, 12);
|
|
32
|
+
expect(bar.top).toBe(30); // min(valueY, baseY) — the higher edge
|
|
33
|
+
expect(bar.height).toBe(70); // |valueY - baseY|
|
|
34
|
+
expect(bar.width).toBe(12);
|
|
35
|
+
expect(bar.left).toBe(50 - 6); // cx - barWidth/2
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('draws a negative bar growing downward from the baseline', () => {
|
|
39
|
+
// value projects BELOW the baseline, so valueY (140) > baseY (100).
|
|
40
|
+
const [bar] = computeBars([{ cx: 20, valueY: 140 }], 100, 10);
|
|
41
|
+
expect(bar.top).toBe(100); // min(valueY, baseY) = baseY here
|
|
42
|
+
expect(bar.height).toBe(40); // grows down to the value
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('draws nothing (zero height) for a value exactly on the baseline', () => {
|
|
46
|
+
// A bar's height is its datum: on the baseline there is nothing to draw, so
|
|
47
|
+
// the default minBarHeight of 0 leaves it invisible rather than a sliver.
|
|
48
|
+
const [bar] = computeBars([{ cx: 0, valueY: 100 }], 100, 12);
|
|
49
|
+
expect(bar.height).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('honors a custom minimum bar height', () => {
|
|
53
|
+
const [bar] = computeBars([{ cx: 0, valueY: 100 }], 100, 12, 3);
|
|
54
|
+
expect(bar.height).toBe(3);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('centers every bar on its own cx', () => {
|
|
58
|
+
const bars = computeBars(
|
|
59
|
+
[
|
|
60
|
+
{ cx: 10, valueY: 40 },
|
|
61
|
+
{ cx: 60, valueY: 20 },
|
|
62
|
+
],
|
|
63
|
+
100,
|
|
64
|
+
8,
|
|
65
|
+
);
|
|
66
|
+
expect(bars[0].left).toBe(10 - 4);
|
|
67
|
+
expect(bars[1].left).toBe(60 - 4);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns an empty array for no bars', () => {
|
|
71
|
+
expect(computeBars([], 100, 12)).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
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
|
+
type ChartPoint,
|
|
14
|
+
UiCartesianChart,
|
|
15
|
+
niceScale,
|
|
16
|
+
} from '../cartesian-chart/cartesian-chart';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
// A bar is just an (x, y) datum — x is the category position, y the bar's value —
|
|
23
|
+
// so it reuses the shared `ChartPoint` rather than defining its own. That also
|
|
24
|
+
// lets a bar series and a line/area series share one dataset on the same axes.
|
|
25
|
+
|
|
26
|
+
/** One bar after projection into plot-local pixel space. */
|
|
27
|
+
type ProjectedBar = {
|
|
28
|
+
/** Center x of the bar, in px. */
|
|
29
|
+
cx: number;
|
|
30
|
+
/** Pixel y of the bar's value (its far edge from the baseline). */
|
|
31
|
+
valueY: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** An axis-aligned rectangle in plot-local pixel space. */
|
|
35
|
+
export type BarShape = {
|
|
36
|
+
left: number;
|
|
37
|
+
top: number;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Pure geometry (exported so it can be unit-tested without Angular)
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Turns projected bars — already in plot-local pixel space — into rectangles
|
|
48
|
+
* ready to render as `<view>`s. A bar is the simplest chart mark on Lynx: one
|
|
49
|
+
* axis-aligned box from the shared `baseY` (the baseline's pixel y) to the
|
|
50
|
+
* value, so this is a straight geometric mapping with no rotation or fill
|
|
51
|
+
* rasterization (unlike the line and area series).
|
|
52
|
+
*
|
|
53
|
+
* Screen y grows *downward*, so a value above the baseline has a *smaller*
|
|
54
|
+
* `valueY` than `baseY`. Taking `top = min(valueY, baseY)` therefore anchors the
|
|
55
|
+
* rectangle at whichever edge is visually higher, which makes bars for negative
|
|
56
|
+
* values (value below the baseline) grow *downward* from the baseline correctly.
|
|
57
|
+
*
|
|
58
|
+
* Unlike a candlestick doji, `minBarHeight` defaults to `0`: a bar's height *is*
|
|
59
|
+
* its datum, so a value exactly on the baseline should draw nothing rather than
|
|
60
|
+
* a misleading sliver. Callers wanting every bar guaranteed visible can raise it.
|
|
61
|
+
*
|
|
62
|
+
* @param baseY pixel y of the baseline every bar grows from.
|
|
63
|
+
* @param barWidth width of each bar in px.
|
|
64
|
+
* @param minBarHeight floor for the bar height (0 = honest zero-height at baseline).
|
|
65
|
+
*/
|
|
66
|
+
export const computeBars = (
|
|
67
|
+
bars: readonly ProjectedBar[],
|
|
68
|
+
baseY: number,
|
|
69
|
+
barWidth: number,
|
|
70
|
+
minBarHeight = 0,
|
|
71
|
+
): BarShape[] =>
|
|
72
|
+
bars.map((b) => {
|
|
73
|
+
const top = Math.min(b.valueY, baseY);
|
|
74
|
+
const height = Math.max(Math.abs(b.valueY - baseY), minBarHeight);
|
|
75
|
+
return { left: b.cx - barWidth / 2, top, width: barWidth, height };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Rendering helpers (mirror the equivalents in line-chart.ts / area-chart.ts —
|
|
80
|
+
// kept local so this module stays self-contained when scaffolded by `dolan add`)
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Formats a pixel value for an inline style, avoiding scientific notation.
|
|
85
|
+
*/
|
|
86
|
+
const px = (n: number): string => `${n.toFixed(2)}px`;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Default axis label formatter — plain JS only (no `Intl`, absent in Lynx).
|
|
90
|
+
*/
|
|
91
|
+
const defaultTickFormat = (value: number): string =>
|
|
92
|
+
String(Math.round(value * 100) / 100);
|
|
93
|
+
|
|
94
|
+
const clamp = (value: number, lo: number, hi: number): number =>
|
|
95
|
+
Math.max(lo, Math.min(hi, value));
|
|
96
|
+
|
|
97
|
+
// Fallback bar width (px) when there is only a single bar, so there is no
|
|
98
|
+
// neighbor to derive a spacing from. Matches the candlestick series' fallback.
|
|
99
|
+
const SINGLE_BAR_WIDTH = 24;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A single bar series drawn inside a `<ui-cartesian-chart>`. Reads the shared
|
|
103
|
+
* scales from the parent chart via DI and renders one `<view>` rectangle per
|
|
104
|
+
* datum, from the shared `baseline` up (or down) to the value.
|
|
105
|
+
*
|
|
106
|
+
* ## Why the host is `position: absolute; width/height: 100%`
|
|
107
|
+
* Identical to `UiLineSeries`: Lynx resolves an absolute child against its
|
|
108
|
+
* **direct parent** (there is no `position: static`), so the host itself must be
|
|
109
|
+
* the plot-sized containing block. Every bar is a direct child of it.
|
|
110
|
+
*
|
|
111
|
+
* ## Per-bar colors
|
|
112
|
+
* `color` fills every bar by default. Pass `colors` to give each bar its own
|
|
113
|
+
* hue (a categorical bar chart) — the palette cycles if it is shorter than the
|
|
114
|
+
* data, so a handful of colors tint any number of bars.
|
|
115
|
+
*/
|
|
116
|
+
@Component({
|
|
117
|
+
selector: 'ui-bar-series',
|
|
118
|
+
standalone: true,
|
|
119
|
+
imports: [LYNX_ELEMENTS],
|
|
120
|
+
encapsulation: ViewEncapsulation.None,
|
|
121
|
+
host: {
|
|
122
|
+
style:
|
|
123
|
+
'position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;',
|
|
124
|
+
'[class]': 'userClass()',
|
|
125
|
+
},
|
|
126
|
+
template: `
|
|
127
|
+
@for (bar of bars(); track $index) {
|
|
128
|
+
<view [style]="bar.style" (bindtap)="onBarTap(bar.point)"></view>
|
|
129
|
+
}
|
|
130
|
+
`,
|
|
131
|
+
})
|
|
132
|
+
export class UiBarSeries {
|
|
133
|
+
readonly #chart = inject(UiCartesianChart);
|
|
134
|
+
|
|
135
|
+
readonly data = input.required<ChartPoint[]>();
|
|
136
|
+
/** Fill for every bar. Overridden per bar by `colors` when that is set. */
|
|
137
|
+
readonly color = input('var(--primary)');
|
|
138
|
+
/** Per-bar fills (a categorical palette). Cycles when shorter than the data;
|
|
139
|
+
* an empty array falls back to `color`. */
|
|
140
|
+
readonly colors = input<string[]>([]);
|
|
141
|
+
/** Bar width as a fraction of the spacing between adjacent bars. 0.7 leaves a
|
|
142
|
+
* ~30% gap between bars, the conventional bar-chart look. */
|
|
143
|
+
readonly barWidthRatio = input(0.7);
|
|
144
|
+
/** Data-space y the bars grow from (usually 0). Values below it grow downward. */
|
|
145
|
+
readonly baseline = input(0);
|
|
146
|
+
/** Corner radius of each bar in px (0 = square corners). */
|
|
147
|
+
readonly radius = input(0);
|
|
148
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
149
|
+
|
|
150
|
+
readonly barTap = output<ChartPoint>();
|
|
151
|
+
|
|
152
|
+
// Project each datum into plot-local pixel space via the shared scales.
|
|
153
|
+
// Reading the chart's scale signals here makes this recompute whenever the
|
|
154
|
+
// domain or plot size changes.
|
|
155
|
+
readonly #projected = computed(() => {
|
|
156
|
+
const scaleX = this.#chart.xScale();
|
|
157
|
+
const scaleY = this.#chart.yScale();
|
|
158
|
+
return this.data().map((point) => ({
|
|
159
|
+
cx: scaleX(point.x),
|
|
160
|
+
valueY: scaleY(point.y),
|
|
161
|
+
point,
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Bar width = a fraction of the pixel spacing between bar centers, so bars
|
|
166
|
+
// scale with the plot and never overlap. With one bar there is no spacing to
|
|
167
|
+
// measure, so fall back to a fixed width.
|
|
168
|
+
readonly #barWidth = computed(() => {
|
|
169
|
+
const projected = this.#projected();
|
|
170
|
+
const n = projected.length;
|
|
171
|
+
if (n < 2) return SINGLE_BAR_WIDTH;
|
|
172
|
+
const span = Math.abs(projected[n - 1].cx - projected[0].cx);
|
|
173
|
+
return (span / (n - 1)) * this.barWidthRatio();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
protected readonly bars = computed(() => {
|
|
177
|
+
const projected = this.#projected();
|
|
178
|
+
const fallback = this.color();
|
|
179
|
+
const palette = this.colors();
|
|
180
|
+
const radius = this.radius();
|
|
181
|
+
// Clamp the baseline into the plot so a baseline outside the domain anchors
|
|
182
|
+
// the bars to the nearest edge (top/bottom) instead of drawing off-plot.
|
|
183
|
+
const baseY = clamp(
|
|
184
|
+
this.#chart.yScale()(this.baseline()),
|
|
185
|
+
0,
|
|
186
|
+
this.#chart.plotHeight(),
|
|
187
|
+
);
|
|
188
|
+
// computeBars works purely from the projected pixel fields; zip its output
|
|
189
|
+
// back with each source point so a bar's tap can emit the original datum.
|
|
190
|
+
return computeBars(projected, baseY, this.#barWidth()).map((shape, i) => {
|
|
191
|
+
const fill = palette.length ? palette[i % palette.length] : fallback;
|
|
192
|
+
return {
|
|
193
|
+
point: projected[i].point,
|
|
194
|
+
style:
|
|
195
|
+
`position: absolute; left: ${px(shape.left)}; top: ${px(shape.top)}; ` +
|
|
196
|
+
`width: ${px(shape.width)}; height: ${px(shape.height)}; ` +
|
|
197
|
+
`background-color: ${fill}; border-radius: ${px(radius)};`,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
protected onBarTap(point: ChartPoint): void {
|
|
203
|
+
this.barTap.emit(point);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A ready-to-use bar chart for the common single-series case. Composes a
|
|
209
|
+
* `<ui-cartesian-chart>` with one `<ui-bar-series>` and auto-derives rounded
|
|
210
|
+
* axis domains + ticks from the data. For a line overlay, or full control over
|
|
211
|
+
* the axes, use `<ui-cartesian-chart>` with a `<ui-bar-series>` (and optionally
|
|
212
|
+
* a `<ui-line-series>`) directly.
|
|
213
|
+
*/
|
|
214
|
+
@Component({
|
|
215
|
+
selector: 'ui-bar-chart',
|
|
216
|
+
standalone: true,
|
|
217
|
+
imports: [LYNX_ELEMENTS, UiCartesianChart, UiBarSeries],
|
|
218
|
+
encapsulation: ViewEncapsulation.None,
|
|
219
|
+
template: `
|
|
220
|
+
<ui-cartesian-chart
|
|
221
|
+
[xDomain]="xAxis().domain"
|
|
222
|
+
[yDomain]="yAxis().domain"
|
|
223
|
+
[xTicks]="xAxis().ticks"
|
|
224
|
+
[yTicks]="yAxis().ticks"
|
|
225
|
+
[width]="width()"
|
|
226
|
+
[height]="height()"
|
|
227
|
+
[tickCount]="tickCount()"
|
|
228
|
+
[showXGrid]="showXGrid()"
|
|
229
|
+
[xAxisLabel]="xAxisLabel()"
|
|
230
|
+
[yAxisLabel]="yAxisLabel()"
|
|
231
|
+
[padding]="effectivePadding()"
|
|
232
|
+
[xTickFormat]="xTickFormat()"
|
|
233
|
+
[yTickFormat]="yTickFormat()"
|
|
234
|
+
[zoomable]="zoomable()"
|
|
235
|
+
[zoomAxes]="zoomAxes()"
|
|
236
|
+
[showZoomControls]="showZoomControls()"
|
|
237
|
+
[minZoom]="minZoom()"
|
|
238
|
+
[maxZoom]="maxZoom()"
|
|
239
|
+
[zoomStep]="zoomStep()"
|
|
240
|
+
[class]="userClass()"
|
|
241
|
+
>
|
|
242
|
+
<ui-bar-series
|
|
243
|
+
[data]="data()"
|
|
244
|
+
[color]="color()"
|
|
245
|
+
[colors]="colors()"
|
|
246
|
+
[barWidthRatio]="barWidthRatio()"
|
|
247
|
+
[baseline]="baseline()"
|
|
248
|
+
[radius]="radius()"
|
|
249
|
+
(barTap)="barTap.emit($event)"
|
|
250
|
+
/>
|
|
251
|
+
</ui-cartesian-chart>
|
|
252
|
+
`,
|
|
253
|
+
})
|
|
254
|
+
export class UiBarChart {
|
|
255
|
+
readonly data = input.required<ChartPoint[]>();
|
|
256
|
+
readonly color = input('var(--primary)');
|
|
257
|
+
readonly colors = input<string[]>([]);
|
|
258
|
+
readonly width = input(320);
|
|
259
|
+
readonly height = input(200);
|
|
260
|
+
readonly tickCount = input(5);
|
|
261
|
+
readonly barWidthRatio = input(0.7);
|
|
262
|
+
/** Data-space y the bars grow from (usually 0). Values below it grow downward. */
|
|
263
|
+
readonly baseline = input(0);
|
|
264
|
+
/** Corner radius of each bar in px. */
|
|
265
|
+
readonly radius = input(0);
|
|
266
|
+
/** Draw vertical gridlines at each x tick. */
|
|
267
|
+
readonly showXGrid = input(false);
|
|
268
|
+
/** Title for the x-axis (centered below the tick labels). */
|
|
269
|
+
readonly xAxisLabel = input<string>('');
|
|
270
|
+
/** Title for the y-axis (rotated in the left gutter). */
|
|
271
|
+
readonly yAxisLabel = input<string>('');
|
|
272
|
+
/** Inner inset of the plot area — merged over the auto half-bar padding
|
|
273
|
+
* (below), so any side you set wins. See {@link ChartPadding}. */
|
|
274
|
+
readonly padding = input<ChartPadding>({});
|
|
275
|
+
/** Force the x-axis lower/upper bound instead of deriving it from the data.
|
|
276
|
+
* Omitted bounds fall back to the data extremes. */
|
|
277
|
+
readonly xMin = input<number | undefined>(undefined);
|
|
278
|
+
readonly xMax = input<number | undefined>(undefined);
|
|
279
|
+
/** Force the y-axis lower/upper bound. The `baseline` is still folded in, so
|
|
280
|
+
* `yMin` only lowers the floor further — it can't clip a bar's base off. */
|
|
281
|
+
readonly yMin = input<number | undefined>(undefined);
|
|
282
|
+
readonly yMax = input<number | undefined>(undefined);
|
|
283
|
+
readonly xTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
284
|
+
readonly yTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
285
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
286
|
+
|
|
287
|
+
// Pan/zoom — forwarded to the underlying <ui-cartesian-chart>. Off by default
|
|
288
|
+
// so a bar chart stays static (and scroll-view friendly) unless opted in.
|
|
289
|
+
readonly zoomable = input(false);
|
|
290
|
+
readonly zoomAxes = input<'x' | 'y' | 'xy'>('xy');
|
|
291
|
+
readonly showZoomControls = input(true);
|
|
292
|
+
readonly minZoom = input(1);
|
|
293
|
+
readonly maxZoom = input(8);
|
|
294
|
+
readonly zoomStep = input(1.4);
|
|
295
|
+
|
|
296
|
+
readonly barTap = output<ChartPoint>();
|
|
297
|
+
|
|
298
|
+
// Auto-compute rounded axes from the data extremes, letting an explicit
|
|
299
|
+
// min/max override either end. `Math.min/max(...[])` yield ±Infinity for empty
|
|
300
|
+
// data, which `niceScale` handles by padding.
|
|
301
|
+
protected readonly xAxis = computed(() => {
|
|
302
|
+
const xs = this.data().map((p) => p.x);
|
|
303
|
+
const lo = this.xMin() ?? Math.min(...xs);
|
|
304
|
+
const hi = this.xMax() ?? Math.max(...xs);
|
|
305
|
+
return niceScale(lo, hi, this.tickCount());
|
|
306
|
+
});
|
|
307
|
+
// Fold the baseline into the y-extent so the axis always spans the whole bar:
|
|
308
|
+
// a positive-only series still shows the baseline at the bottom, and negative
|
|
309
|
+
// values pull the domain below it. An explicit yMin/yMax overrides the
|
|
310
|
+
// corresponding data-derived end (the baseline still participates in the other).
|
|
311
|
+
protected readonly yAxis = computed(() => {
|
|
312
|
+
const ys = this.data().map((p) => p.y);
|
|
313
|
+
const base = this.baseline();
|
|
314
|
+
const lo = this.yMin() ?? Math.min(base, ...ys);
|
|
315
|
+
const hi = this.yMax() ?? Math.max(base, ...ys);
|
|
316
|
+
return niceScale(lo, hi, this.tickCount());
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// A bar is centered on its x, so the first and last bars would spill over the
|
|
320
|
+
// plot edges. Insetting the scales by half a bar's spacing keeps them fully
|
|
321
|
+
// inside. User `padding` is spread last, so an explicit side wins.
|
|
322
|
+
protected readonly effectivePadding = computed<ChartPadding>(() => {
|
|
323
|
+
const n = this.data().length;
|
|
324
|
+
const half = n > 1 ? 0.5 / (n - 1) : 0;
|
|
325
|
+
return { left: half, right: half, ...this.padding() };
|
|
326
|
+
});
|
|
327
|
+
}
|