@aceshooting/lyra-ui 1.0.0 → 1.1.0
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/README.md +8 -9
- package/custom-elements.json +10463 -4519
- package/dist/components/chart/chart.d.ts +46 -1
- package/dist/components/chart/chart.js +148 -10
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +74 -0
- package/dist/components/chart/lite-chart.js +384 -0
- package/dist/components/chart/lite-chart.stories.d.ts +12 -0
- package/dist/components/chart/lite-chart.stories.js +98 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +75 -12
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +48 -3
- package/dist/components/flag/flag.js +65 -29
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +137 -12
- package/dist/components/heatmap/heatmap.js +427 -27
- package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
- package/dist/components/heatmap/heatmap.stories.js +130 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +122 -0
- package/dist/components/select/select.js +516 -0
- package/dist/components/select/select.stories.d.ts +19 -0
- package/dist/components/select/select.stories.js +75 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +188 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +27 -1
- package/dist/components/stat/stat.js +56 -3
- package/dist/components/stat/stat.stories.d.ts +3 -0
- package/dist/components/stat/stat.stories.js +49 -1
- package/dist/components/stat/stat.styles.js +44 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +15 -1
- package/dist/components/widget/widget.js +140 -5
- package/dist/components/widget/widget.stories.d.ts +1 -0
- package/dist/components/widget/widget.stories.js +10 -0
- package/dist/components/widget/widget.styles.js +11 -1
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +54 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +892 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
|
@@ -22,3 +22,78 @@ export const Default = {
|
|
|
22
22
|
`;
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* `area` fills every series lacking its own per-series `fill` (unlike
|
|
27
|
+
* `Default`'s "Sessions" series, which opts into a fill via its own
|
|
28
|
+
* `fill: true` instead of the chart-wide default).
|
|
29
|
+
*/
|
|
30
|
+
export const AreaFill = {
|
|
31
|
+
render: () => {
|
|
32
|
+
const series = [{ label: 'Sessions', data: [4, 7, 6, 9, 12] }];
|
|
33
|
+
return html `
|
|
34
|
+
<lyra-line-chart
|
|
35
|
+
area
|
|
36
|
+
height="16rem"
|
|
37
|
+
style="width: 22rem"
|
|
38
|
+
.labels=${['Jan', 'Feb', 'Mar', 'Apr', 'May']}
|
|
39
|
+
.datasets=${series}
|
|
40
|
+
></lyra-line-chart>
|
|
41
|
+
`;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
/** `zoom` enables wheel/drag/pinch zoom; a reset-zoom button appears once zoomed. */
|
|
45
|
+
export const WithZoom = {
|
|
46
|
+
render: () => {
|
|
47
|
+
const series = [{ label: 'Sessions', data: [4, 7, 6, 9, 12, 8, 15, 10, 6, 9] }];
|
|
48
|
+
return html `
|
|
49
|
+
<lyra-line-chart
|
|
50
|
+
zoom
|
|
51
|
+
height="16rem"
|
|
52
|
+
style="width: 22rem"
|
|
53
|
+
.labels=${['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O']}
|
|
54
|
+
.datasets=${series}
|
|
55
|
+
@lyra-zoom=${(e) => console.log('lyra-zoom', e.detail)}
|
|
56
|
+
></lyra-line-chart>
|
|
57
|
+
`;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
/** A second series on `axis: "y2"` plots against its own right-side scale, labelled by `y2-label`. */
|
|
61
|
+
export const DualAxis = {
|
|
62
|
+
render: () => {
|
|
63
|
+
const series = [
|
|
64
|
+
{ label: 'Sessions', data: [40, 70, 60, 90, 120] },
|
|
65
|
+
{ label: 'Conversion rate', data: [0.02, 0.03, 0.025, 0.04, 0.05], axis: 'y2' },
|
|
66
|
+
];
|
|
67
|
+
return html `
|
|
68
|
+
<lyra-line-chart
|
|
69
|
+
legend
|
|
70
|
+
height="16rem"
|
|
71
|
+
style="width: 22rem"
|
|
72
|
+
y-label="Sessions"
|
|
73
|
+
y2-label="Conversion rate"
|
|
74
|
+
.labels=${['Jan', 'Feb', 'Mar', 'Apr', 'May']}
|
|
75
|
+
.datasets=${series}
|
|
76
|
+
></lyra-line-chart>
|
|
77
|
+
`;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* `beginAtZero` lets the y axis start near the data's own minimum instead of
|
|
82
|
+
* 0 — set via the `.beginAtZero` property, since the `begin-at-zero`
|
|
83
|
+
* *attribute*'s presence (not its string value) is what Lit's boolean
|
|
84
|
+
* converter reads, so `begin-at-zero="false"` would not turn it off.
|
|
85
|
+
*/
|
|
86
|
+
export const WithoutBeginAtZero = {
|
|
87
|
+
render: () => {
|
|
88
|
+
const series = [{ label: 'Temperature (°C)', data: [18, 19, 21, 20, 22] }];
|
|
89
|
+
return html `
|
|
90
|
+
<lyra-line-chart
|
|
91
|
+
height="16rem"
|
|
92
|
+
style="width: 22rem"
|
|
93
|
+
.beginAtZero=${false}
|
|
94
|
+
.labels=${['Jan', 'Feb', 'Mar', 'Apr', 'May']}
|
|
95
|
+
.datasets=${series}
|
|
96
|
+
></lyra-line-chart>
|
|
97
|
+
`;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
export interface LiteSeries {
|
|
4
|
+
label: string;
|
|
5
|
+
data: (number | null)[];
|
|
6
|
+
/** Defaults to a color from the built-in categorical palette, keyed by dataset index. */
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
export type LyraLiteChartType = 'bar' | 'line';
|
|
10
|
+
/**
|
|
11
|
+
* `<lyra-lite-chart>` — a dependency-free bar/line chart, plain SVG/DOM
|
|
12
|
+
* rendering with zero peer dependencies (unlike `lyra-chart`, which wraps
|
|
13
|
+
* `chart.js`). For a project whose architecture forbids a charting
|
|
14
|
+
* dependency outright, this covers the common bar/line case: grouped or
|
|
15
|
+
* stacked bars, multi-series lines, per-point click, and hover tooltips
|
|
16
|
+
* (native SVG `<title>`, no positioning JS needed) — not a full `lyra-chart`
|
|
17
|
+
* replacement (no zoom/pan, no pie/doughnut/radar/scatter/bubble types, no
|
|
18
|
+
* horizontal/dual-y-axis, no raw-config passthrough).
|
|
19
|
+
*
|
|
20
|
+
* Because this renders real DOM (not canvas), it reuses `lyra-chart`'s
|
|
21
|
+
* `--lyra-chart-*` theme tokens directly via CSS `var()` — no
|
|
22
|
+
* `getComputedStyle()`-based re-theming step is needed the way `chart.ts`
|
|
23
|
+
* needs one for its canvas.
|
|
24
|
+
*
|
|
25
|
+
* @customElement lyra-lite-chart
|
|
26
|
+
* @event lyra-point-click - Fired when a bar/point is activated (click, or
|
|
27
|
+
* Enter/Space while focused). `detail: { datasetIndex: number, index:
|
|
28
|
+
* number, label: string | undefined, value: number | null }` — same shape
|
|
29
|
+
* as `lyra-chart`'s `lyra-point-click`.
|
|
30
|
+
* @csspart base - The host's flex layout wrapper.
|
|
31
|
+
* @csspart grid-line - Each horizontal gridline.
|
|
32
|
+
* @csspart axis-label - Each axis tick label.
|
|
33
|
+
* @csspart axis-title - The x/y axis title text, when set.
|
|
34
|
+
* @csspart bar - Each bar rect (type="bar").
|
|
35
|
+
* @csspart line - Each series' stroked line path (type="line").
|
|
36
|
+
* @csspart point - Each series' per-point hit target (type="line").
|
|
37
|
+
* @csspart legend - The legend row, when `legend` is set.
|
|
38
|
+
* @csspart legend-item - Each legend entry.
|
|
39
|
+
* @csspart legend-swatch - Each legend entry's color swatch.
|
|
40
|
+
*/
|
|
41
|
+
export declare class LyraLiteChart extends LyraElement {
|
|
42
|
+
static styles: import("lit").CSSResultGroup[];
|
|
43
|
+
type: LyraLiteChartType;
|
|
44
|
+
labels: string[];
|
|
45
|
+
datasets: LiteSeries[];
|
|
46
|
+
legend: boolean;
|
|
47
|
+
height: string;
|
|
48
|
+
xLabel: string;
|
|
49
|
+
yLabel: string;
|
|
50
|
+
beginAtZero: boolean;
|
|
51
|
+
/** Stacks each category's bars into one segmented bar. Ignored for `type="line"`. */
|
|
52
|
+
stacked: boolean;
|
|
53
|
+
private plotWidth;
|
|
54
|
+
private plotHeight;
|
|
55
|
+
private svgEl?;
|
|
56
|
+
private resizeObserver?;
|
|
57
|
+
connectedCallback(): void;
|
|
58
|
+
disconnectedCallback(): void;
|
|
59
|
+
protected firstUpdated(): void;
|
|
60
|
+
protected updated(changed: PropertyValues): void;
|
|
61
|
+
private colorFor;
|
|
62
|
+
private domain;
|
|
63
|
+
private emitPoint;
|
|
64
|
+
private onPointKeyDown;
|
|
65
|
+
private renderGrid;
|
|
66
|
+
private renderBars;
|
|
67
|
+
private renderLines;
|
|
68
|
+
render(): TemplateResult;
|
|
69
|
+
}
|
|
70
|
+
declare global {
|
|
71
|
+
interface HTMLElementTagNameMap {
|
|
72
|
+
'lyra-lite-chart': LyraLiteChart;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { html, svg, nothing } from 'lit';
|
|
8
|
+
import { property, state, query } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { styles } from './lite-chart.styles.js';
|
|
12
|
+
// Colorblind-considered categorical palette, used round-robin when a Series
|
|
13
|
+
// doesn't set its own `color` — same role as Chart.js's default dataset
|
|
14
|
+
// colors for lyra-chart, just a fixed JS array instead of a peer dep's.
|
|
15
|
+
const DEFAULT_PALETTE = [
|
|
16
|
+
'#4c6ef5',
|
|
17
|
+
'#f76707',
|
|
18
|
+
'#2f9e44',
|
|
19
|
+
'#e64980',
|
|
20
|
+
'#ae3ec9',
|
|
21
|
+
'#1098ad',
|
|
22
|
+
'#f59f00',
|
|
23
|
+
'#495057',
|
|
24
|
+
];
|
|
25
|
+
const PAD_LEFT = 36;
|
|
26
|
+
const PAD_RIGHT = 8;
|
|
27
|
+
const PAD_TOP = 8;
|
|
28
|
+
const PAD_BOTTOM = 20;
|
|
29
|
+
const AXIS_TITLE_SPACE = 14;
|
|
30
|
+
const TICK_COUNT = 4;
|
|
31
|
+
const BAR_GROUP_GAP = 0.2; // fraction of a category slot left as gap between categories
|
|
32
|
+
const BAR_GAP = 0.08; // fraction of a category slot left as gap between grouped bars
|
|
33
|
+
/**
|
|
34
|
+
* Picks a "nice" (1/2/5 × 10^n) step size for an axis spanning `span` over
|
|
35
|
+
* roughly `count` ticks — the standard Heckbert nice-numbers approach, so
|
|
36
|
+
* axis labels read as 0/25/50/75/100 rather than 0/23.4/46.8/70.2/93.6.
|
|
37
|
+
*/
|
|
38
|
+
function niceStep(span, count) {
|
|
39
|
+
if (span <= 0)
|
|
40
|
+
return 1;
|
|
41
|
+
const rough = span / Math.max(1, count);
|
|
42
|
+
const magnitude = 10 ** Math.floor(Math.log10(rough));
|
|
43
|
+
const residual = rough / magnitude;
|
|
44
|
+
const niceResidual = residual < 1.5 ? 1 : residual < 3 ? 2 : residual < 7 ? 5 : 10;
|
|
45
|
+
return niceResidual * magnitude;
|
|
46
|
+
}
|
|
47
|
+
/** Nice-rounded [lo, hi, ticks[]] for an axis covering `dataLo..dataHi`. */
|
|
48
|
+
function niceDomain(dataLo, dataHi, beginAtZero, count) {
|
|
49
|
+
let lo = beginAtZero ? Math.min(0, dataLo) : dataLo;
|
|
50
|
+
let hi = beginAtZero ? Math.max(0, dataHi) : dataHi;
|
|
51
|
+
if (lo === hi) {
|
|
52
|
+
lo -= 1;
|
|
53
|
+
hi += 1;
|
|
54
|
+
}
|
|
55
|
+
const step = niceStep(hi - lo, count);
|
|
56
|
+
lo = Math.floor(lo / step) * step;
|
|
57
|
+
hi = Math.ceil(hi / step) * step;
|
|
58
|
+
const ticks = [];
|
|
59
|
+
for (let v = lo; v <= hi + step / 2; v += step)
|
|
60
|
+
ticks.push(Math.round(v / step) * step);
|
|
61
|
+
return { lo, hi, ticks };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `<lyra-lite-chart>` — a dependency-free bar/line chart, plain SVG/DOM
|
|
65
|
+
* rendering with zero peer dependencies (unlike `lyra-chart`, which wraps
|
|
66
|
+
* `chart.js`). For a project whose architecture forbids a charting
|
|
67
|
+
* dependency outright, this covers the common bar/line case: grouped or
|
|
68
|
+
* stacked bars, multi-series lines, per-point click, and hover tooltips
|
|
69
|
+
* (native SVG `<title>`, no positioning JS needed) — not a full `lyra-chart`
|
|
70
|
+
* replacement (no zoom/pan, no pie/doughnut/radar/scatter/bubble types, no
|
|
71
|
+
* horizontal/dual-y-axis, no raw-config passthrough).
|
|
72
|
+
*
|
|
73
|
+
* Because this renders real DOM (not canvas), it reuses `lyra-chart`'s
|
|
74
|
+
* `--lyra-chart-*` theme tokens directly via CSS `var()` — no
|
|
75
|
+
* `getComputedStyle()`-based re-theming step is needed the way `chart.ts`
|
|
76
|
+
* needs one for its canvas.
|
|
77
|
+
*
|
|
78
|
+
* @customElement lyra-lite-chart
|
|
79
|
+
* @event lyra-point-click - Fired when a bar/point is activated (click, or
|
|
80
|
+
* Enter/Space while focused). `detail: { datasetIndex: number, index:
|
|
81
|
+
* number, label: string | undefined, value: number | null }` — same shape
|
|
82
|
+
* as `lyra-chart`'s `lyra-point-click`.
|
|
83
|
+
* @csspart base - The host's flex layout wrapper.
|
|
84
|
+
* @csspart grid-line - Each horizontal gridline.
|
|
85
|
+
* @csspart axis-label - Each axis tick label.
|
|
86
|
+
* @csspart axis-title - The x/y axis title text, when set.
|
|
87
|
+
* @csspart bar - Each bar rect (type="bar").
|
|
88
|
+
* @csspart line - Each series' stroked line path (type="line").
|
|
89
|
+
* @csspart point - Each series' per-point hit target (type="line").
|
|
90
|
+
* @csspart legend - The legend row, when `legend` is set.
|
|
91
|
+
* @csspart legend-item - Each legend entry.
|
|
92
|
+
* @csspart legend-swatch - Each legend entry's color swatch.
|
|
93
|
+
*/
|
|
94
|
+
export class LyraLiteChart extends LyraElement {
|
|
95
|
+
constructor() {
|
|
96
|
+
super(...arguments);
|
|
97
|
+
this.type = 'bar';
|
|
98
|
+
this.labels = [];
|
|
99
|
+
this.datasets = [];
|
|
100
|
+
this.legend = false;
|
|
101
|
+
this.height = '280px';
|
|
102
|
+
this.xLabel = '';
|
|
103
|
+
this.yLabel = '';
|
|
104
|
+
this.beginAtZero = true;
|
|
105
|
+
/** Stacks each category's bars into one segmented bar. Ignored for `type="line"`. */
|
|
106
|
+
this.stacked = false;
|
|
107
|
+
this.plotWidth = 0;
|
|
108
|
+
this.plotHeight = 0;
|
|
109
|
+
}
|
|
110
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
111
|
+
connectedCallback() {
|
|
112
|
+
super.connectedCallback();
|
|
113
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
114
|
+
const box = entries[0]?.contentBoxSize?.[0];
|
|
115
|
+
if (box) {
|
|
116
|
+
this.plotWidth = box.inlineSize;
|
|
117
|
+
this.plotHeight = box.blockSize;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const rect = this.svgEl?.getBoundingClientRect();
|
|
121
|
+
if (rect) {
|
|
122
|
+
this.plotWidth = rect.width;
|
|
123
|
+
this.plotHeight = rect.height;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
disconnectedCallback() {
|
|
129
|
+
super.disconnectedCallback();
|
|
130
|
+
this.resizeObserver?.disconnect();
|
|
131
|
+
}
|
|
132
|
+
firstUpdated() {
|
|
133
|
+
if (this.svgEl)
|
|
134
|
+
this.resizeObserver?.observe(this.svgEl);
|
|
135
|
+
}
|
|
136
|
+
updated(changed) {
|
|
137
|
+
if (changed.has('height')) {
|
|
138
|
+
this.style.setProperty('--lyra-chart-height', this.height);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
colorFor(index, series) {
|
|
142
|
+
return series.color ?? DEFAULT_PALETTE[index % DEFAULT_PALETTE.length];
|
|
143
|
+
}
|
|
144
|
+
domain() {
|
|
145
|
+
let lo = Infinity;
|
|
146
|
+
let hi = -Infinity;
|
|
147
|
+
if (this.type === 'bar' && this.stacked) {
|
|
148
|
+
// Stacked bars: each category's extent is the sum of its (signed)
|
|
149
|
+
// positive/negative segments, not the max single value.
|
|
150
|
+
for (let i = 0; i < this.labels.length; i++) {
|
|
151
|
+
let pos = 0;
|
|
152
|
+
let neg = 0;
|
|
153
|
+
for (const s of this.datasets) {
|
|
154
|
+
const v = s.data[i];
|
|
155
|
+
if (v == null)
|
|
156
|
+
continue;
|
|
157
|
+
if (v >= 0)
|
|
158
|
+
pos += v;
|
|
159
|
+
else
|
|
160
|
+
neg += v;
|
|
161
|
+
}
|
|
162
|
+
lo = Math.min(lo, neg);
|
|
163
|
+
hi = Math.max(hi, pos);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
for (const s of this.datasets) {
|
|
168
|
+
for (const v of s.data) {
|
|
169
|
+
if (v == null)
|
|
170
|
+
continue;
|
|
171
|
+
lo = Math.min(lo, v);
|
|
172
|
+
hi = Math.max(hi, v);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
|
|
177
|
+
lo = 0;
|
|
178
|
+
hi = 1;
|
|
179
|
+
}
|
|
180
|
+
return niceDomain(lo, hi, this.beginAtZero, TICK_COUNT);
|
|
181
|
+
}
|
|
182
|
+
emitPoint(datasetIndex, index) {
|
|
183
|
+
const label = this.labels[index];
|
|
184
|
+
const value = this.datasets[datasetIndex]?.data[index] ?? null;
|
|
185
|
+
this.emit('lyra-point-click', { datasetIndex, index, label, value });
|
|
186
|
+
}
|
|
187
|
+
onPointKeyDown(e, datasetIndex, index) {
|
|
188
|
+
if (e.key !== 'Enter' && e.key !== ' ')
|
|
189
|
+
return;
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
this.emitPoint(datasetIndex, index);
|
|
192
|
+
}
|
|
193
|
+
renderGrid(plotX, plotY, plotW, plotH, ticks, lo, hi) {
|
|
194
|
+
const span = hi - lo || 1;
|
|
195
|
+
return ticks.map((t) => {
|
|
196
|
+
const y = plotY + plotH - ((t - lo) / span) * plotH;
|
|
197
|
+
return svg `
|
|
198
|
+
<line part="grid-line" x1=${plotX} y1=${y} x2=${plotX + plotW} y2=${y}></line>
|
|
199
|
+
<text part="axis-label" x=${plotX - 6} y=${y} text-anchor="end" dominant-baseline="middle">${formatTick(t)}</text>
|
|
200
|
+
`;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
renderBars(plotX, plotY, plotW, plotH, lo, hi) {
|
|
204
|
+
const span = hi - lo || 1;
|
|
205
|
+
const n = this.labels.length;
|
|
206
|
+
const slot = n > 0 ? plotW / n : 0;
|
|
207
|
+
const groupCount = this.stacked ? 1 : Math.max(1, this.datasets.length);
|
|
208
|
+
const groupW = slot * (1 - BAR_GROUP_GAP);
|
|
209
|
+
const barW = (groupW - BAR_GAP * slot * (groupCount - 1)) / groupCount;
|
|
210
|
+
const bars = [];
|
|
211
|
+
for (let i = 0; i < n; i++) {
|
|
212
|
+
const slotStart = plotX + i * slot + (slot - groupW) / 2;
|
|
213
|
+
let stackPos = 0; // running positive-side offset (in value units) for stacked bars
|
|
214
|
+
let stackNeg = 0; // running negative-side offset
|
|
215
|
+
this.datasets.forEach((s, di) => {
|
|
216
|
+
const v = s.data[i];
|
|
217
|
+
if (v == null)
|
|
218
|
+
return;
|
|
219
|
+
const color = this.colorFor(di, s);
|
|
220
|
+
let barX;
|
|
221
|
+
let barValLo;
|
|
222
|
+
let barValHi;
|
|
223
|
+
if (this.stacked) {
|
|
224
|
+
barX = slotStart;
|
|
225
|
+
if (v >= 0) {
|
|
226
|
+
barValLo = stackPos;
|
|
227
|
+
barValHi = stackPos + v;
|
|
228
|
+
stackPos += v;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
barValLo = stackNeg + v;
|
|
232
|
+
barValHi = stackNeg;
|
|
233
|
+
stackNeg += v;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
barX = slotStart + di * (barW + BAR_GAP * slot);
|
|
238
|
+
barValLo = Math.min(0, v);
|
|
239
|
+
barValHi = Math.max(0, v);
|
|
240
|
+
}
|
|
241
|
+
const y1 = plotY + plotH - ((barValHi - lo) / span) * plotH;
|
|
242
|
+
const y2 = plotY + plotH - ((barValLo - lo) / span) * plotH;
|
|
243
|
+
const label = this.labels[i];
|
|
244
|
+
bars.push(svg `
|
|
245
|
+
<rect
|
|
246
|
+
part="bar"
|
|
247
|
+
x=${barX}
|
|
248
|
+
y=${y1}
|
|
249
|
+
width=${Math.max(0, barW)}
|
|
250
|
+
height=${Math.max(0, y2 - y1)}
|
|
251
|
+
fill=${color}
|
|
252
|
+
tabindex="0"
|
|
253
|
+
role="button"
|
|
254
|
+
aria-label=${`${s.label}, ${label}: ${v}`}
|
|
255
|
+
@click=${() => this.emitPoint(di, i)}
|
|
256
|
+
@keydown=${(e) => this.onPointKeyDown(e, di, i)}
|
|
257
|
+
><title>${s.label} — ${label}: ${v}</title></rect>
|
|
258
|
+
`);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return bars;
|
|
262
|
+
}
|
|
263
|
+
renderLines(plotX, plotY, plotW, plotH, lo, hi) {
|
|
264
|
+
const span = hi - lo || 1;
|
|
265
|
+
const n = this.labels.length;
|
|
266
|
+
const xFor = (i) => plotX + (n > 1 ? (i / (n - 1)) * plotW : plotW / 2);
|
|
267
|
+
const yFor = (v) => plotY + plotH - ((v - lo) / span) * plotH;
|
|
268
|
+
return this.datasets.map((s, di) => {
|
|
269
|
+
const color = this.colorFor(di, s);
|
|
270
|
+
const pts = s.data
|
|
271
|
+
.map((v, i) => (v == null ? null : [xFor(i), yFor(v)]))
|
|
272
|
+
.filter((p) => p !== null);
|
|
273
|
+
const d = pts.map(([x, y], i) => `${i ? 'L' : 'M'}${x},${y}`).join(' ');
|
|
274
|
+
const dots = s.data.map((v, i) => {
|
|
275
|
+
if (v == null)
|
|
276
|
+
return nothing;
|
|
277
|
+
const label = this.labels[i];
|
|
278
|
+
return svg `
|
|
279
|
+
<circle
|
|
280
|
+
part="point"
|
|
281
|
+
cx=${xFor(i)}
|
|
282
|
+
cy=${yFor(v)}
|
|
283
|
+
r="4"
|
|
284
|
+
fill=${color}
|
|
285
|
+
tabindex="0"
|
|
286
|
+
role="button"
|
|
287
|
+
aria-label=${`${s.label}, ${label}: ${v}`}
|
|
288
|
+
@click=${() => this.emitPoint(di, i)}
|
|
289
|
+
@keydown=${(e) => this.onPointKeyDown(e, di, i)}
|
|
290
|
+
><title>${s.label} — ${label}: ${v}</title></circle>
|
|
291
|
+
`;
|
|
292
|
+
});
|
|
293
|
+
return svg `<path part="line" d=${d} stroke=${color}></path>${dots}`;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
render() {
|
|
297
|
+
const w = this.plotWidth || 400;
|
|
298
|
+
const h = this.plotHeight || 200;
|
|
299
|
+
const padLeft = PAD_LEFT + (this.yLabel ? AXIS_TITLE_SPACE : 0);
|
|
300
|
+
const padBottom = PAD_BOTTOM + (this.xLabel ? AXIS_TITLE_SPACE : 0);
|
|
301
|
+
const plotX = padLeft;
|
|
302
|
+
const plotY = PAD_TOP;
|
|
303
|
+
const plotW = Math.max(0, w - padLeft - PAD_RIGHT);
|
|
304
|
+
const plotH = Math.max(0, h - plotY - padBottom);
|
|
305
|
+
const { lo, hi, ticks } = this.domain();
|
|
306
|
+
const grid = this.renderGrid(plotX, plotY, plotW, plotH, ticks, lo, hi);
|
|
307
|
+
const marks = this.type === 'bar'
|
|
308
|
+
? this.renderBars(plotX, plotY, plotW, plotH, lo, hi)
|
|
309
|
+
: this.renderLines(plotX, plotY, plotW, plotH, lo, hi);
|
|
310
|
+
const categoryLabels = this.labels.map((label, i) => {
|
|
311
|
+
const n = this.labels.length;
|
|
312
|
+
const x = plotX + (n > 1 ? (i / (n - 1)) * plotW : plotW / 2);
|
|
313
|
+
return svg `<text part="axis-label" x=${x} y=${plotY + plotH + 14} text-anchor="middle">${label}</text>`;
|
|
314
|
+
});
|
|
315
|
+
const chartLabel = this.datasets.map((d) => d.label).join(', ') || 'Chart';
|
|
316
|
+
return html `
|
|
317
|
+
<div part="base">
|
|
318
|
+
<svg viewBox="0 0 ${w} ${h}" role="group" aria-label=${chartLabel}>
|
|
319
|
+
${grid}
|
|
320
|
+
${categoryLabels}
|
|
321
|
+
${marks}
|
|
322
|
+
${this.yLabel
|
|
323
|
+
? svg `<text part="axis-title" x=${12} y=${plotY + plotH / 2} text-anchor="middle" transform="rotate(-90, 12, ${plotY + plotH / 2})">${this.yLabel}</text>`
|
|
324
|
+
: nothing}
|
|
325
|
+
${this.xLabel
|
|
326
|
+
? svg `<text part="axis-title" x=${plotX + plotW / 2} y=${plotY + plotH + padBottom - 2} text-anchor="middle">${this.xLabel}</text>`
|
|
327
|
+
: nothing}
|
|
328
|
+
</svg>
|
|
329
|
+
${this.legend
|
|
330
|
+
? html `<div part="legend">
|
|
331
|
+
${this.datasets.map((s, i) => html `
|
|
332
|
+
<span part="legend-item">
|
|
333
|
+
<span part="legend-swatch" style="background:${this.colorFor(i, s)}"></span>
|
|
334
|
+
${s.label}
|
|
335
|
+
</span>
|
|
336
|
+
`)}
|
|
337
|
+
</div>`
|
|
338
|
+
: nothing}
|
|
339
|
+
</div>
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
__decorate([
|
|
344
|
+
property()
|
|
345
|
+
], LyraLiteChart.prototype, "type", void 0);
|
|
346
|
+
__decorate([
|
|
347
|
+
property({ attribute: false })
|
|
348
|
+
], LyraLiteChart.prototype, "labels", void 0);
|
|
349
|
+
__decorate([
|
|
350
|
+
property({ attribute: false })
|
|
351
|
+
], LyraLiteChart.prototype, "datasets", void 0);
|
|
352
|
+
__decorate([
|
|
353
|
+
property({ type: Boolean })
|
|
354
|
+
], LyraLiteChart.prototype, "legend", void 0);
|
|
355
|
+
__decorate([
|
|
356
|
+
property()
|
|
357
|
+
], LyraLiteChart.prototype, "height", void 0);
|
|
358
|
+
__decorate([
|
|
359
|
+
property({ attribute: 'x-label' })
|
|
360
|
+
], LyraLiteChart.prototype, "xLabel", void 0);
|
|
361
|
+
__decorate([
|
|
362
|
+
property({ attribute: 'y-label' })
|
|
363
|
+
], LyraLiteChart.prototype, "yLabel", void 0);
|
|
364
|
+
__decorate([
|
|
365
|
+
property({ type: Boolean, attribute: 'begin-at-zero' })
|
|
366
|
+
], LyraLiteChart.prototype, "beginAtZero", void 0);
|
|
367
|
+
__decorate([
|
|
368
|
+
property({ type: Boolean })
|
|
369
|
+
], LyraLiteChart.prototype, "stacked", void 0);
|
|
370
|
+
__decorate([
|
|
371
|
+
state()
|
|
372
|
+
], LyraLiteChart.prototype, "plotWidth", void 0);
|
|
373
|
+
__decorate([
|
|
374
|
+
state()
|
|
375
|
+
], LyraLiteChart.prototype, "plotHeight", void 0);
|
|
376
|
+
__decorate([
|
|
377
|
+
query('svg')
|
|
378
|
+
], LyraLiteChart.prototype, "svgEl", void 0);
|
|
379
|
+
function formatTick(v) {
|
|
380
|
+
// Avoid float noise (e.g. 0.30000000000000004) from the niceStep() math
|
|
381
|
+
// above without hardcoding a fixed decimal count that'd butcher large ints.
|
|
382
|
+
return Number(v.toFixed(6)).toString();
|
|
383
|
+
}
|
|
384
|
+
defineElement('lite-chart', LyraLiteChart);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/web-components-vite';
|
|
2
|
+
declare const meta: Meta;
|
|
3
|
+
export default meta;
|
|
4
|
+
type Story = StoryObj;
|
|
5
|
+
/** Zero-dependency alternative to `lyra-chart` — plain SVG/DOM, no `chart.js` peer dep. */
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const GroupedBars: Story;
|
|
8
|
+
/** `stacked` sums each category's bars into one segmented bar instead of grouping them side by side. */
|
|
9
|
+
export declare const StackedBars: Story;
|
|
10
|
+
export declare const Line: Story;
|
|
11
|
+
/** Clicking (or Enter/Space on a focused) bar/point fires `lyra-point-click`, same detail shape as `lyra-chart`'s. */
|
|
12
|
+
export declare const ClickToFilter: Story;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
const meta = {
|
|
3
|
+
title: 'Charts/LiteChart',
|
|
4
|
+
component: 'lyra-lite-chart',
|
|
5
|
+
tags: ['autodocs'],
|
|
6
|
+
};
|
|
7
|
+
export default meta;
|
|
8
|
+
/** Zero-dependency alternative to `lyra-chart` — plain SVG/DOM, no `chart.js` peer dep. */
|
|
9
|
+
export const Default = {
|
|
10
|
+
render: () => {
|
|
11
|
+
const series = [{ label: 'Revenue', data: [12, 19, 14, 22] }];
|
|
12
|
+
return html `
|
|
13
|
+
<lyra-lite-chart
|
|
14
|
+
type="bar"
|
|
15
|
+
height="16rem"
|
|
16
|
+
style="width: 22rem"
|
|
17
|
+
legend
|
|
18
|
+
.labels=${['Q1', 'Q2', 'Q3', 'Q4']}
|
|
19
|
+
.datasets=${series}
|
|
20
|
+
></lyra-lite-chart>
|
|
21
|
+
`;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export const GroupedBars = {
|
|
25
|
+
render: () => {
|
|
26
|
+
const series = [
|
|
27
|
+
{ label: 'This year', data: [12, 19, 14, 22] },
|
|
28
|
+
{ label: 'Last year', data: [9, 15, 11, 18] },
|
|
29
|
+
];
|
|
30
|
+
return html `
|
|
31
|
+
<lyra-lite-chart
|
|
32
|
+
type="bar"
|
|
33
|
+
height="16rem"
|
|
34
|
+
style="width: 24rem"
|
|
35
|
+
legend
|
|
36
|
+
.labels=${['Q1', 'Q2', 'Q3', 'Q4']}
|
|
37
|
+
.datasets=${series}
|
|
38
|
+
></lyra-lite-chart>
|
|
39
|
+
`;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
/** `stacked` sums each category's bars into one segmented bar instead of grouping them side by side. */
|
|
43
|
+
export const StackedBars = {
|
|
44
|
+
render: () => {
|
|
45
|
+
const series = [
|
|
46
|
+
{ label: 'Docs', data: [4, 6, 3, 8] },
|
|
47
|
+
{ label: 'Bugs', data: [3, 2, 5, 4] },
|
|
48
|
+
{ label: 'Features', data: [5, 7, 6, 3] },
|
|
49
|
+
];
|
|
50
|
+
return html `
|
|
51
|
+
<lyra-lite-chart
|
|
52
|
+
type="bar"
|
|
53
|
+
stacked
|
|
54
|
+
height="16rem"
|
|
55
|
+
style="width: 24rem"
|
|
56
|
+
legend
|
|
57
|
+
x-label="Week"
|
|
58
|
+
y-label="Commits"
|
|
59
|
+
.labels=${['W1', 'W2', 'W3', 'W4']}
|
|
60
|
+
.datasets=${series}
|
|
61
|
+
></lyra-lite-chart>
|
|
62
|
+
`;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export const Line = {
|
|
66
|
+
render: () => {
|
|
67
|
+
const series = [
|
|
68
|
+
{ label: 'CPU', data: [20, 35, 42, 30, 55] },
|
|
69
|
+
{ label: 'Memory', data: [40, 38, 45, 60, 58] },
|
|
70
|
+
];
|
|
71
|
+
return html `
|
|
72
|
+
<lyra-lite-chart
|
|
73
|
+
type="line"
|
|
74
|
+
height="16rem"
|
|
75
|
+
style="width: 24rem"
|
|
76
|
+
legend
|
|
77
|
+
.labels=${['00:00', '00:05', '00:10', '00:15', '00:20']}
|
|
78
|
+
.datasets=${series}
|
|
79
|
+
></lyra-lite-chart>
|
|
80
|
+
`;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
/** Clicking (or Enter/Space on a focused) bar/point fires `lyra-point-click`, same detail shape as `lyra-chart`'s. */
|
|
84
|
+
export const ClickToFilter = {
|
|
85
|
+
render: () => {
|
|
86
|
+
const series = [{ label: 'Runs', data: [12, 19, 14, 22] }];
|
|
87
|
+
return html `
|
|
88
|
+
<lyra-lite-chart
|
|
89
|
+
type="bar"
|
|
90
|
+
height="16rem"
|
|
91
|
+
style="width: 22rem"
|
|
92
|
+
.labels=${['Q1', 'Q2', 'Q3', 'Q4']}
|
|
93
|
+
.datasets=${series}
|
|
94
|
+
@lyra-point-click=${(e) => alert(JSON.stringify(e.detail))}
|
|
95
|
+
></lyra-lite-chart>
|
|
96
|
+
`;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|