@columenlabs/lumen-ui 0.1.124 → 0.1.125

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.
@@ -1,10 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Trend widgets: up to 4 number fields over a date field of the view's rows, rolled up per day, week,
3
- // month or year, drawn as a line, area, stacked area, bars over time, combo, a spark or a tracker.
3
+ // month or year, drawn as a line, area, stacked area, bars over time, stacked bars, combo, a spark
4
+ // or a tracker.
4
5
  // With the allocation, donut, bars and card styles they cover every Tremor visualization, drawn on
5
6
  // our own Recharts in ChartContainer. Worked out in the browser from the rows; nothing is fetched.
6
7
  import { formatBytes, isDollarFormat, } from '@columenlabs/lumen-ui-types/views';
7
- import { useMemo } from 'react';
8
+ import { useMemo, useState } from 'react';
8
9
  import { Area, AreaChart, Bar, BarChart, CartesianGrid, ComposedChart, Line, LineChart, XAxis, YAxis, } from 'recharts';
9
10
  import { cn } from './lib/utils.js';
10
11
  import { Headline } from './lumen-widget-charts.js';
@@ -115,6 +116,8 @@ function tick(at, bucket) {
115
116
  export function TrendBody({ style, date, values, bucket, agg, rows, read, legend = true, }) {
116
117
  const hidden = useAmountsHidden();
117
118
  const points = useMemo(() => trendPoints(rows, date, values, bucket, agg, read), [rows, date, values, bucket, agg, read]);
119
+ // Series hidden by a tap on the legend; the last one shown can't be hidden.
120
+ const [off, setOff] = useState([]);
118
121
  const first = values[0];
119
122
  if (!points.length || !first)
120
123
  return _jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No dated rows to draw." });
@@ -124,6 +127,12 @@ export function TrendBody({ style, date, values, bucket, agg, rows, read, legend
124
127
  `v${i}`,
125
128
  { label: agg === 'count' ? `${f.label} (count)` : f.label, color: COLORS[i] },
126
129
  ]));
130
+ const keys = values.map((_, i) => `v${i}`);
131
+ const on = keys.filter((k) => !off.includes(k));
132
+ const toggle = (k) => setOff((o) => o.includes(k) ? o.filter((x) => x !== k) : o.length + 1 < keys.length ? [...o, k] : o);
133
+ // Stacked parts add up: the headline and tooltip read their total.
134
+ const stacks = (style === 'stacked' || style === 'stacked_bars') && on.length > 1;
135
+ const totalOf = (p) => on.reduce((t, k) => t + Number(p?.[k] ?? 0), 0);
127
136
  const last = Number(points[points.length - 1]?.v0 ?? 0);
128
137
  const start = Number(points[0]?.v0 ?? 0);
129
138
  if (style === 'tracker') {
@@ -143,17 +152,23 @@ export function TrendBody({ style, date, values, bucket, agg, rows, read, legend
143
152
  _jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }, "g"),
144
153
  _jsx(XAxis, { dataKey: "at", tickLine: false, axisLine: false, minTickGap: 24, tickFormatter: (v) => tick(String(v), bucket), fontSize: 11 }, "x"),
145
154
  _jsx(YAxis, { tickLine: false, axisLine: false, width: 48, tickFormatter: (v) => fmt(Number(v)), fontSize: 11 }, "y"),
146
- _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { labelFormatter: (v) => tick(String(v), bucket), formatter: (v, name) => {
155
+ _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { labelFormatter: (v) => tick(String(v), bucket), formatter: (v, name, item) => {
147
156
  const i = Number(String(name).slice(1));
148
157
  const f = values[i] ?? first;
149
- return (_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: COLORS[i] } }), _jsx("span", { className: "truncate", children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v), f) })] }));
158
+ return (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: COLORS[i] } }), _jsx("span", { className: "truncate", children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v), f) })] }), stacks && String(name) === on[on.length - 1] && (_jsxs("span", { className: "flex w-full items-center gap-2 border-t pt-1.5 font-medium", children: [_jsx("span", { className: "size-2 shrink-0" }), _jsx("span", { className: "truncate", children: "Total" }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(totalOf(item.payload), f) })] }))] }));
150
159
  } }) }, "t"),
151
160
  ...(legend && values.length > 1
152
- ? [_jsx(ChartLegend, { content: _jsx(ChartLegendContent, {}) }, "l")]
161
+ ? [_jsx(ChartLegend, { content: _jsx(ChartLegendContent, { off: off, onSeries: toggle }) }, "l")]
153
162
  : []),
154
163
  ];
155
- const lead = (_jsx(Headline, { value: fmt(last), caption: `${config.v0?.label} · ${tick(String(points[points.length - 1]?.at), bucket)}` }));
164
+ const end = points[points.length - 1];
165
+ const lead0 = on[0] ?? 'v0';
166
+ const lead = (_jsx(Headline, { value: fmt(stacks ? totalOf(end) : Number(end?.[lead0] ?? 0)), caption: `${stacks ? 'Total' : config[lead0]?.label} · ${tick(String(end?.at), bucket)}` }));
156
167
  const box = 'aspect-auto h-56 w-full';
157
- const keys = values.map((_, i) => `v${i}`);
158
- return (_jsxs("div", { className: "flex flex-col", children: [lead, _jsx(ChartContainer, { config: config, className: box, children: style === 'bars' ? (_jsxs(BarChart, { data: points, margin: { left: 0, right: 4 }, children: [axes, keys.map((k) => (_jsx(Bar, { dataKey: k, fill: `var(--color-${k})`, radius: 2, isAnimationActive: false }, k)))] })) : style === 'combo' ? (_jsxs(ComposedChart, { data: points, margin: { left: 0, right: 4 }, children: [axes, keys.map((k, i) => i === 0 ? (_jsx(Bar, { dataKey: k, fill: `var(--color-${k})`, radius: 2, isAnimationActive: false }, k)) : (_jsx(Line, { dataKey: k, type: "monotone", stroke: `var(--color-${k})`, strokeWidth: 1.5, dot: false, isAnimationActive: false }, k)))] })) : style === 'area' || style === 'stacked' ? (_jsxs(AreaChart, { data: points, margin: { left: 0, right: 4 }, children: [axes, keys.map((k) => (_jsx(Area, { dataKey: k, type: "monotone", stackId: style === 'stacked' ? 'a' : undefined, stroke: `var(--color-${k})`, fill: `var(--color-${k})`, fillOpacity: 0.15, strokeWidth: 1.5, isAnimationActive: false }, k)))] })) : (_jsxs(LineChart, { data: points, margin: { left: 0, right: 4 }, children: [axes, keys.map((k) => (_jsx(Line, { dataKey: k, type: "monotone", stroke: `var(--color-${k})`, strokeWidth: 1.5, dot: false, isAnimationActive: false }, k)))] })) })] }));
168
+ // Room above the top gridline, so its label sits on the line rather than below it (a bar near
169
+ // the top read as reaching the label's value).
170
+ const margin = { top: 8, left: 0, right: 4 };
171
+ // A stack's top part rounds its data end; a 2px gap of the card's colour parts the segments.
172
+ const top = on[on.length - 1];
173
+ return (_jsxs("div", { className: "flex flex-col", children: [lead, _jsx(ChartContainer, { config: config, className: box, children: style === 'bars' || style === 'stacked_bars' ? (_jsxs(BarChart, { data: points, margin: margin, children: [axes, keys.map((k) => style === 'stacked_bars' ? (_jsx(Bar, { dataKey: k, hide: off.includes(k), stackId: "a", fill: `var(--color-${k})`, stroke: "var(--card)", strokeWidth: on.length > 1 ? 2 : 0, radius: k === top ? [2, 2, 0, 0] : 0, isAnimationActive: false }, k)) : (_jsx(Bar, { dataKey: k, hide: off.includes(k), fill: `var(--color-${k})`, radius: 2, isAnimationActive: false }, k)))] })) : style === 'combo' ? (_jsxs(ComposedChart, { data: points, margin: margin, children: [axes, keys.map((k, i) => i === 0 ? (_jsx(Bar, { dataKey: k, hide: off.includes(k), fill: `var(--color-${k})`, radius: 2, isAnimationActive: false }, k)) : (_jsx(Line, { dataKey: k, hide: off.includes(k), type: "monotone", stroke: `var(--color-${k})`, strokeWidth: 1.5, dot: false, isAnimationActive: false }, k)))] })) : style === 'area' || style === 'stacked' ? (_jsxs(AreaChart, { data: points, margin: margin, children: [axes, keys.map((k) => (_jsx(Area, { dataKey: k, hide: off.includes(k), type: "monotone", stackId: style === 'stacked' ? 'a' : undefined, stroke: `var(--color-${k})`, fill: `var(--color-${k})`, fillOpacity: 0.15, strokeWidth: 1.5, isAnimationActive: false }, k)))] })) : (_jsxs(LineChart, { data: points, margin: margin, children: [axes, keys.map((k) => (_jsx(Line, { dataKey: k, hide: off.includes(k), type: "monotone", stroke: `var(--color-${k})`, strokeWidth: 1.5, dot: false, isAnimationActive: false }, k)))] })) })] }));
159
174
  }
@@ -690,7 +690,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
690
690
  bucket: "day" | "month" | "week" | "year";
691
691
  agg: "count" | "last" | "mean" | "sum";
692
692
  kind: "chart";
693
- style: "area" | "bars" | "combo" | "line" | "stacked";
693
+ style: "area" | "bars" | "combo" | "line" | "stacked" | "stacked_bars";
694
694
  } | {
695
695
  id: string;
696
696
  title: string | null;
@@ -37,8 +37,12 @@ declare function ChartTooltipContent({ active, payload, className, indicator, hi
37
37
  labelKey?: string;
38
38
  } & Omit<RechartsPrimitive.DefaultTooltipContentProps<TooltipValueType, TooltipNameType>, 'accessibilityLayer'>): React.JSX.Element | null;
39
39
  declare const ChartLegend: React.MemoExoticComponent<(outsideProps: RechartsPrimitive.LegendProps) => React.ReactPortal | null>;
40
- declare function ChartLegendContent({ className, hideIcon, payload, verticalAlign, nameKey, }: React.ComponentProps<'div'> & {
40
+ declare function ChartLegendContent({ className, hideIcon, payload, verticalAlign, nameKey, off, onSeries, }: React.ComponentProps<'div'> & {
41
41
  hideIcon?: boolean;
42
42
  nameKey?: string;
43
+ /** Series keys hidden by a tap on the legend; they stay listed, dimmed. */
44
+ off?: string[];
45
+ /** Given, each item is a button that shows or hides its series. */
46
+ onSeries?: (key: string) => void;
43
47
  } & RechartsPrimitive.DefaultLegendContentProps): React.JSX.Element | null;
44
48
  export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, };
package/dist/ui/chart.js CHANGED
@@ -81,7 +81,7 @@ function ChartTooltipContent({ active, payload, className, indicator = 'dot', hi
81
81
  }) })] }));
82
82
  }
83
83
  const ChartLegend = RechartsPrimitive.Legend;
84
- function ChartLegendContent({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey, }) {
84
+ function ChartLegendContent({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey, off, onSeries, }) {
85
85
  const { config } = useChart();
86
86
  if (!payload?.length) {
87
87
  return null;
@@ -91,8 +91,16 @@ function ChartLegendContent({ className, hideIcon = false, payload, verticalAlig
91
91
  .map((item, index) => {
92
92
  const key = `${nameKey ?? item.dataKey ?? 'value'}`;
93
93
  const itemConfig = getPayloadConfigFromPayload(config, item, key);
94
- return (_jsxs("div", { title: typeof itemConfig?.label === 'string' ? itemConfig.label : undefined, className: cn('flex max-w-full min-w-0 items-center gap-1.5 sm:max-w-64 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'), children: [itemConfig?.icon && !hideIcon ? (_jsx(itemConfig.icon, {})) : (_jsx("div", { className: "h-2 w-2 shrink-0 rounded-[2px]", style: {
95
- backgroundColor: item.color,
94
+ const series = String(item.dataKey ?? key);
95
+ const hidden = off?.includes(series) ?? false;
96
+ const Item = onSeries ? 'button' : 'div';
97
+ return (_jsxs(Item, { title: typeof itemConfig?.label === 'string' ? itemConfig.label : undefined, ...(onSeries && {
98
+ type: 'button',
99
+ 'aria-pressed': !hidden,
100
+ onClick: () => onSeries(series),
101
+ }), className: cn('flex max-w-full min-w-0 items-center gap-1.5 sm:max-w-64 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground', onSeries &&
102
+ 'min-h-6 cursor-pointer rounded-sm px-1 hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', hidden && 'text-muted-foreground line-through'), children: [itemConfig?.icon && !hideIcon ? (_jsx(itemConfig.icon, {})) : (_jsx("div", { className: "h-2 w-2 shrink-0 rounded-[2px]", style: {
103
+ backgroundColor: hidden ? 'var(--muted-foreground)' : item.color,
96
104
  } })), _jsx("span", { className: "truncate", children: itemConfig?.label })] }, index));
97
105
  }) }));
98
106
  }
@@ -514,5 +514,5 @@ export declare const readsRows: (card: ViewWidget) => card is {
514
514
  bucket: "day" | "month" | "week" | "year";
515
515
  agg: "count" | "last" | "mean" | "sum";
516
516
  kind: "chart";
517
- style: "area" | "bars" | "combo" | "line" | "stacked";
517
+ style: "area" | "bars" | "combo" | "line" | "stacked" | "stacked_bars";
518
518
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.124",
3
+ "version": "0.1.125",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.124",
52
+ "@columenlabs/lumen-ui-types": "0.1.125",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",