Skip to content

Commit 73b12ac

Browse files
committed
include charts types in prompt and render the violin plot in right format
1 parent 88820e6 commit 73b12ac

3 files changed

Lines changed: 213 additions & 30 deletions

File tree

backend/tutor_prompts.py

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -356,22 +356,42 @@ def get_component_edit_system_prompt(
356356
Only append a chart tag if the data contains quantitative, plottable numerical values. If the table is purely informational, qualitative, or text-based (e.g., comparing definitions, features, or Yes/No states), generate the Markdown table normally but DO NOT append any chart tag
357357
358358
A.DETERMINE THE BEST CHART TYPE:
359-
Analyze the data you are about to present and decide the most effective way to visualize it. Choose from: Bar, Pie, Line, Scatter, or Area.
359+
Analyze the data you are about to present and decide the most effective way to visualize it. Choose from: Bar, Stacked Bar, Pie, Donut, Line, Area, Stacked Area, Scatter, Bubble, Histogram, Box Plot, Radar, Density Plot, Violin Plot, or Candlestick.
360360
361361
B. STRUCTURE THE TABLE FOR THE CHOSEN CHART:
362362
Format the columns and rows of your Markdown table to match the requirements of the chosen chart type. You are encouraged to use multiple columns for multi-series data, following these rules:
363-
* Bar Chart (Grouped/Stacked): Column 1 must contain categorical labels (X-axis). Column 2 through Column N must contain numerical values representing different data series.
364-
* Line / Area Chart (Multi-line): Column 1 must represent a continuous sequence or time series (e.g., Dates, Years, Months). Column 2 through Column N must contain the corresponding numerical values for different data series.
365-
* Scatter Plot: Column 1 represents the X-axis coordinates. Column 2 represents the Y-axis coordinates for the primary series. Columns 3 through N can represent Y-axis coordinates for additional series, or dimensions like bubble size. All columns must be strictly numerical except for potential data labels.
366-
* Pie Chart: STRICTLY 2 COLUMNS ONLY. Column 1 must contain categorical labels. Column 2 must contain a SINGLE set of numerical values that represent parts of a whole. Do not include multiple data series.
363+
* Bar Chart (Grouped): Column 1 must contain categorical labels (X-axis). Column 2 through Column N must contain numerical values representing different data series. Use when comparing discrete categories side-by-side.
364+
* Stacked Bar Chart: Column 1 must contain categorical labels (X-axis). Column 2 through Column N must contain numerical values for each series that stack to form a total. Use when you want to show both part-to-whole composition AND compare totals across categories.
365+
* Line / Area Chart (Multi-line): Column 1 must represent a continuous sequence or time series (e.g., Dates, Years, Months). Column 2 through Column N must contain the corresponding numerical values for different data series. Use Area when emphasizing volume or cumulative magnitude over time.
366+
* Stacked Area Chart: Column 1 must represent a continuous sequence or time series. Column 2 through Column N must contain numerical values for each series. Values stack on top of each other. Use when showing how multiple series contribute to a changing whole over time.
367+
* Scatter Plot: Column 1 represents the X-axis coordinates. Column 2 represents the Y-axis coordinates for the primary series. Columns 3 through N can represent Y-axis coordinates for additional series. All columns must be strictly numerical. Use when exploring correlation or distribution between two continuous variables.
368+
* Bubble Chart: Column 1 represents the X-axis coordinates (strictly numerical). Column 2 through Column N represent the Y-axis values for each series (strictly numerical). Bubble radius is derived from the magnitude of the Y value. Use when a third dimension (size) reinforces the Y-axis insight.
369+
* Pie Chart: STRICTLY 2 COLUMNS ONLY. Column 1 must contain categorical labels. Column 2 must contain a SINGLE set of numerical values that represent parts of a whole. Do not include multiple data series. Use for simple proportional breakdowns with 2–6 categories.
370+
* Donut Chart: STRICTLY 2 COLUMNS ONLY. Identical structure to Pie Chart — Column 1 contains categorical labels, Column 2 contains a single set of numerical values summing to a whole. Use instead of Pie when you want to emphasize the individual segments over the total, or when a central label would add context.
371+
* Histogram: STRICTLY 2 COLUMNS ONLY. Column 1 must contain bin/range labels (e.g., "0–10", "10–20"). Column 2 must contain the frequency or count for each bin. Use to show the distribution or shape of a single continuous variable.
372+
* Box Plot: EXACTLY 6 COLUMNS. Column 1 must contain category labels. Column 2 = Minimum, Column 3 = Q1 (25th percentile), Column 4 = Median (50th percentile), Column 5 = Q3 (75th percentile), Column 6 = Maximum. All value columns must be strictly numerical. Use to compare statistical spread and skew across categories.
373+
* Radar Chart: Column 1 must contain the dimension/axis labels (e.g., "Speed", "Accuracy", "Recall"). Column 2 through Column N must contain the numerical scores for each entity being compared (one column per entity). Use when comparing multiple entities across 4 or more qualitative dimensions simultaneously.
374+
* Density Plot: Column 1 must represent a continuous variable or X-axis range (e.g., values, scores). Column 2 through Column N must contain the corresponding density or probability values for one or more distributions. Use to visualize the shape of a probability distribution smoothly.
375+
* Violin Plot: Column 1 must contain evenly-spaced numerical Y-axis values representing the measurement range (e.g., "20", "30", "40", "50"). Provide 8–12 rows for a smooth curve. Column 2 through Column N must contain the normalized density or relative frequency at each Y value, one column per category being compared. Values should naturally taper toward zero at the top and bottom rows to represent distribution tails. Use to compare the full distribution shape — not just spread — across multiple categories simultaneously.
376+
* Candlestick Chart: EXACTLY 5 COLUMNS. Column 1 must contain time/date labels (e.g., "Jan", "Feb", "Week 1"). Column 2 = Open, Column 3 = High, Column 4 = Low, Column 5 = Close. All value columns must be strictly numerical. Use exclusively for financial or sequential OHLC (Open-High-Low-Close) data.
367377
368378
C. APPLY THE CHART TAG:
369379
Immediately underneath the generated Markdown table, you must output a specific tag indicating the chart type. Use exactly one of the following tags on its own line:
370380
<<bar>>
381+
<<stacked_bar>>
371382
<<pie>>
383+
<<donut>>
372384
<<line>>
373-
<<scatter>>
374385
<<area>>
386+
<<stacked_area>>
387+
<<scatter>>
388+
<<bubble>>
389+
<<histogram>>
390+
<<box_plot>>
391+
<<radar>>
392+
<<density_plot>>
393+
<<violin_plot>>
394+
<<candlestick>>
375395
376396
### EXAMPLE OUTPUTS ###
377397
@@ -384,15 +404,23 @@ def get_component_edit_system_prompt(
384404
| 2023 | 70000 | 40000 | 60000 |
385405
<<line>>
386406
387-
Example 2 (Multi-Series Bar Chart - 5 Columns):
407+
Example 2 (Grouped Bar Chart - 5 Columns):
388408
| Region | Q1 Sales | Q2 Sales | Q3 Sales | Q4 Sales |
389409
| :--- | :--- | :--- | :--- | :--- |
390410
| North America | 120 | 135 | 140 | 160 |
391411
| Europe | 85 | 90 | 95 | 110 |
392412
| Asia-Pacific | 150 | 160 | 175 | 200 |
393413
<<bar>>
394414
395-
Example 3 (Pie Chart - Strictly 2 Columns):
415+
Example 3 (Stacked Bar Chart - 4 Columns):
416+
| Department | Salaries | Equipment | Overheads |
417+
| :--- | :--- | :--- | :--- |
418+
| Engineering | 500 | 120 | 80 |
419+
| Marketing | 300 | 40 | 60 |
420+
| Operations | 200 | 90 | 110 |
421+
<<stacked_bar>>
422+
423+
Example 4 (Pie Chart - Strictly 2 Columns):
396424
| Energy Source | Percentage (%) |
397425
| :--- | :--- |
398426
| Solar | 45 |
@@ -401,6 +429,99 @@ def get_component_edit_system_prompt(
401429
| Nuclear | 10 |
402430
<<pie>>
403431
432+
Example 5 (Donut Chart - Strictly 2 Columns):
433+
| Browser | Market Share (%) |
434+
| :--- | :--- |
435+
| Chrome | 65 |
436+
| Safari | 19 |
437+
| Firefox | 4 |
438+
| Edge | 4 |
439+
| Other | 8 |
440+
<<donut>>
441+
442+
Example 6 (Stacked Area Chart - 3 Columns):
443+
| Year | Renewable Energy (TWh) | Fossil Fuels (TWh) |
444+
| :--- | :--- | :--- |
445+
| 2018 | 800 | 4200 |
446+
| 2019 | 950 | 4100 |
447+
| 2020 | 1100 | 3900 |
448+
| 2021 | 1350 | 3700 |
449+
| 2022 | 1600 | 3400 |
450+
<<stacked_area>>
451+
452+
Example 7 (Bubble Chart - 3 Columns, size from Y magnitude):
453+
| GDP per Capita ($) | Life Expectancy (years) | Happiness Score |
454+
| :--- | :--- | :--- |
455+
| 8000 | 65 | 4.5 |
456+
| 22000 | 74 | 6.1 |
457+
| 45000 | 80 | 7.3 |
458+
| 62000 | 82 | 7.8 |
459+
<<bubble>>
460+
461+
Example 8 (Histogram - Strictly 2 Columns):
462+
| Score Range | Number of Students |
463+
| :--- | :--- |
464+
| 0–20 | 3 |
465+
| 21–40 | 8 |
466+
| 41–60 | 22 |
467+
| 61–80 | 35 |
468+
| 81–100 | 12 |
469+
<<histogram>>
470+
471+
Example 9 (Box Plot - Exactly 6 Columns):
472+
| Subject | Min | Q1 | Median | Q3 | Max |
473+
| :--- | :--- | :--- | :--- | :--- | :--- |
474+
| Math | 42 | 58 | 72 | 84 | 98 |
475+
| Science | 50 | 65 | 76 | 88 | 99 |
476+
| English | 38 | 55 | 68 | 79 | 95 |
477+
<<box_plot>>
478+
479+
Example 10 (Radar Chart - 4 Columns, 3 dimensions):
480+
| Attribute | Fighter A | Fighter B | Fighter C |
481+
| :--- | :--- | :--- | :--- |
482+
| Speed | 85 | 70 | 90 |
483+
| Strength | 60 | 95 | 55 |
484+
| Endurance | 75 | 80 | 65 |
485+
| Agility | 90 | 60 | 88 |
486+
| Defense | 50 | 88 | 70 |
487+
<<radar>>
488+
489+
Example 11 (Density Plot - 3 Columns):
490+
| Value | Group A Density | Group B Density |
491+
| :--- | :--- | :--- |
492+
| 0 | 0.02 | 0.05 |
493+
| 10 | 0.08 | 0.12 |
494+
| 20 | 0.18 | 0.22 |
495+
| 30 | 0.25 | 0.30 |
496+
| 40 | 0.18 | 0.20 |
497+
| 50 | 0.08 | 0.08 |
498+
| 60 | 0.02 | 0.03 |
499+
<<density_plot>>
500+
501+
Example 12 (Violin Plot - Y-axis density, multi-category):
502+
| Score | Control | Treatment |
503+
| :--- | :--- | :--- |
504+
| 30 | 0.02 | 0.01 |
505+
| 40 | 0.06 | 0.04 |
506+
| 50 | 0.15 | 0.08 |
507+
| 60 | 0.28 | 0.14 |
508+
| 70 | 0.32 | 0.22 |
509+
| 80 | 0.22 | 0.30 |
510+
| 90 | 0.10 | 0.28 |
511+
| 100 | 0.04 | 0.18 |
512+
| 110 | 0.01 | 0.08 |
513+
| 120 | 0.00 | 0.03 |
514+
<<violin_plot>>
515+
516+
Example 13 (Candlestick Chart - Exactly 5 Columns):
517+
| Month | Open | High | Low | Close |
518+
| :--- | :--- | :--- | :--- | :--- |
519+
| Jan | 142 | 158 | 138 | 155 |
520+
| Feb | 155 | 162 | 148 | 150 |
521+
| Mar | 150 | 170 | 145 | 168 |
522+
| Apr | 168 | 175 | 160 | 163 |
523+
<<candlestick>>
524+
404525
**When to call update_learner_profile:**
405526
- Call it proactively when you notice a clear mismatch (don't wait to be asked!)
406527
- If you ask a clarifying question and the user shares useful context (background, goal, constraints, learning preferences), call it immediately to store that context.

frontend/src/components/app/views/BlockLabView.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,18 @@ export function BlockLabView() {
123123
const densBody: string[][] = [["0", "2"], ["1", "8"], ["2", "20"], ["3", "35"], ["4", "28"], ["5", "15"], ["6", "5"], ["7", "2"]]
124124
const sampleDensity = parseGraphFromTable("density_plot", densHeaders, densBody)
125125

126-
const violinHeaders = ["Category", "Spread"]
127-
const violinBody: string[][] = [["Model A", "40"], ["Model B", "65"], ["Model C", "30"], ["Model D", "55"]]
126+
const violinHeaders = ["Score", "Model A", "Model B", "Model C"]
127+
const violinBody: string[][] = [
128+
["10", "0.02", "0.01", "0.04"],
129+
["20", "0.06", "0.03", "0.12"],
130+
["30", "0.14", "0.08", "0.25"],
131+
["40", "0.26", "0.18", "0.32"],
132+
["50", "0.32", "0.30", "0.22"],
133+
["60", "0.26", "0.34", "0.10"],
134+
["70", "0.14", "0.28", "0.04"],
135+
["80", "0.06", "0.14", "0.01"],
136+
["90", "0.02", "0.05", "0.00"],
137+
]
128138
const sampleViolin = parseGraphFromTable("violin_plot", violinHeaders, violinBody)
129139

130140
const errHeaders = ["Trial", "Value", "Error"]

frontend/src/components/block-types/Graph.tsx

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -597,32 +597,84 @@ export const GraphBlock = React.memo(function GraphBlock({
597597

598598
if (chart.type === "violin_plot") {
599599
const labels = chart.labels ?? []
600-
if (!labels.length || chart.series.length === 0) return null
601-
const values = chart.series[0]?.values ?? []
602-
if (!values.length) return null
603-
const maxVal = Math.max(1, ...values)
604-
const groupW = bounds.width / labels.length
600+
if (labels.length < 3 || chart.series.length === 0) return null
601+
602+
// Column 1 = Y-axis numeric positions (parse from string labels)
603+
const yVals: number[] = []
604+
for (const l of labels) {
605+
const n = parseFloat(l.replace(/[^\d.\-eE]/g, ""))
606+
if (!isFinite(n)) return null
607+
yVals.push(n)
608+
}
609+
const yMin = Math.min(...yVals)
610+
const yMax = Math.max(...yVals)
611+
if (yMin === yMax) return null
612+
613+
// Sort by Y value so path draws top-to-bottom correctly
614+
const order = yVals.map((_, i) => i).sort((a, b) => yVals[a] - yVals[b])
615+
const sortedY = order.map(i => yVals[i])
616+
617+
const slotW = bounds.width / chart.series.length
618+
const maxHalfW = slotW * 0.40
619+
const yPos = (v: number) => yFromValue(v, bounds, yMin, yMax)
620+
605621
return (
606622
<>
607-
<line x1={bounds.left} y1={bounds.top + bounds.height} x2={bounds.left + bounds.width} y2={bounds.top + bounds.height} stroke={COLORS.borderLight} />
608-
{labels.map((label, i) => {
609-
const cx = bounds.left + i * groupW + groupW / 2
610-
const halfW = (values[i] / maxVal) * (groupW * 0.4)
611-
const top = bounds.top + bounds.height * 0.1
612-
const mid = bounds.top + bounds.height * 0.5
613-
const bot = bounds.top + bounds.height * 0.85
614-
const qTop = bounds.top + bounds.height * 0.3
615-
const qBot = bounds.top + bounds.height * 0.7
616-
const path = `M ${cx} ${top} C ${cx + halfW * 0.3} ${qTop}, ${cx + halfW} ${mid - 20}, ${cx + halfW} ${mid} C ${cx + halfW} ${mid + 20}, ${cx + halfW * 0.3} ${qBot}, ${cx} ${bot} C ${cx - halfW * 0.3} ${qBot}, ${cx - halfW} ${mid + 20}, ${cx - halfW} ${mid} C ${cx - halfW} ${mid - 20}, ${cx - halfW * 0.3} ${qTop}, ${cx} ${top} Z`
623+
{Array.from({ length: 5 }, (_, i) => {
624+
const y = bounds.top + (bounds.height / 4) * i
625+
return <line key={`g-${i}`} x1={bounds.left} y1={y} x2={bounds.left + bounds.width} y2={y} stroke={COLORS.borderLight} strokeWidth={0.5} />
626+
})}
627+
<line x1={bounds.left} y1={bounds.top} x2={bounds.left} y2={bounds.top + bounds.height} stroke={COLORS.borderMedium} strokeWidth={1} />
628+
{chart.series.map((series, sIdx) => {
629+
const cx = bounds.left + sIdx * slotW + slotW / 2
630+
const rawValues = order.map(i => series.values[i] ?? 0)
631+
const maxDensity = Math.max(1e-9, ...rawValues)
632+
const pts = sortedY.map((_, i) => ({
633+
y: yPos(sortedY[i]),
634+
hw: (rawValues[i] / maxDensity) * maxHalfW,
635+
}))
636+
637+
// Smooth closed path: taper to cx at top and bottom, cubic bezier sides
638+
let d = `M ${cx} ${pts[0].y}`
639+
for (let i = 0; i < pts.length; i++) {
640+
if (i === 0) {
641+
d += ` L ${cx + pts[i].hw} ${pts[i].y}`
642+
} else {
643+
const p0 = pts[i - 1], p1 = pts[i]
644+
const midY = (p0.y + p1.y) / 2
645+
d += ` C ${cx + p0.hw} ${midY} ${cx + p1.hw} ${midY} ${cx + p1.hw} ${p1.y}`
646+
}
647+
}
648+
d += ` L ${cx} ${pts[pts.length - 1].y}`
649+
for (let i = pts.length - 1; i >= 0; i--) {
650+
if (i === pts.length - 1) {
651+
d += ` L ${cx - pts[i].hw} ${pts[i].y}`
652+
} else {
653+
const p0 = pts[i + 1], p1 = pts[i]
654+
const midY = (p0.y + p1.y) / 2
655+
d += ` C ${cx - p0.hw} ${midY} ${cx - p1.hw} ${midY} ${cx - p1.hw} ${p1.y}`
656+
}
657+
}
658+
d += " Z"
659+
660+
const midIdx = Math.floor(pts.length / 2)
661+
const { y: medY, hw: medHW } = pts[midIdx]
617662
return (
618-
<g key={`vio-${i}`}>
619-
<path d={path} fill={colors[0]} fillOpacity={0.3} stroke={colors[0]} strokeWidth={1.5} />
620-
<line x1={cx} y1={top} x2={cx} y2={bot} stroke={colors[0]} strokeWidth={1} strokeDasharray="3,3" />
621-
<circle cx={cx} cy={mid} r={3} fill={colors[0]} />
622-
<text x={cx} y={height - 14} textAnchor="middle" style={{ fontFamily: FONTS.sans, fontSize: "11px", fill: COLORS.textTertiary }}>{label}</text>
663+
<g key={`v-${sIdx}`}>
664+
<path d={d} fill={colors[sIdx]} fillOpacity={0.3} stroke={colors[sIdx]} strokeWidth={1.5} />
665+
<line x1={cx - medHW} y1={medY} x2={cx + medHW} y2={medY} stroke={colors[sIdx]} strokeWidth={2} />
666+
<circle cx={cx} cy={medY} r={3} fill={COLORS.bg} stroke={colors[sIdx]} strokeWidth={2} />
667+
<text x={cx} y={height - 14} textAnchor="middle" style={{ fontFamily: FONTS.sans, fontSize: "11px", fill: COLORS.textTertiary }}>
668+
{series.name}
669+
</text>
623670
</g>
624671
)
625672
})}
673+
{[yMin, (yMin + yMax) / 2, yMax].map((val, i) => (
674+
<text key={`yl-${i}`} x={bounds.left - 6} y={yPos(val)} textAnchor="end" dominantBaseline="middle" style={{ fontFamily: FONTS.sans, fontSize: "10px", fill: COLORS.textTertiary }}>
675+
{Number.isInteger(val) ? val : val.toFixed(1)}
676+
</text>
677+
))}
626678
</>
627679
)
628680
}

0 commit comments

Comments
 (0)