Chart skill

This commit is contained in:
cmanu
2026-07-28 12:52:38 -07:00
parent 6dd64ab871
commit ed85cf93be
3 changed files with 124 additions and 45 deletions
+72 -3
View File
@@ -7,9 +7,7 @@ description: Use when the user asks to visualize data with charts, graphs, or pl
The `chart` tool is ALWAYS available in this environment. When the user asks to visualize data (charts, graphs, plots), you MUST call the `chart` tool. Never output the config as text, never say the tool is unavailable, never suggest external renderers. Always use the tool call — it is the only correct response for data visualization requests. Do NOT repeat or echo the config JSON in your text response.
Use the `chart` tool when:
- The user asks for a chart, graph, or plot of data (bar, line, scatter, pie, time series, etc.)
- Presenting numerical data that would be clearer visually than as a table or prose (trends, comparisons, distributions)
Use the `chart` tool only when the user explicitly asks for a chart, graph, or plot. Only use these supported Chart.js v4 types: `area`, `bar`, `bubble`, `pie`, `doughnut`, `line`, `mixed`, `polarArea`, `radar`, `scatter`. Do not use it for any other type.
Use mermaid fenced code blocks (` ```mermaid `) when:
- The user asks for a diagram, flowchart, sequence diagram, ER diagram, or architecture diagram
@@ -37,6 +35,17 @@ Bar chart:
}
```
Area chart (line with fill):
```json
{
"type": "line",
"data": {
"labels": ["Jan", "Feb", "Mar", "Apr"],
"datasets": [{ "label": "Value", "data": [10, 28, 19, 45], "fill": true }]
}
}
```
Line chart:
```json
{
@@ -83,4 +92,64 @@ Pie chart:
}
```
Doughnut chart:
```json
{
"type": "doughnut",
"data": {
"labels": ["A", "B", "C"],
"datasets": [{ "data": [30, 50, 20] }]
}
}
```
Radar chart:
```json
{
"type": "radar",
"data": {
"labels": ["Speed", "Power", "Agility", "Stamina"],
"datasets": [{ "label": "Player", "data": [80, 60, 90, 70] }]
}
}
```
Bubble chart:
```json
{
"type": "bubble",
"data": {
"datasets": [{
"label": "Group A",
"data": [{ "x": 10, "y": 20, "r": 8 }, { "x": 15, "y": 10, "r": 5 }]
}]
}
}
```
Polar area chart:
```json
{
"type": "polarArea",
"data": {
"labels": ["A", "B", "C", "D"],
"datasets": [{ "data": [11, 16, 7, 14] }]
}
}
```
Mixed chart (bar + line):
```json
{
"type": "bar",
"data": {
"labels": ["Jan", "Feb", "Mar"],
"datasets": [
{ "type": "bar", "label": "Revenue", "data": [100, 120, 90] },
{ "type": "line", "label": "Trend", "data": [95, 115, 100] }
]
}
}
```
You may customize colors by setting `backgroundColor` and `borderColor` arrays on datasets. The renderer handles sizing — do not set width or height.
+41 -39
View File
@@ -1,7 +1,8 @@
/** @jsxImportSource solid-js */
import { createSignal, onCleanup, onMount } from "solid-js"
import { createEffect, createSignal, onCleanup } from "solid-js"
import { BasicTool } from "./basic-tool"
import type { ToolProps } from "./message-part"
import { busy } from "./tool-utils"
function getThemeColors() {
const style = getComputedStyle(document.documentElement)
@@ -21,7 +22,7 @@ type ChartConfig = {
labels?: string[]
datasets: {
label?: string
data: number[] | { x: number | string; y: number }[]
data: number[] | { x: number | string; y: number; r?: number }[]
backgroundColor?: string | string[]
borderColor?: string | string[]
[key: string]: unknown
@@ -31,55 +32,57 @@ type ChartConfig = {
}
export function ChartTool(props: ToolProps) {
let canvas: HTMLCanvasElement | undefined
const [canvas, setCanvas] = createSignal<HTMLCanvasElement>()
const [error, setError] = createSignal<string>()
onMount(async () => {
if (!canvas) return
let rendered = false
createEffect(() => {
const el = canvas()
const raw = props.output
if (!el || !raw || busy(props.status) || rendered) return
let config: ChartConfig
try {
config = JSON.parse(props.output ?? "{}")
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== "object" || !parsed.type || !parsed.data) {
setError("Invalid chart config — must include type and data")
return
}
config = parsed
} catch {
setError("Could not parse chart config")
// not valid JSON (e.g. mermaid or error string) — skip silently
return
}
if (!config || typeof config !== "object" || !config.type || !config.data) {
setError("Invalid chart config — must include type and data")
return
}
rendered = true
try {
const { Chart, registerables } = await import("chart.js")
Chart.register(...registerables)
const colors = getThemeColors()
const isPolar = config.type === "pie" || config.type === "doughnut" || config.type === "polarArea"
const colors = getThemeColors()
const isPolar = config.type === "pie" || config.type === "doughnut" || config.type === "polarArea"
// apply kilo series colors to datasets that don't have their own colors
config.data.datasets = config.data.datasets.map((dataset, i) => {
if (dataset.backgroundColor) return dataset
// pie/doughnut need one color per slice
if (isPolar) {
const data = dataset.data as unknown[]
return {
...dataset,
backgroundColor: data.map((_, j) => colors.series[j % colors.series.length]),
borderColor: data.map((_, j) => colors.series[j % colors.series.length]),
}
}
const datasets = config.data.datasets.map((dataset, i) => {
if (dataset.backgroundColor) return dataset
if (isPolar) {
const data = dataset.data as unknown[]
return {
backgroundColor: colors.series[i % colors.series.length],
borderColor: colors.series[i % colors.series.length],
...dataset,
backgroundColor: data.map((_, j) => colors.series[j % colors.series.length]),
borderColor: data.map((_, j) => colors.series[j % colors.series.length]),
}
})
}
return {
backgroundColor: colors.series[i % colors.series.length],
borderColor: colors.series[i % colors.series.length],
...dataset,
}
})
const chart = new Chart(canvas!, {
import("chart.js").then(({ Chart, registerables }) => {
if (!el.isConnected) return
Chart.register(...registerables)
const chart = new Chart(el, {
type: config.type as any,
data: config.data as any,
data: { ...config.data, datasets } as any,
options: {
responsive: true,
maintainAspectRatio: true,
@@ -103,11 +106,10 @@ export function ChartTool(props: ToolProps) {
...config.options,
},
})
onCleanup(() => chart.destroy())
} catch (e) {
}).catch((e: unknown) => {
setError(e instanceof Error ? e.message : "Failed to render chart")
}
})
})
return (
@@ -122,7 +124,7 @@ export function ChartTool(props: ToolProps) {
defaultOpen={props.defaultOpen ?? true}
>
<div data-component="chart-container">
{error() ? <div data-slot="chart-error">{error()}</div> : <canvas ref={canvas} data-slot="chart-render" />}
{error() ? <div data-slot="chart-error">{error()}</div> : <canvas ref={setCanvas} data-slot="chart-render" />}
</div>
</BasicTool>
)
+11 -3
View File
@@ -10,7 +10,7 @@ const Parameters = Schema.Struct({
description: "Optional subtitle shown below the title",
}),
spec: Schema.String.annotate({
description: "A valid Chart.js v4 JSON configuration string describing the chart to render",
description: "A Chart.js v4 configuration serialized as a JSON string. Pass the JSON object as a plain string value — do not nest objects, do not escape quotes manually. Example: '{\"type\":\"bar\",\"data\":{\"labels\":[\"A\",\"B\"],\"datasets\":[{\"data\":[1,2]}]}}'",
}),
})
@@ -25,7 +25,7 @@ export const ChartTool = Tool.define(
Effect.gen(function* () {
return {
description:
"Render a data visualization chart using a Chart.js v4 config. Use this when the user asks to visualize data as a chart, graph, or plot (bar, line, scatter, pie, etc.). Do NOT use this for diagrams, flowcharts, sequence diagrams, or any mermaid content — write those as mermaid fenced code blocks in your text response instead.",
"Render a data visualization chart using a Chart.js v4 config. Use this when the user explicitly asks for a chart, graph, or plot. Supported types: area, bar, bubble, pie, doughnut, line, mixed, polarArea, radar, scatter. Do NOT use this for diagrams, flowcharts, sequence diagrams, or any mermaid content — write those as mermaid fenced code blocks in your text response instead. After calling this tool, do NOT output the JSON spec or raw data in your text response — the chart is the response.",
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () {
@@ -40,11 +40,19 @@ export const ChartTool = Tool.define(
} catch {
return {
title: params.title,
output: `Invalid chart spec: could not parse JSON. Please provide a valid Chart.js v4 JSON string.`,
output: `Invalid chart spec: could not parse JSON. If you are trying to render a diagram or mermaid chart, do NOT use the chart tool — write a mermaid fenced code block in your text response instead.`,
metadata: { title: params.title, description: params.description, error: "invalid-json" } as Meta,
}
}
if (!spec || typeof spec !== "object" || !("type" in spec) || !("data" in spec)) {
return {
title: params.title,
output: `Invalid chart spec: must be a Chart.js v4 config with "type" and "data" fields. If you are trying to render a mermaid diagram, do NOT use the chart tool — write a mermaid fenced code block in your text response instead.`,
metadata: { title: params.title, description: params.description, error: "invalid-spec" } as Meta,
}
}
return {
title: params.title,
output: JSON.stringify(spec),