Skip to content

Commit

Permalink
create component for exercise scale
Browse files Browse the repository at this point in the history
  • Loading branch information
holtzy committed Sep 27, 2024
1 parent be7769e commit f4116fa
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 130 deletions.
138 changes: 138 additions & 0 deletions pages/course/scales/CircleScaleExercise.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Button } from '@/component/UI/button';
import { useCallback, useState } from 'react';

export const CircleScaleExercise = () => {
// Initial positions of the circles
const [circles, setCircles] = useState([
{ id: 1, cx: 200, value: 0 },
{ id: 2, cx: 220, value: 50 },
{ id: 3, cx: 240, value: 60 },
{ id: 4, cx: 260, value: 82 },
{ id: 5, cx: 280, value: 100 },
]);
const [draggingCircleId, setDraggingCircleId] = useState(null);

// Handle mouse down event to start dragging
const handleMouseDown = useCallback((e, id) => {
setDraggingCircleId(id);
}, []);

// Handle mouse move event to update circle's position
const handleMouseMove = useCallback(
(e) => {
if (draggingCircleId !== null) {
const svgRect = e.currentTarget.getBoundingClientRect();
const newCx = e.clientX - svgRect.left;
const boundedCx = Math.min(Math.max(newCx, 0), 500);

setCircles((prevCircles) =>
prevCircles.map((circle) =>
circle.id === draggingCircleId
? { ...circle, cx: boundedCx }
: circle
)
);
}
},
[draggingCircleId]
);

// Handle mouse up event to stop dragging
const handleMouseUp = useCallback(() => {
setDraggingCircleId(null);
}, []);

return (
<>
<div className="mx-auto">
<svg
width={500}
height={400}
overflow={'visible'}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseDown={(e) => e.preventDefault()} // Prevent default behavior to avoid unwanted text selection
onMouseEnter={() => setDraggingCircleId(null)}
>
{circles.map((circle) => (
<g key={circle.id}>
<circle
cx={circle.cx}
cy={140}
r={30}
fill="#69b3a2"
stroke="black"
fillOpacity={1}
onMouseDown={(e) => handleMouseDown(e, circle.id)}
cursor={'pointer'}
/>
<text
x={circle.cx}
y={140}
textAnchor="middle"
alignmentBaseline="central"
fontSize={12}
cursor={'pointer'}
pointerEvents={'none'}
>
{circle.value}
</text>
</g>
))}

{/* Annotation */}
<line x1={0} x2={500} y1={200} y2={200} stroke="black" />

<line x1={0} x2={0} y1={200} y2={200 + 5} stroke="black" />
<text
x={0}
y={200 + 20}
textAnchor="middle"
fill="black"
fontSize={14}
>
0 px
</text>

<line x1={250} x2={250} y1={200} y2={200 + 5} stroke="black" />
<text
x={250}
y={200 + 20}
textAnchor="middle"
fill="black"
fontSize={14}
>
250 px
</text>

<line x1={500} x2={500} y1={200} y2={200 + 5} stroke="black" />
<text
x={500}
y={200 + 20}
textAnchor="middle"
fill="black"
fontSize={14}
>
500 px
</text>
</svg>
</div>

<div>
<Button
onClick={() => {
setCircles([
{ id: 1, cx: 0, value: 0 },
{ id: 2, cx: 500 / 2, value: 50 },
{ id: 3, cx: (60 / 100) * 500, value: 60 },
{ id: 4, cx: (82 / 100) * 500, value: 82 },
{ id: 5, cx: 500, value: 100 },
]);
}}
>
Show right positions
</Button>
</div>
</>
);
};
132 changes: 3 additions & 129 deletions pages/course/scales/introduction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,16 @@ import { LayoutCourse } from '@/component/LayoutCourse';
import { lessonList } from '@/util/lessonList';
import { CodeBlock } from '@/component/UI/CodeBlock';
import { Button } from '@/component/UI/button';
import { CircleScaleExercise } from './CircleScaleExercise';

const previousURL = '/course/svg/path-element';
const currentURL = '/course/scales/introduction';
const nextURL = undefined;
const nextURL = '/course/scales/linear-scale';
const seoDescription = '';

export default function Home() {
const currentLesson = lessonList.find((l) => l.link === currentURL);

// Initial positions of the circles
const [circles, setCircles] = useState([
{ id: 1, cx: 200, value: 0 },
{ id: 2, cx: 220, value: 50 },
{ id: 3, cx: 240, value: 60 },
{ id: 4, cx: 260, value: 82 },
{ id: 5, cx: 280, value: 100 },
]);
const [draggingCircleId, setDraggingCircleId] = useState(null);

// Handle mouse down event to start dragging
const handleMouseDown = useCallback((e, id) => {
setDraggingCircleId(id);
}, []);

// Handle mouse move event to update circle's position
const handleMouseMove = useCallback(
(e) => {
if (draggingCircleId !== null) {
const svgRect = e.currentTarget.getBoundingClientRect();
const newCx = e.clientX - svgRect.left;
const boundedCx = Math.min(Math.max(newCx, 0), 500);

setCircles((prevCircles) =>
prevCircles.map((circle) =>
circle.id === draggingCircleId
? { ...circle, cx: boundedCx }
: circle
)
);
}
},
[draggingCircleId]
);

// Handle mouse up event to stop dragging
const handleMouseUp = useCallback(() => {
setDraggingCircleId(null);
}, []);

return (
<LayoutCourse
title={currentLesson.name}
Expand Down Expand Up @@ -103,95 +64,8 @@ export default function Home() {
<p className="text-xs">
Note: the number in each circle represents its value in the dataset.
</p>
<div className="mx-auto">
<svg
width={500}
height={400}
overflow={'visible'}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseDown={(e) => e.preventDefault()} // Prevent default behavior to avoid unwanted text selection
onMouseEnter={() => setDraggingCircleId(null)}
>
{circles.map((circle) => (
<g key={circle.id}>
<circle
cx={circle.cx}
cy={140}
r={30}
fill="#69b3a2"
stroke="black"
fillOpacity={1}
onMouseDown={(e) => handleMouseDown(e, circle.id)}
cursor={'pointer'}
/>
<text
x={circle.cx}
y={140}
textAnchor="middle"
alignmentBaseline="central"
fontSize={12}
cursor={'pointer'}
pointerEvents={'none'}
>
{circle.value}
</text>
</g>
))}

{/* Annotation */}
<line x1={0} x2={500} y1={200} y2={200} stroke="black" />

<line x1={0} x2={0} y1={200} y2={200 + 5} stroke="black" />
<text
x={0}
y={200 + 20}
textAnchor="middle"
fill="black"
fontSize={14}
>
0 px
</text>

<line x1={250} x2={250} y1={200} y2={200 + 5} stroke="black" />
<text
x={250}
y={200 + 20}
textAnchor="middle"
fill="black"
fontSize={14}
>
250 px
</text>

<line x1={500} x2={500} y1={200} y2={200 + 5} stroke="black" />
<text
x={500}
y={200 + 20}
textAnchor="middle"
fill="black"
fontSize={14}
>
500 px
</text>
</svg>
</div>

<div>
<Button
onClick={() => {
setCircles([
{ id: 1, cx: 0, value: 0 },
{ id: 2, cx: 500 / 2, value: 50 },
{ id: 3, cx: (60 / 100) * 500, value: 60 },
{ id: 4, cx: (82 / 100) * 500, value: 82 },
{ id: 5, cx: 500, value: 100 },
]);
}}
>
Show right positions
</Button>
</div>
<CircleScaleExercise />

<h2>How it actually works</h2>

Expand Down
95 changes: 95 additions & 0 deletions pages/course/scales/linear-scale.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useCallback, useState } from 'react';
import TitleAndDescription from '@/component/TitleAndDescription';
import { LayoutCourse } from '@/component/LayoutCourse';
import { lessonList } from '@/util/lessonList';
import { CodeBlock } from '@/component/UI/CodeBlock';
import { Button } from '@/component/UI/button';
import Link from 'next/link';
import { CircleScaleExercise } from './CircleScaleExercise';

const previousURL = '/course/scales/introduction';
const currentURL = '/course/scales/linear-scale';
const nextURL = '/course/scales/other-scale';
const seoDescription = '';

export default function Home() {
const currentLesson = lessonList.find((l) => l.link === currentURL);

return (
<LayoutCourse
title={currentLesson.name}
seoDescription={seoDescription}
nextTocItem={lessonList.find((l) => l.link === nextURL)}
previousTocItem={lessonList.find((l) => l.link === previousURL)}
>
<TitleAndDescription
title={currentLesson.name}
lessonStatus={currentLesson.status}
readTime={currentLesson.readTime}
selectedLesson={currentLesson}
description={
<>
<p>
The previous lesson described the concept of{' '}
<Link href="/course/scales/introduction">scale</Link> in data
visualization. Scales allow, for instance, to translate a value in
our dataset to a position on the screen.
</p>
<p>
Now, let's study the most common scale type and its d3.js
implementation: the <b>linear</b> scale and its{' '}
<code>scaleLinear()</code> function.
</p>
</>
}
/>

<CircleScaleExercise />

{/* -
-
-
-
-
-
- */}

<h2>
The <code>scaleLinear()</code> function
</h2>
<p>
The <code>scaleLinear()</code> function is part of the{' '}
<a href="https://github.com/d3/d3-scale">d3-scale</a> module of d3.js.
</p>
<p>
It expects 2 inputs: a <b>domain</b> and a <b>range</b>.
</p>
<h3>🏠 Domain</h3>
<p>
Usually an array of length 2. It provides the <code>min</code> and the{' '}
<code>max</code> of the values we have in the dataset.
</p>
<h3>📏 Range</h3>
<p>
Usually an array of length 2. It provides the start and the end of the
positions we are targeting in pixel.
</p>
<p>
The output is a function that expects only 1 argument. You give it a
value from the domain, and it returns the corresponding value in the
Range
</p>

{/* -
-
-
-
-
-
- */}

<h2>Much more power</h2>
<p>The scaleLinear function actually make much more than that!!!</p>
</LayoutCourse>
);
}
2 changes: 1 addition & 1 deletion util/lessonList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export const lessonList: Lesson[] = [
),
readTime: 4,
link: '/course/scales/introduction',
status: 'wip',
status: 'free',
moduleId: 'scales',
},
{
Expand Down

0 comments on commit f4116fa

Please sign in to comment.