Skip to content

Commit b0e9942

Browse files
feat(reports): keep every metric in the data, and let the reader switch between them
Report #2. The visualization only ever showed one dataset because the outline prompt explicitly said so: "If the data has multiple metrics, choose the most scientifically relevant one" - and the schema only had room for one data array. Everything else the researcher uploaded was discarded at generation time, so no amount of UI could get it back. - The chart schema gains `additionalSeries`: one entry per OTHER metric, each with its own name, axis label with units, chart type and data. The primary metric stays as `data` so the rendered PNG and older reports are unaffected. - The prompt now requires those other metrics to be returned rather than dropped, and says why: the researcher uploaded them and expects to see them. - The full-size visualization dialog lists the metrics as chips and swaps the interactive chart between them. With one metric it renders exactly as before; older reports with no additionalSeries fall back to the stored PNG. Note this only helps reports generated from here on - the discarded metrics were never persisted, so existing reports have nothing to switch to and will keep showing their single chart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 7ce203f commit b0e9942

3 files changed

Lines changed: 106 additions & 15 deletions

File tree

app/api/report/outline/route.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ RULES:
7474
- "bar" - comparing a numeric metric across conditions / samples / formulations (DEFAULT).
7575
- "pie" - showing proportion / share / composition of a whole (values should sum to ~100% or represent parts of one whole). Use pie ONLY when parts-of-a-whole is the actual meaning.
7676
3. Use SHORT labels (max 15 characters) for the categories. Abbreviate names (e.g. "C0 Control", "E1", "F2 Glycine").
77-
4. All values MUST be in the same unit. If the data has multiple metrics, choose the most scientifically relevant one.
77+
4. All values within ONE series MUST share a unit. If the data has several metrics, pick the most scientifically relevant one as the PRIMARY chart - and return every OTHER metric in additionalSeries, one entry each, with its own label and units. Do NOT silently drop the other metrics: the researcher uploaded them and expects to be able to see them.
7878
5. Provide a clear chartTitle (e.g. "Mean Viscosity by Formulation") and yAxisLabel WITH units for bar charts (e.g. "Viscosity (mPa·s)"). yAxisLabel can be empty for pie charts.
7979
6. Return only POSITIVE values when possible. If comparing % changes, use absolute values or the raw measurement instead.
8080
7. IMPORTANT: Include ALL data points / conditions / formulations from the dataset. Do NOT skip or drop any rows.`
@@ -89,7 +89,8 @@ Return a JSON object with:
8989
- chartTitle: descriptive title
9090
- chartType: "bar" or "pie"
9191
- yAxisLabel: Y-axis label with units (for bar charts)
92-
- data: array of {label, value} pairs with short labels and consistent numeric values`
92+
- data: array of {label, value} pairs with short labels and consistent numeric values
93+
- additionalSeries: one entry per OTHER metric in the data (metric name, yAxisLabel with units, its own data array)`
9394

9495
try {
9596
const completion = await openai().beta.chat.completions.parse({
@@ -408,7 +409,30 @@ const chartTool = tool(
408409
yAxisLabel: z
409410
.string()
410411
.optional()
411-
.describe("Label for the Y axis with units (bar charts only)")
412+
.describe("Label for the Y axis with units (bar charts only)"),
413+
/**
414+
* EVERY other metric present in the data, one entry each. The primary
415+
* `data` above stays the headline chart (and the rendered PNG); these let
416+
* the researcher switch to the other readouts instead of losing them.
417+
*/
418+
additionalSeries: z
419+
.array(
420+
z.object({
421+
metric: z
422+
.string()
423+
.describe("What this series measures, e.g. 'SEC monomer'"),
424+
yAxisLabel: z
425+
.string()
426+
.describe("Axis label WITH units, e.g. 'Monomer (%)'"),
427+
chartType: z.enum(["bar", "pie"]).optional(),
428+
data: z.array(z.object({ label: z.string(), value: z.number() }))
429+
})
430+
)
431+
.max(8)
432+
.optional()
433+
.describe(
434+
"One entry per ADDITIONAL metric in the data. Omit only if the data truly has a single metric."
435+
)
412436
})
413437
}
414438
)

components/reports/report-editor.tsx

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import { InputsTab } from "@/components/reports/tabs/inputs-tab"
3434
import { ReportTab as ReportTabView } from "@/components/reports/tabs/report-tab"
3535
import { ReportPreviewModal } from "@/components/reports/report-preview-modal"
3636
import { ReportGeneratingView } from "@/components/reports/report-generating-view"
37+
import {
38+
ReportChart,
39+
type ChartType
40+
} from "@/app/[locale]/[workspaceid]/report/components/report-chart"
3741
import { ELNExportModal } from "@/components/eln/eln-export-modal"
3842
import { ELNConnectModal } from "@/components/eln/eln-connect-modal"
3943
import { ELNConnection } from "@/types/eln"
@@ -208,6 +212,7 @@ export function ReportEditor({
208212
const [regeneratingChart, setRegeneratingChart] = useState(false)
209213
const [showPreview, setShowPreview] = useState(false)
210214
const [showViz, setShowViz] = useState(false)
215+
const [vizSeries, setVizSeries] = useState(0)
211216
const [isSavingNow, setIsSavingNow] = useState(false)
212217
const sectionSaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
213218

@@ -1124,18 +1129,80 @@ export function ReportEditor({
11241129
{report?.name ? `${report.name} — results` : "Result data"}
11251130
</DialogTitle>
11261131
</DialogHeader>
1127-
{report?.chart_image ? (
1128-
// eslint-disable-next-line @next/next/no-img-element
1129-
<img
1130-
src={report.chart_image as string}
1131-
alt="Result visualization"
1132-
className="border-ink-200 max-h-[70vh] w-full rounded-lg border bg-white object-contain"
1133-
/>
1134-
) : (
1135-
<p className="text-ink-500 py-8 text-center text-sm">
1136-
No chart has been generated for this report yet.
1137-
</p>
1138-
)}
1132+
{(() => {
1133+
const cd = (report?.chart_data ?? null) as any
1134+
// Primary metric first, then every other metric the outline found.
1135+
// The generator used to be told to pick ONE metric and discard the
1136+
// rest, so uploads with several readouts only ever charted one.
1137+
const series: Array<{
1138+
metric: string
1139+
yAxisLabel?: string
1140+
chartType?: ChartType
1141+
data: { label: string; value: number }[]
1142+
}> = []
1143+
if (Array.isArray(cd?.data) && cd.data.length) {
1144+
series.push({
1145+
metric: cd.chartTitle || "Primary result",
1146+
yAxisLabel: cd.yAxisLabel,
1147+
chartType: cd.chartType,
1148+
data: cd.data
1149+
})
1150+
}
1151+
for (const extra of cd?.additionalSeries ?? []) {
1152+
if (Array.isArray(extra?.data) && extra.data.length) {
1153+
series.push({
1154+
metric: extra.metric || "Metric",
1155+
yAxisLabel: extra.yAxisLabel,
1156+
chartType: extra.chartType,
1157+
data: extra.data
1158+
})
1159+
}
1160+
}
1161+
const active = series[Math.min(vizSeries, series.length - 1)]
1162+
if (!active) {
1163+
return report?.chart_image ? (
1164+
// eslint-disable-next-line @next/next/no-img-element
1165+
<img
1166+
src={report.chart_image as string}
1167+
alt="Result visualization"
1168+
className="border-ink-200 max-h-[70vh] w-full rounded-lg border bg-white object-contain"
1169+
/>
1170+
) : (
1171+
<p className="text-ink-500 py-8 text-center text-sm">
1172+
No chart has been generated for this report yet.
1173+
</p>
1174+
)
1175+
}
1176+
return (
1177+
<div className="space-y-3">
1178+
{series.length > 1 && (
1179+
<div className="flex flex-wrap gap-1.5">
1180+
{series.map((sr, i) => (
1181+
<button
1182+
key={sr.metric + i}
1183+
type="button"
1184+
onClick={() => setVizSeries(i)}
1185+
className={cn(
1186+
"rounded-full border px-3 py-1 text-[12px] transition-colors",
1187+
i === vizSeries
1188+
? "border-ink bg-ink text-white"
1189+
: "border-line text-ink-2 hover:border-line-strong"
1190+
)}
1191+
>
1192+
{sr.metric}
1193+
</button>
1194+
))}
1195+
</div>
1196+
)}
1197+
<ReportChart
1198+
data={active.data}
1199+
chartTitle={active.metric}
1200+
yAxisLabel={active.yAxisLabel}
1201+
chartType={active.chartType ?? "bar"}
1202+
/>
1203+
</div>
1204+
)
1205+
})()}
11391206
</DialogContent>
11401207
</Dialog>
11411208

0 commit comments

Comments
 (0)