@@ -142,7 +151,7 @@ function ChartBlock({ chart, queryData }: { chart: ParsedChartBlock; queryData:
data-chart={chartData}
dangerouslySetInnerHTML={{ __html: svg }}
/>
- {chart.chartType !== 'pie' && }
+ {chart.chartType !== 'pie' && }
);
} catch {
@@ -150,7 +159,7 @@ function ChartBlock({ chart, queryData }: { chart: ParsedChartBlock; queryData:
}
}
-function ChartLegend({ series }: { series: ParsedChartBlock['series'] }) {
+function ChartLegend({ series, chart }: { series: ParsedChartBlock['series']; chart: ParsedChartBlock }) {
return (
{series.map((s, i) => {
@@ -161,7 +170,7 @@ function ChartLegend({ series }: { series: ParsedChartBlock['series'] }) {
style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#6b7280', fontSize: 12 }}
>
- {s.label || labelize(s.data_key)}
+ {s.label || labelize(s.data_key, chart.dateLocale)}
);
})}
@@ -306,6 +315,9 @@ function toChartConfig(chart: ParsedChartBlock) {
x_axis_type: chart.xAxisType as displayChart.XAxisType | null,
series: chart.series,
title: chart.title,
+ filter_state_types: chart.filterStateTypes,
+ filter_state_names: chart.filterStateNames,
+ date_locale: chart.dateLocale,
};
}
@@ -345,9 +357,10 @@ const TOOLTIP_SCRIPT = `
(function(){
var PIE_COLORS=['#104e64','#f54900','#009689','#ffb900','#fe9a00'];
function escHtml(s){return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''')}
- function labelize(s){
+ function labelize(s,loc){
var str=String(s);
- if(/^\\d{4}-\\d{2}-\\d{2}/.test(str)){var d=new Date(str);if(!isNaN(d.getTime()))return escHtml(d.toLocaleDateString('en-US',{timeZone:'UTC'}))}
+ var lc=loc||undefined;
+ if(/^\\d{4}-\\d{2}-\\d{2}/.test(str)){var d=new Date(str);if(!isNaN(d.getTime()))return escHtml(d.toLocaleDateString(lc,{timeZone:'UTC'}))}
return escHtml(str.replace(/_/g,' ').replace(/\\b\\w/g,function(c){return c.toUpperCase()}))
}
function formatVal(v){return escHtml(typeof v==='number'?v.toLocaleString():String(v!=null?v:''))}
@@ -416,7 +429,8 @@ const TOOLTIP_SCRIPT = `
function showTip(e,row){
var label=row[cfg.xAxisKey];
var isPie=!!pieColorMap;
- var html=''+(isPie?labelize(cfg.series[0]&&(cfg.series[0].label||cfg.series[0].data_key)||''):labelize(label!=null?label:''))+'
';
+ var loc=cfg.dateLocale||undefined;
+ var html=''+(isPie?labelize(cfg.series[0]&&(cfg.series[0].label||cfg.series[0].data_key)||'',loc):labelize(label!=null?label:'',loc))+'
';
html+='';
var numericValues=[];
cfg.series.forEach(function(s){
@@ -429,7 +443,7 @@ const TOOLTIP_SCRIPT = `
}
var val=row[s.data_key];
if(typeof val==='number')numericValues.push(val);
- var rowName=isPie?labelize(label!=null?label:''):labelize(s.label||s.data_key);
+ var rowName=isPie?labelize(label!=null?label:'',loc):labelize(s.label||s.data_key,loc);
html+='
);
@@ -243,6 +273,7 @@ export interface ChartDisplayProps {
series: displayChart.SeriesConfig[];
title?: string;
showGrid?: boolean;
+ chartDateLocale?: string;
}
export const ChartDisplay = memo(function ChartDisplay({
@@ -254,6 +285,7 @@ export const ChartDisplay = memo(function ChartDisplay({
series,
title,
showGrid = true,
+ chartDateLocale,
}: ChartDisplayProps) {
const { visibleSeries, hiddenSeriesKeys, handleToggleSeriesVisibility } = useSeriesVisibility(series);
@@ -263,14 +295,14 @@ export const ChartDisplay = memo(function ChartDisplay({
return [...values].reduce(
(acc, v, index) => {
acc[toKey(v)] = {
- label: labelize(v),
+ label: labelize(v, chartDateLocale),
color: Colors[index % Colors.length],
};
return acc;
},
{
[xAxisKey]: {
- label: labelize(xAxisKey),
+ label: labelize(xAxisKey, chartDateLocale),
},
} as ChartConfig,
);
@@ -278,12 +310,12 @@ export const ChartDisplay = memo(function ChartDisplay({
return series.reduce((acc, s, idx) => {
acc[s.data_key] = {
- label: s.label || labelize(s.data_key),
+ label: s.label || labelize(s.data_key, chartDateLocale),
color: s.color || Colors[idx % Colors.length],
};
return acc;
}, {} as ChartConfig);
- }, [series, xAxisKey, data, chartType]);
+ }, [series, xAxisKey, data, chartType, chartDateLocale]);
const colorFor = useMemo(
() =>
@@ -296,12 +328,12 @@ export const ChartDisplay = memo(function ChartDisplay({
const legendPayload = useMemo(
() =>
series.map((s, idx) => ({
- value: s.label || labelize(s.data_key),
+ value: s.label || labelize(s.data_key, chartDateLocale),
dataKey: s.data_key,
color: s.color || Colors[idx % Colors.length],
isHidden: hiddenSeriesKeys.has(s.data_key),
})),
- [series, hiddenSeriesKeys],
+ [series, hiddenSeriesKeys, chartDateLocale],
);
const chartElement = useMemo(
@@ -314,6 +346,7 @@ export const ChartDisplay = memo(function ChartDisplay({
series: visibleSeries,
colorFor,
labelFormatter: xAxisLabelFormatter,
+ dateLocale: chartDateLocale,
showGrid,
margin: { top: 0, right: 0, bottom: 0, left: 0 },
children: [
@@ -322,7 +355,9 @@ export const ChartDisplay = memo(function ChartDisplay({
animationDuration={150}
animationEasing='linear'
allowEscapeViewBox={{ y: true, x: false }}
- content={
labelize(value)} />}
+ content={
+ labelize(value, chartDateLocale)} />
+ }
/>,
chartType !== 'pie' && (
c.toUpperCase());
@@ -67,6 +67,8 @@ export interface BuildChartProps {
margin?: { top?: number; right?: number; bottom?: number; left?: number };
title?: string;
maxXAxisTicks?: number;
+ /** BCP 47 locale for ISO date axis labels; forwarded to labelize when no custom labelFormatter */
+ dateLocale?: string;
}
/**
@@ -98,7 +100,8 @@ export function buildChart(props: BuildChartProps) {
function buildResolved(props: BuildChartProps) {
const colorFor = props.colorFor ?? defaultColorFor;
- const labelFormatter = props.labelFormatter ?? ((v: string) => labelize(v));
+ const dateLc = props.dateLocale;
+ const labelFormatter = props.labelFormatter ?? ((v: string) => labelize(v, dateLc));
const titleChild = props.title ? (
, logicalKey: string): string {
+ for (const [key, raw] of Object.entries(row)) {
+ if (key.toLowerCase() === logicalKey) {
+ if (raw === null || raw === undefined) {
+ return '';
+ }
+ return typeof raw === 'string' ? raw : String(raw);
+ }
+ }
+ return '';
+}
+
+export function filterChartRowsByStateDimensions(
+ data: Record[],
+ filters: ChartStateDimensionFilters,
+): Record[] {
+ let rows = data;
+ const types = filters.filterStateTypes?.filter((t) => t.length > 0);
+ const names = filters.filterStateNames?.filter((n) => n.length > 0);
+
+ if (types?.length) {
+ const allowed = new Set(types);
+ rows = rows.filter((row) => allowed.has(readStateColumn(row, 'state_type')));
+ }
+ if (names?.length) {
+ const allowed = new Set(names);
+ rows = rows.filter((row) => allowed.has(readStateColumn(row, 'state_name')));
+ }
+ return rows;
+}
diff --git a/apps/shared/src/index.ts b/apps/shared/src/index.ts
index ab16281d9..ad37979b0 100644
--- a/apps/shared/src/index.ts
+++ b/apps/shared/src/index.ts
@@ -1,4 +1,5 @@
export * from './chart-builder';
+export * from './chart-row-filters';
export * from './citation';
export * from './date';
export * from './mcp';
diff --git a/apps/shared/src/story-segments.ts b/apps/shared/src/story-segments.ts
index 299de8e01..6a2287d80 100644
--- a/apps/shared/src/story-segments.ts
+++ b/apps/shared/src/story-segments.ts
@@ -5,6 +5,9 @@ export interface ParsedChartBlock {
xAxisType: string | null;
series: Array<{ data_key: string; color: string; label?: string }>;
title: string;
+ filterStateTypes?: string[];
+ filterStateNames?: string[];
+ dateLocale?: string;
}
export interface ParsedTableBlock {
@@ -32,6 +35,18 @@ export function parseChartAttributes(attrString: string): Record
return attrs;
}
+function tryParseStringArray(raw: string | undefined): string[] | undefined {
+ if (!raw?.trim()) {
+ return undefined;
+ }
+ try {
+ const parsed = JSON.parse(raw);
+ return Array.isArray(parsed) ? parsed.map(String) : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
export function parseChartBlock(attrString: string): ParsedChartBlock | null {
const attrs = parseChartAttributes(attrString);
if (!attrs.query_id || !attrs.chart_type || !attrs.x_axis_key) {
@@ -52,6 +67,10 @@ export function parseChartBlock(attrString: string): ParsedChartBlock | null {
});
}
+ const filterStateTypes = tryParseStringArray(attrs.filter_state_types);
+ const filterStateNames = tryParseStringArray(attrs.filter_state_names);
+ const dateLocale = attrs.date_locale?.trim() || undefined;
+
return {
queryId: attrs.query_id,
chartType: attrs.chart_type,
@@ -59,6 +78,9 @@ export function parseChartBlock(attrString: string): ParsedChartBlock | null {
xAxisType: attrs.x_axis_type || null,
series,
title: attrs.title || '',
+ filterStateTypes: filterStateTypes?.length ? filterStateTypes : undefined,
+ filterStateNames: filterStateNames?.length ? filterStateNames : undefined,
+ dateLocale,
};
}
diff --git a/apps/shared/src/tools/display-chart.ts b/apps/shared/src/tools/display-chart.ts
index 970a00ab5..02b9a053d 100644
--- a/apps/shared/src/tools/display-chart.ts
+++ b/apps/shared/src/tools/display-chart.ts
@@ -36,6 +36,24 @@ export const InputSchema = z.object({
.describe(
'A concise and descriptive title of what the chart shows. Do not include the type of chart in the title or other chart configurations.',
),
+ filter_state_types: z
+ .array(z.string())
+ .optional()
+ .describe(
+ 'When set and non-empty, only rows whose `state_type` column equals one of these values are plotted (flow/pipeline/task run dashboards). Omit if the result has no state_type.',
+ ),
+ filter_state_names: z
+ .array(z.string())
+ .optional()
+ .describe(
+ 'When set and non-empty, only rows whose `state_name` column equals one of these values are plotted. Omit if the result has no state_name.',
+ ),
+ date_locale: z
+ .string()
+ .optional()
+ .describe(
+ 'BCP 47 locale for formatting ISO dates on axes (e.g. "en-GB"). Omit for the viewer default locale.',
+ ),
});
export const OutputSchema = z.object({