|
| 1 | +// anyplot.ai |
| 2 | +// errorbar-basic: Basic Error Bar Plot |
| 3 | +// Library: chartjs 4.4.7 | JavaScript 22.23.0 |
| 4 | +// Quality: 90/100 | Created: 2026-06-30 |
| 5 | + |
| 6 | +const t = window.ANYPLOT_TOKENS; |
| 7 | + |
| 8 | +// Temperature × plant growth study: mean stem length (cm) ± 1 SD across 5 conditions |
| 9 | +const labels = ["15 °C", "20 °C", "25 °C", "30 °C", "35 °C"]; |
| 10 | +const means = [12.3, 18.7, 24.2, 21.5, 16.1]; |
| 11 | +const errors = [2.1, 1.8, 2.3, 2.9, 3.4]; |
| 12 | + |
| 13 | +// Peak at 25°C — full opacity to draw the eye to the inverted-U apex |
| 14 | +const peakIdx = 2; |
| 15 | + |
| 16 | +// Plugin: fill canvas with theme background before anything is drawn |
| 17 | +const bgPlugin = { |
| 18 | + id: "bg", |
| 19 | + beforeDraw(chart) { |
| 20 | + const { ctx } = chart; |
| 21 | + ctx.save(); |
| 22 | + ctx.fillStyle = t.pageBg; |
| 23 | + ctx.fillRect(0, 0, chart.width, chart.height); |
| 24 | + ctx.restore(); |
| 25 | + }, |
| 26 | +}; |
| 27 | + |
| 28 | +// Plugin: draw L-shaped frame (left + bottom spines only) — Chart.js border.display is |
| 29 | +// disabled on both scales so this replaces the default 4-sided box with a clean L-shape |
| 30 | +const spinePlugin = { |
| 31 | + id: "spine", |
| 32 | + afterDraw(chart) { |
| 33 | + const { ctx, chartArea } = chart; |
| 34 | + ctx.save(); |
| 35 | + ctx.strokeStyle = t.ink; |
| 36 | + ctx.lineWidth = 1.5; |
| 37 | + ctx.beginPath(); |
| 38 | + ctx.moveTo(chartArea.left, chartArea.top); |
| 39 | + ctx.lineTo(chartArea.left, chartArea.bottom); |
| 40 | + ctx.lineTo(chartArea.right, chartArea.bottom); |
| 41 | + ctx.stroke(); |
| 42 | + ctx.restore(); |
| 43 | + }, |
| 44 | +}; |
| 45 | + |
| 46 | +// Plugin: I-bar error whiskers drawn over each bar after bars are rendered |
| 47 | +const errorBarPlugin = { |
| 48 | + id: "errorBar", |
| 49 | + afterDatasetsDraw(chart) { |
| 50 | + const { ctx, scales } = chart; |
| 51 | + const meta = chart.getDatasetMeta(0); |
| 52 | + |
| 53 | + ctx.save(); |
| 54 | + ctx.strokeStyle = t.ink; |
| 55 | + ctx.lineWidth = 2.5; |
| 56 | + ctx.lineCap = "square"; |
| 57 | + |
| 58 | + meta.data.forEach((bar, i) => { |
| 59 | + const x = bar.x; |
| 60 | + const CAP = bar.width * 0.35; // cap width proportional to bar width |
| 61 | + const yHi = scales.y.getPixelForValue(means[i] + errors[i]); |
| 62 | + const yLo = scales.y.getPixelForValue(means[i] - errors[i]); |
| 63 | + |
| 64 | + // Vertical stem |
| 65 | + ctx.beginPath(); |
| 66 | + ctx.moveTo(x, yHi); |
| 67 | + ctx.lineTo(x, yLo); |
| 68 | + ctx.stroke(); |
| 69 | + |
| 70 | + // Top cap |
| 71 | + ctx.beginPath(); |
| 72 | + ctx.moveTo(x - CAP, yHi); |
| 73 | + ctx.lineTo(x + CAP, yHi); |
| 74 | + ctx.stroke(); |
| 75 | + |
| 76 | + // Bottom cap |
| 77 | + ctx.beginPath(); |
| 78 | + ctx.moveTo(x - CAP, yLo); |
| 79 | + ctx.lineTo(x + CAP, yLo); |
| 80 | + ctx.stroke(); |
| 81 | + }); |
| 82 | + |
| 83 | + ctx.restore(); |
| 84 | + }, |
| 85 | +}; |
| 86 | + |
| 87 | +// Mount |
| 88 | +const canvas = document.createElement("canvas"); |
| 89 | +document.getElementById("container").appendChild(canvas); |
| 90 | + |
| 91 | +// Chart |
| 92 | +new Chart(canvas, { |
| 93 | + type: "bar", |
| 94 | + plugins: [bgPlugin, spinePlugin, errorBarPlugin], |
| 95 | + data: { |
| 96 | + labels, |
| 97 | + datasets: [ |
| 98 | + { |
| 99 | + label: "Mean stem length (cm)", |
| 100 | + data: means, |
| 101 | + // Peak bar (25°C) at full opacity; remaining bars at 60% to guide attention |
| 102 | + backgroundColor: means.map((_, i) => i === peakIdx ? t.palette[0] : t.palette[0] + "99"), |
| 103 | + borderColor: t.palette[0], |
| 104 | + borderWidth: means.map((_, i) => i === peakIdx ? 3 : 1), |
| 105 | + borderRadius: 3, |
| 106 | + barPercentage: 0.55, |
| 107 | + categoryPercentage: 0.85, |
| 108 | + }, |
| 109 | + ], |
| 110 | + }, |
| 111 | + options: { |
| 112 | + responsive: true, |
| 113 | + maintainAspectRatio: false, |
| 114 | + animation: false, |
| 115 | + layout: { padding: { top: 10, right: 24, bottom: 10, left: 10 } }, |
| 116 | + plugins: { |
| 117 | + title: { |
| 118 | + display: true, |
| 119 | + text: "errorbar-basic · javascript · chartjs · anyplot.ai", |
| 120 | + color: t.ink, |
| 121 | + font: { size: 22, weight: "500" }, |
| 122 | + padding: { top: 12, bottom: 18 }, |
| 123 | + }, |
| 124 | + legend: { |
| 125 | + labels: { color: t.inkSoft, font: { size: 16 }, boxWidth: 14 }, |
| 126 | + }, |
| 127 | + }, |
| 128 | + scales: { |
| 129 | + x: { |
| 130 | + border: { display: false }, // spinePlugin draws the L-frame instead |
| 131 | + ticks: { color: t.inkSoft, font: { size: 14 } }, |
| 132 | + grid: { display: false }, // Y-axis only grid for bar charts |
| 133 | + title: { |
| 134 | + display: true, |
| 135 | + text: "Growth Temperature", |
| 136 | + color: t.ink, |
| 137 | + font: { size: 15 }, |
| 138 | + }, |
| 139 | + }, |
| 140 | + y: { |
| 141 | + border: { display: false }, // spinePlugin draws the L-frame instead |
| 142 | + min: 0, |
| 143 | + suggestedMax: 30, |
| 144 | + ticks: { color: t.inkSoft, font: { size: 14 } }, |
| 145 | + grid: { color: t.grid }, |
| 146 | + title: { |
| 147 | + display: true, |
| 148 | + text: "Stem Length (cm)", |
| 149 | + color: t.ink, |
| 150 | + font: { size: 15 }, |
| 151 | + }, |
| 152 | + }, |
| 153 | + }, |
| 154 | + }, |
| 155 | +}); |
0 commit comments