diff --git a/apps/backend/src/components/ai/system-prompt.tsx b/apps/backend/src/components/ai/system-prompt.tsx index e87e69834..82f206b64 100644 --- a/apps/backend/src/components/ai/system-prompt.tsx +++ b/apps/backend/src/components/ai/system-prompt.tsx @@ -94,6 +94,14 @@ export function SystemPrompt({ memories = [], userRules, connections = [], skill (e.g. YYYY-MM-DD). Use "category" for quarter labels (quarter_ending), fiscal periods (FY25-Q1), or any non-ISO-date strings. + + For display_chart on pipeline or task run data: if the SQL result includes{' '} + state_type and/or state_name columns, pass{' '} + filter_state_types and/or filter_state_names (each a non-empty array + of strings) so only matching rows are plotted; when both are set, rows must satisfy both. Optional{' '} + date_locale (BCP 47 tag, e.g. en-GB) controls how ISO date axis values are + formatted. + For display_chart chart_type: use "scatter" for correlations between two numeric variables (set x_axis_type to "number"). Use "radar" for comparing multiple metrics across a fixed set of diff --git a/apps/backend/src/components/generate-chart.tsx b/apps/backend/src/components/generate-chart.tsx index 6f508750c..a648794ca 100644 --- a/apps/backend/src/components/generate-chart.tsx +++ b/apps/backend/src/components/generate-chart.tsx @@ -1,4 +1,4 @@ -import { buildChart, defaultColorFor, labelize } from '@nao/shared'; +import { buildChart, defaultColorFor, filterChartRowsByStateDimensions, labelize } from '@nao/shared'; import type { displayChart } from '@nao/shared/tools'; import React from 'react'; import { renderToString } from 'react-dom/server'; @@ -6,7 +6,17 @@ import { renderToString } from 'react-dom/server'; import { createSvg, type LegendEntry, svgToPng } from '../utils/generate-chart'; export interface RenderChartInput { - config: Pick; + config: Pick< + displayChart.Input, + | 'chart_type' + | 'x_axis_key' + | 'x_axis_type' + | 'series' + | 'title' + | 'filter_state_types' + | 'filter_state_names' + | 'date_locale' + >; data: Record[]; width?: number; height?: number; @@ -20,7 +30,11 @@ export function generateChartImage(input: RenderChartInput): Buffer { } export function renderChartToSvg(input: RenderChartInput): string { - const { config, data } = input; + const { config } = input; + const data = filterChartRowsByStateDimensions(input.data, { + filterStateTypes: input.config.filter_state_types, + filterStateNames: input.config.filter_state_names, + }); const width = input.width ?? 800; const height = input.height ?? 500; const margin = input.margin ?? { top: 10, right: 20, bottom: 5, left: 0 }; @@ -31,7 +45,7 @@ export function renderChartToSvg(input: RenderChartInput): string { return series?.color || defaultColorFor(key, index); }; - const maxLabelWidth = estimateMaxLabelWidth(data, config.x_axis_key); + const maxLabelWidth = estimateMaxLabelWidth(data, config.x_axis_key, config.date_locale); const chart = buildChart({ data, @@ -44,13 +58,14 @@ export function renderChartToSvg(input: RenderChartInput): string { margin, title: config.title, maxXAxisTicks: Math.floor(width / maxLabelWidth), + dateLocale: config.date_locale, }); const html = renderToString(React.cloneElement(chart, { width, height })); const legend: LegendEntry[] = includeLegend ? config.series.map((s, i) => ({ - label: s.label || labelize(s.data_key), + label: s.label || labelize(s.data_key, config.date_locale), dataKey: s.data_key, color: colorFor(s.data_key, i), })) @@ -63,9 +78,9 @@ const CHAR_WIDTH_PX = 7; const TICK_PADDING_PX = 16; const MIN_TICK_WIDTH_PX = 40; -function estimateMaxLabelWidth(data: Record[], xAxisKey: string): number { +function estimateMaxLabelWidth(data: Record[], xAxisKey: string, dateLocale?: string): number { const maxCharCount = data.reduce((max, row) => { - const formatted = labelize(String(row[xAxisKey] ?? '')); + const formatted = labelize(String(row[xAxisKey] ?? ''), dateLocale); return Math.max(max, formatted.length); }, 0); return Math.max(maxCharCount * CHAR_WIDTH_PX + TICK_PADDING_PX, MIN_TICK_WIDTH_PX); diff --git a/apps/backend/src/utils/story-html.tsx b/apps/backend/src/utils/story-html.tsx index 68e9f719d..5b0675cdb 100644 --- a/apps/backend/src/utils/story-html.tsx +++ b/apps/backend/src/utils/story-html.tsx @@ -1,4 +1,4 @@ -import { defaultColorFor, labelize } from '@nao/shared'; +import { defaultColorFor, filterChartRowsByStateDimensions, labelize } from '@nao/shared'; import type { ParsedChartBlock, ParsedTableBlock, Segment } from '@nao/shared/story-segments'; import { splitCodeIntoSegments } from '@nao/shared/story-segments'; import { formatCellValue, isNumericColumn } from '@nao/shared/story-table-utils'; @@ -48,7 +48,7 @@ function StoryDocument({ title, children }: { title: string; children: React.Rea } function StoryFooter() { - const date = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); + const date = new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }); return (