My Hands-On Review: JavaScript Scatter Charts That Actually Worked For Me

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,