Skip to content

Commit 684400a

Browse files
Merge pull request #1923 from objectstack-ai/feat/chart-metric-customization
feat(charts,dashboard): data-screen customization — bare metric, chart palette, donut/horizontal-bar
2 parents e29c12d + 677f7ed commit 684400a

7 files changed

Lines changed: 100 additions & 8 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@object-ui/plugin-charts": minor
3+
"@object-ui/plugin-dashboard": minor
4+
"@object-ui/types": minor
5+
---
6+
7+
feat(charts,dashboard): data-screen customization primitives
8+
9+
- object-metric `variant:'bare'` — big tinted number + label, no card chrome
10+
(data-screen KPIs that stay data-bound).
11+
- object-chart `colors` prop overrides the theme `--chart-1..n` palette so a
12+
page/dashboard can brand its charts; compact metric formatting (`'0.0a'`
13+
"1.1M").
14+
- ObjectChartSchema.chartType widened to donut/horizontal-bar/column.

.claude/launch.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
],
1212
"port": 5180
1313
},
14+
{
15+
"name": "console-build-test",
16+
"runtimeExecutable": "bash",
17+
"runtimeArgs": [
18+
"-lc",
19+
"cd /Users/zhuangjianguo/Documents/GitHub/objectui/.wt-aipicker/apps/console && VITE_SERVER_URL= DEV_PROXY_TARGET=http://localhost:4000 exec pnpm dev --port 5181 --strictPort"
20+
],
21+
"port": 5181
22+
},
1423
{
1524
"name": "gantt-demo",
1625
"runtimeExecutable": "pnpm",

packages/plugin-charts/src/AdvancedChartImpl.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ export interface AdvancedChartImplProps {
9797
xAxisKey?: string;
9898
series?: Array<{ dataKey: string; chartType?: 'bar' | 'line' | 'area'; variant?: 'current' | 'comparison'; opacity?: number; dashArray?: string; label?: string }>;
9999
className?: string;
100+
/** Categorical/series colour palette. Overrides the theme's `--chart-1..n`
101+
* defaults so a page/dashboard can brand its charts (data-screens). */
102+
colors?: string[];
100103
/**
101104
* Optional drill-down click handler. Fires when a chart segment is clicked
102105
* with `{ category, series, value }`. Wired for bar/horizontal-bar/line/
@@ -116,6 +119,7 @@ export default function AdvancedChartImpl({
116119
xAxisKey = 'name',
117120
series = [],
118121
className = '',
122+
colors,
119123
onChartClick,
120124
}: AdvancedChartImplProps) {
121125
// Normalize 'column' → 'bar' (Recharts BarChart is already vertical).
@@ -234,14 +238,15 @@ export default function AdvancedChartImpl({
234238
[data, xAxisKey],
235239
);
236240

237-
// Helper function to get color palette
238-
const getPalette = () => [
239-
'hsl(var(--chart-1))',
240-
'hsl(var(--chart-2))',
241-
'hsl(var(--chart-3))',
241+
// Helper function to get color palette. An explicit `colors` prop (set by the
242+
// page/dashboard) wins; otherwise fall back to the theme's --chart-1..5 vars.
243+
const getPalette = () => (Array.isArray(colors) && colors.length > 0 ? colors : [
244+
'hsl(var(--chart-1))',
245+
'hsl(var(--chart-2))',
246+
'hsl(var(--chart-3))',
242247
'hsl(var(--chart-4))',
243248
'hsl(var(--chart-5))'
244-
];
249+
]);
245250

246251
// Compact numeric formatter for Y-axis ticks (1,200,000 → 1.2M).
247252
// Keeps the axis readable when bar/area series have large values.

packages/plugin-charts/src/ChartRenderer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface ChartRendererProps {
4848
config?: Record<string, any>;
4949
xAxisKey?: string;
5050
series?: Array<{ dataKey: string; label?: string; variant?: 'current' | 'comparison'; opacity?: number; dashArray?: string; chartType?: 'bar' | 'line' | 'area' }>;
51+
colors?: string[];
5152
};
5253
/** Drill-down click handler — wired by ObjectChart when drillDown is enabled. */
5354
onChartClick?: (event: { category?: string; series?: string; value?: number }) => void;
@@ -108,6 +109,7 @@ export const ChartRenderer: React.FC<ChartRendererProps> = ({ schema, onChartCli
108109
xAxisKey={props.xAxisKey}
109110
series={props.series}
110111
className={props.className}
112+
colors={(schema as any).colors}
111113
onChartClick={onChartClick}
112114
/>
113115
</Suspense>

packages/plugin-dashboard/src/MetricWidget.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ function formatMetricValue(
8080
const decimalsMatch = trimmed.match(/0\.(0+)/);
8181
const decimals = decimalsMatch ? decimalsMatch[1].length : 0;
8282

83+
// Compact / abbreviated notation (numeral.js 'a' convention, e.g. '0.0a' →
84+
// "1.1M", '$0a' → "$1M"). Keeps big KPI numbers from overflowing a tile.
85+
const isCompact = /a/i.test(trimmed);
86+
if (isCompact) {
87+
const opts: Intl.NumberFormatOptions = { notation: 'compact', maximumFractionDigits: decimals || 1 };
88+
if (isCurrency) { opts.style = 'currency'; opts.currency = currency || symbolMap[trimmed[0]] || 'USD'; }
89+
try { return new Intl.NumberFormat('en-US', opts).format(value as number); } catch { /* fall through */ }
90+
}
91+
8392
if (isCurrency) {
8493
const code = currency || symbolMap[trimmed[0]] || 'USD';
8594
try {
@@ -133,6 +142,19 @@ const VARIANT_ICON_CLASSES: Record<MetricColorVariant, string> = {
133142
danger: 'bg-rose-500/10 text-rose-600 dark:text-rose-400',
134143
};
135144

145+
/** Text-colour per variant — used by the `bare` layout to tint the big number
146+
* (data-screen KPIs) instead of an icon chip. */
147+
const VARIANT_TEXT_CLASSES: Record<MetricColorVariant, string> = {
148+
default: 'text-foreground',
149+
blue: 'text-blue-600 dark:text-blue-400',
150+
teal: 'text-teal-600 dark:text-teal-400',
151+
orange: 'text-orange-600 dark:text-orange-400',
152+
purple: 'text-purple-600 dark:text-purple-400',
153+
success: 'text-emerald-600 dark:text-emerald-400',
154+
warning: 'text-amber-600 dark:text-amber-400',
155+
danger: 'text-rose-600 dark:text-rose-400',
156+
};
157+
136158
export interface MetricWidgetProps {
137159
label: string | { key?: string; defaultValue?: string };
138160
value: string | number;
@@ -160,6 +182,12 @@ export interface MetricWidgetProps {
160182
suffix?: string;
161183
/** When set, the entire card becomes clickable and emits this handler. */
162184
onClick?: () => void;
185+
/**
186+
* Layout variant. `'card'` (default) is the bordered KPI card; `'bare'` drops
187+
* all card chrome and renders a large tinted number + label — the dense
188+
* big-number look data-screens (大屏) want, while staying data-bound.
189+
*/
190+
variant?: 'card' | 'bare';
163191
}
164192

165193
export const MetricWidget = ({
@@ -177,6 +205,7 @@ export const MetricWidget = ({
177205
prefix,
178206
suffix,
179207
onClick,
208+
variant = 'card',
180209
...props
181210
}: MetricWidgetProps) => {
182211
const iconClasses = VARIANT_ICON_CLASSES[colorVariant] || VARIANT_ICON_CLASSES.default;
@@ -211,6 +240,34 @@ export const MetricWidget = ({
211240
return icon;
212241
}, [icon]);
213242

243+
if (variant === 'bare') {
244+
const textClasses = VARIANT_TEXT_CLASSES[colorVariant] || VARIANT_TEXT_CLASSES.default;
245+
return (
246+
<div
247+
className={cn('flex flex-col gap-1 min-w-0', onClick && 'cursor-pointer', className)}
248+
onClick={onClick}
249+
role={onClick ? 'button' : undefined}
250+
tabIndex={onClick ? 0 : undefined}
251+
onKeyDown={onClick ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } } : undefined}
252+
>
253+
{loading ? (
254+
<div className="flex items-center gap-2 text-muted-foreground" data-testid="metric-loading">
255+
<Loader2 className="h-4 w-4 animate-spin" /><span className="text-sm">Loading…</span>
256+
</div>
257+
) : error ? (
258+
<div className="flex items-center gap-2" data-testid="metric-error" role="alert">
259+
<AlertCircle className="h-4 w-4 text-destructive shrink-0" /><span className="text-xs text-destructive truncate">{error}</span>
260+
</div>
261+
) : (
262+
<>
263+
<div className={cn('text-[2rem] leading-none font-extrabold tabular-nums tracking-tight truncate', textClasses)}>{displayValue}</div>
264+
<div className="text-xs font-medium text-muted-foreground truncate">{resolveLabel(label)}</div>
265+
</>
266+
)}
267+
</div>
268+
);
269+
}
270+
214271
return (
215272
<Card
216273
className={cn(

packages/plugin-dashboard/src/ObjectMetricWidget.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export interface ObjectMetricWidgetProps {
102102
compareTo?: CompareToConfig;
103103
/** Optional i18n translator used to localize the trend label. */
104104
t?: (key: string, defaultValue: string) => string;
105+
/** Layout variant forwarded to MetricWidget: 'card' (default) or 'bare'. */
106+
variant?: 'card' | 'bare';
105107
}
106108

107109
export const ObjectMetricWidget: React.FC<ObjectMetricWidgetProps> = ({
@@ -125,6 +127,7 @@ export const ObjectMetricWidget: React.FC<ObjectMetricWidgetProps> = ({
125127
title,
126128
compareTo,
127129
t: tProp,
130+
variant,
128131
}) => {
129132
const context = useContext(SchemaRendererContext);
130133
const dataSource = propDataSource || context?.dataSource;
@@ -432,6 +435,7 @@ export const ObjectMetricWidget: React.FC<ObjectMetricWidgetProps> = ({
432435
currency={inferredCurrency}
433436
prefix={prefix}
434437
suffix={suffix}
438+
variant={variant}
435439
onClick={drillEnabled ? () => setDrillOpen(true) : undefined}
436440
/>
437441
{drillOpen && drillDrawer}

packages/types/src/objectql.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2083,8 +2083,9 @@ export interface ObjectChartSchema extends BaseSchema {
20832083
type: 'object-chart';
20842084
/** ObjectQL object name (legacy inline path; optional under ADR-0021 dataset binding) */
20852085
objectName?: string;
2086-
/** Chart type */
2087-
chartType: 'bar' | 'line' | 'pie' | 'area' | 'scatter';
2086+
/** Chart type. Includes donut / horizontal-bar / column — all rendered by
2087+
* AdvancedChartImpl (previously only reachable by passing an untyped string). */
2088+
chartType: 'bar' | 'column' | 'horizontal-bar' | 'line' | 'area' | 'pie' | 'donut' | 'scatter';
20882089
/** Field for X axis (categories) — legacy inline path */
20892090
xAxisField?: string;
20902091
/** Fields for Y axis (values) — legacy */

0 commit comments

Comments
 (0)