I’m Kayla. I live in hoodies, coffee, and messy CSV files. Last month, I needed a scatter chart that told a simple story fast. I tried three tools in JavaScript: Chart.js, D3, and ECharts. I didn’t just read docs. I built real charts for work, with real data, on a real deadline. You know what? Each one had a mood. If you're curious about the full narrative behind that experiment, I put together my hands-on review of JavaScript scatter charts that actually worked for me with even more screenshots and code.
Chart.js — “I need something clean in 10 minutes”
I started with Chart.js because I needed a quick scatter for a sprint review. Steps vs calories from my team’s wellness challenge. Simple and neat. I later revisited Chart.js alongside a handful of other open-source projects in this deep dive into JavaScript chart tools that actually worked.
What I liked:
- It was fast to set up.
- Tooltips looked good right away.
- I could add a trend line with a tiny helper.
What bugged me:
- With thousands of points, it felt slow unless I tweaked it.
- Styling small bits took more clicks than I wanted.
Real example I used (Steps vs Calories):
<canvas id="scatter" width="600" height="350"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Tiny data sample (I had ~2,400 rows in real life)
const points = [
{ x: 3200, y: 220 }, { x: 5400, y: 310 }, { x: 8000, y: 420 },
{ x: 12000, y: 560 }, { x: 4000, y: 260 }, { x: 10000, y: 500 }
];
// Simple linear regression to draw a trend line
function linReg(data) {
const n = data.length;
const sx = data.reduce((s, d) => s + d.x, 0);
const sy = data.reduce((s, d) => s + d.y, 0);
const sxy = data.reduce((s, d) => s + d.x * d.y, 0);
const sxx = data.reduce((s, d) => s + d.x * d.x, 0);
const m = (n * sxy - sx * sy) / (n * sxx - sx * sx);
const b = (sy - m * sx) / n;
const xs = points.map(p => p.x);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
return [
{ x: minX, y: m * minX + b },
{ x: maxX, y: m * maxX + b }
];
}
const ctx = document.getElementById('scatter').getContext('2d');
new Chart(ctx, {
type: 'scatter',
data: {
datasets: [
{
label: 'Steps vs Calories',
data: points,
pointRadius: 3,
backgroundColor: 'rgba(54,162,235,0.7)'
},
{
type: 'line',
label: 'Trend',
data: linReg(points),
borderColor: 'rgba(255,99,132,0.9)',
borderWidth: 2,
pointRadius: 0
}
]
},
options: {
parsing: false,
plugins: {
tooltip: {
callbacks: {
label: ctx => `Steps: ${ctx.parsed.x}, Cal: ${ctx.parsed.y}`
}
}
},
scales: {
x: { title: { display: true, text: 'Steps' } },
y: { title: { display: true, text: 'Calories' } }
}
}
});
</script>
A tiny tip: with bigger data (I had 10k points one day), I set pointRadius: 1, used parsing: false, and kept the canvas size steady. That kept it smooth enough to show my manager without panic.
Also, my cat walked on the keyboard while I was testing tooltips. No harm done. Good sign.
D3 — “I want full control, even if it takes time”
D3 felt like clay. It let me shape the scatter just the way I wanted. I used it for a marketing report: sessions vs conversion rate, colored by channel. I added jitter, a trend line, and custom axes. It took longer. But it looked like me, not a template. For the curious, I documented the parts that clicked—and the parts that didn't—in a dedicated write-up on building charts with D3.js.
What I liked:
- Full control over scales, ticks, and colors.
- Easy to add a custom trend line.
- Great for teaching why a chart says what it says.
What bugged me:
- More code, more care.
- You own the little things (legends, tooltips).
Real example I shipped (Sessions vs Conversion Rate):
<div id="d3-scatter"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script>
const data = [
{ x: 500, y: 0.7, channel: 'Email' },
{ x: 1200, y: 1.2, channel: 'Search' },
{ x: 3000, y: 1.1, channel: 'Social' },
{ x: 800, y: 0.6, channel: 'Email' },
{ x: 2200, y: 0.9, channel: 'Search' }
];
const w = 640, h = 420, m = { t: 20, r: 20, b: 40, l: 50 };
const svg = d3.select('#d3-scatter').append('svg')
.attr('width', w).attr('height', h);
const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.x)).nice()
.range([m.l, w - m.r]);
const y = d3.scaleLinear()
.domain(d3.extent(data, d => d.y)).nice()
.range([h - m.b, m.t]);
const color = d3.scaleOrdinal()
.domain(['Email','Search','Social'])
.range(['#1f77b4','#2ca02c','#ff7f0e']);
svg.append('g')
.attr('transform', `translate(0,${h - m.b})`)
.call(d3.axisBottom(x));
svg.append('g')
.attr('transform', `translate(${m.l},0)`)
.call(d3.axisLeft(y));
svg.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y))
.attr('r', 4)
.attr('fill', d => color(d.channel))
.attr('opacity', 0.7);
// Simple linear regression for trend line
function linReg(points) {
const n = points.length;
const sx = d3.sum(points, p => p.x);
const sy = d3.sum(points, p => p.y);
const sxy = d3.sum(points, p => p.x * p.y);
const sxx = d3.sum(points, p => p.x * p.x);
const m = (n * sxy - sx * sy) / (n * sxx - sx * sx);
const b = (sy - m * sx) / n;
const xs = d3.extent(points, p => p.x);
return [{ x: xs[0], y: m * xs[0] + b }, { x: xs[1], y: m * xs[1] + b }];
}
const trend = linReg(data);
svg.append('line')
.attr('x1', x(trend[0].x))
.attr('y1', y(trend[0].y))
.attr('x2', x(trend[1].x))
.attr('y2', y(trend[1].y))
.attr('stroke', '#d62728')
.attr('stroke-width', 2);
</script>
I used tiny hover cards with title tags on circles for quick hints. Not fancy, but it worked during a live call. Honestly, the trend line stole the show.
ECharts — “Bring on the big data”
I reached for ECharts when I had 50k+ points from a log export. Time vs payload size. I needed pan and zoom that felt smooth. Also, I wanted brush select so my teammate could grab clusters like a lasso.
What I liked:
- Big data felt okay with canvas and clever tricks.
- Zoom, brush,
