Skip to content

Commit

Permalink
post scatter drag
Browse files Browse the repository at this point in the history
  • Loading branch information
holtzy committed Jan 9, 2025
1 parent 86eff02 commit 03fdd50
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 102 deletions.
129 changes: 70 additions & 59 deletions pages/example/scatterplot-r2-playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,94 +13,105 @@ import { ScatterplotR2PlaygroundDemo } from '@/viz/ScatterplotR2Playground/Scatt
const graphDescription = (
<>
<p>
This tutorial is a variation around the general{' '}
<Link href="/scatter-plot">introduction to scatterplot with react</Link>{' '}
and d3.js. You should probably understand the concepts described there
before reading here.
<b></b> and <b>correlation</b> are often seen as definitive measures to
validate the relationship between two variables.
</p>
<p>
This post features an interactive sandbox that explores several edge
cases, demonstrating how relying on these summary statistics without
visualizing the data can be <b>dangerously misleading</b>.
</p>
<p>Talk about r2 and why it is confusing</p>
</>
);

export default function Home() {
return (
<Layout
title="Scatterplot, r2, and misleading results"
seoDescription="Learn how to build a scatterplot rendered in canvas with React for better performance. Code and explanation provided."
title="Why R² Alone Fails"
seoDescription="An interactive scatterplot that lets you move dots and instantly see the R² update in real time, revealing just how misleading this metric can be without proper context."
>
<TitleAndDescription
title="Scatterplot, r2, and misleading results"
title="Why R² Alone Fails"
description={graphDescription}
chartType="scatter"
/>

{/*
//
// What is R2
//
*/}
<h2 id="definition">🤔 What are R2 and correlation?</h2>
<h3>&rarr; r2</h3>
<p>
R², or the{' '}
<a
href="https://en.wikipedia.org/wiki/Coefficient_of_determination"
target="_blank"
>
coefficient of determination
</a>
, measures the <b>proportion of variance</b> in the <u>dep</u>endent
variable that is explained by the <u>indep</u>endent variable in a
regression model.
</p>
<p>
It ranges from <code>0</code> to <code>1</code>, with higher values
indicating a stronger linear relationship.
</p>

<h3>&rarr; correlation</h3>
<p>
The{' '}
<a
href="https://en.wikipedia.org/wiki/Correlation_coefficient"
target="_blank"
>
correlation coefficient
</a>{' '}
(<code>r</code>) measures the <b>strength</b> and <b>direction</b> of a
linear relationship between two variables, ranging from <code>-1</code>{' '}
to <code>1</code>. R² is actually the square of the correlation
coefficient in a simple linear regression!
</p>
<p>
The correlation describes the <b>relationship</b> directly, R² focuses
on the <b>explanatory power </b>of a regression model.
</p>

{/*
//
// Plot and code
//
*/}
<h2 id="Implementation">Scatterplot canvas implementation</h2>
<p>The trick here is to use 2 layers of drawing:</p>
<ul>
<li>
The first layer is for the <b>axes</b>. It is an SVG element that will
add the X and Y axes using some usual <code>AxisLeft</code> and{' '}
<code>AxisBottom</code> components.
</li>
<li>
The second layer is for the <b>markers</b>, it is the{' '}
<code>canvas</code> element. It has a <code>ref</code>. We can then
call a function in a <code>useEffect</code> hook to draw inside this
canvas element.
</li>
</ul>
<h2 id="sandbox">🎮 Scatterplot, R², and Draggable Circles</h2>
<p>
Summary statistics are popular because they condense large datasets into
a few <b>easy-to-understand numbers</b>. However, relying solely on them
can lead to a <b>false sense of clarity</b>.
</p>
<p>
The graph below showcases datasets with high R² and correlation values,
even when there's clearly <b>no meaningful relationship</b> between x
and y.
</p>
<p>
Bonus: the circles are <b>draggable</b>! Experiment by moving them
around and watch how the R² and correlation change in real time. It’s a
great way to build intuition about these metrics.
</p>

<ChartOrSandbox
vizName={'ScatterplotR2PlaygroundDemo'}
VizComponent={ScatterplotR2PlaygroundDemo}
maxWidth={500}
height={500}
height={580}
caption="A scatterplot made with React, using SVG for the axes and Canvas for the markers to improve performance."
/>
<p>
<br />
<br />
</p>
<p>
Canvas is an important topic in data visualization for the web. I plan
to write complete articles on the topic. You can know when it's ready by{' '}
<Link href="/subscribe">subscribing</Link> to the project.
</p>
<LinkAsButton size="sm" isFilled href="/subscribe">
{'Tell me when the canvas post is ready!'}
</LinkAsButton>

<div className="full-bleed border-t h-0 bg-gray-100 mb-3 mt-24" />
<ChartFamilySection chartFamily="flow" />
<div className="mt-20" />
</Layout>
);
}

const snippetFunction = `
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const ctx = canvas.getContext('2d');
// Clear the canvas
ctx.clearRect(0, 0, width, height);
// Draw each data point as a circle
data.forEach((point) => {
ctx.beginPath();
ctx.arc(xScale(point.x), yScale(point.y), CIRCLE_RADIUS, 0, 2 * Math.PI);
ctx.globalAlpha = 0.5;
ctx.fillStyle = '#cb1dd1';
ctx.fill();
});
}, [data, xScale, yScale, width, height]);
`.trim();
44 changes: 44 additions & 0 deletions viz/ScatterplotR2Playground/Circle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { animated, useSpring } from 'react-spring';

type CircleVizProps = {
r: number;
cx: number;
cy: number;
fill: string;
stroke: string;
fillOpacity: number;
strokeWidth: number;
onMouseDown: () => void;
onMouseUp: () => void;
};

export const Circle = ({
r,
cx,
cy,
fill,
stroke,
fillOpacity,
strokeWidth,
onMouseDown,
onMouseUp,
}: CircleVizProps) => {
const springProps = useSpring({
to: { r, cx, cy, fill, stroke, fillOpacity, strokeWidth },
});

return (
<animated.circle
cursor={'pointer'}
strokeWidth={springProps.strokeWidth}
fillOpacity={springProps.fillOpacity}
r={springProps.r}
cy={springProps.cy}
cx={springProps.cx}
stroke={springProps.stroke}
fill={springProps.fill}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
/>
);
};
24 changes: 24 additions & 0 deletions viz/ScatterplotR2Playground/Line.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { animated, useSpring } from 'react-spring';

type LineVizProps = {
x1: number;
y1: number;
x2: number;
y2: number;
};

export const Line = ({ x1, y1, x2, y2 }: LineVizProps) => {
const springProps = useSpring({
to: { x1, y1, x2, y2 },
});

return (
<animated.line
x1={springProps.x1}
y1={springProps.y1}
x2={springProps.x2}
y2={springProps.y2}
stroke="black"
/>
);
};
51 changes: 35 additions & 16 deletions viz/ScatterplotR2Playground/Scatterplot.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as d3 from 'd3';
import { AxisLeft } from './AxisLeft';
import { AxisBottom } from './AxisBottom';
import { MouseEventHandler, useMemo, useState } from 'react';
import { MouseEventHandler, useCallback, useMemo, useState } from 'react';
import { Circle } from './Circle';
import { Line } from './Line';

const MARGIN = { top: 60, right: 60, bottom: 60, left: 60 };

Expand Down Expand Up @@ -30,11 +32,11 @@ export const Scatterplot = ({
// Scales
const yScale = useMemo(() => {
return d3.scaleLinear().domain([0, 10]).range([boundsHeight, 0]);
}, []);
}, [height]);

const xScale = useMemo(() => {
return d3.scaleLinear().domain([0, 10]).range([0, boundsWidth]);
}, []);
}, [width]);

// Compute regression parameters
const xMean = d3.mean(data, (d) => d.x) || 0;
Expand All @@ -56,10 +58,21 @@ export const Scatterplot = ({
const x2 = 10;
const y2 = slope * x2 + intercept;

const handleMouseDown = (index: number) => {
// COEFFICIENT OF CORRELATION
const numerator = d3.sum(data, (d) => (d.x - xMean) * (d.y - yMean)); // Numerator: Covariance of x and y

// Denominator: Product of standard deviations of x and y
const xVariance = d3.sum(data, (d) => (d.x - xMean) ** 2);
const yVariance = d3.sum(data, (d) => (d.y - yMean) ** 2);
const denominator = Math.sqrt(xVariance * yVariance);

// Correlation coefficient
const correlationCoefficient = numerator / denominator;

const handleMouseDown = useCallback((index: number) => {
setIsDragging(true);
setDraggedPointIndex(index);
};
}, []);

const handleMouseMove: MouseEventHandler<SVGRectElement> = (event) => {
if (!isDragging) {
Expand All @@ -82,15 +95,15 @@ export const Scatterplot = ({
setData(newData);
};

const handleMouseUp = () => {
const handleMouseUp = useCallback(() => {
setIsDragging(false);
setDraggedPointIndex(null);
};
}, []);

// Build shapes
const allShapes = data.map((d, i) => {
return (
<circle
<Circle
key={i}
r={i === draggedPointIndex ? 12 : 8}
cx={xScale(d.x)}
Expand All @@ -107,7 +120,7 @@ export const Scatterplot = ({
});

return (
<div>
<div className="relative">
<svg width={width} height={height}>
<g
width={boundsWidth}
Expand Down Expand Up @@ -142,22 +155,28 @@ export const Scatterplot = ({
/>
</g>

{/* Circles */}
{allShapes}

{/* Regression line */}
<line
<Line
x1={xScale(x1)}
y1={yScale(y1)}
x2={xScale(x2)}
y2={yScale(y2)}
stroke="blue"
strokeWidth={2}
/>

{/* Circles */}
{allShapes}
</g>
</svg>

<p>R²: {r2.toFixed(3)}</p>
<div
className="absolute inset-0 w-full flex justify-center gap-2"
style={{ height: 60 }}
>
<p style={{ opacity: r2 + 0.2 }} className="text-md">
R² &rarr; <code className="mr-5">{r2.toFixed(3)}</code> Correlation
&rarr; <code>{correlationCoefficient.toFixed(3)}</code>
</p>
</div>
</div>
);
};
Loading

0 comments on commit 03fdd50

Please sign in to comment.