Skip to content

Commit

Permalink
k
Browse files Browse the repository at this point in the history
  • Loading branch information
holtzy committed Oct 29, 2024
1 parent c438d41 commit b76bb5a
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 60 deletions.
177 changes: 146 additions & 31 deletions pages/course/axis/axis-with-d3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { ExerciseAccordion } from '@/component/ExerciseAccordion';
import Link from 'next/link';
import { AxisBasicD3Demo } from '@/viz/AxisBasicD3/AxisBasicD3Demo';
import { ChartOrSandbox } from '@/component/ChartOrSandbox';
import { Caption } from '@/component/UI/Caption';
import { TakeHome } from '@/component/TakeHome';
import { Sidenote } from '@/component/SideNote';

const previousURL = '/course/axis/axis-variations';
const currentURL = '/course/axis/axis-with-d3';
const nextURL = undefined;
const nextURL = '/course/axis/project';
const seoDescription = '';

export default function Home() {
Expand All @@ -36,30 +39,60 @@ export default function Home() {
description={
<>
<p>
d3 offers some very smart functions when it comes to building
axes.
The previous lessons taught us how to build React{' '}
<b>axis components</b> that can be used in any of your charts.
</p>
<p>
I personnaly prefer to avoid using d3 rendering functions like
this. I have more control when using react for rendering. But if
you are familiar with d3, it can be very handy to use those
functions, and here is how to wrap them in a usEffect
However, there's an alternative worth mentioning:{' '}
<b>D3 can also draw axes</b>. Let's explore this option and see
which one works best for you.
</p>
</>
}
/>

<h2>The d3 axis module</h2>
<p>Explain what it is with links and examples</p>
<p>
D3 has a{' '}
<a href="https://d3js.org/d3-axis" target="_blank">
whole module
</a>{' '}
dedicated to drawing axes! It is called ... <code>d3-axis</code> 🙃
</p>
<p>
It performs essentially the same function as the <code>AxisBottom</code>{' '}
and
<code>AxisLeft</code> components we created in the previous lesson:
taking a scale and <b>rendering</b> lines and ticks based on it on the
screen.
</p>

<p>
<br />
</p>
<center>
<img
src="/img/d3-axis-overview.png"
width={700}
className="border p-2"
/>
<Caption>
<p>
A few axes made with d3.js and its <code>d3-axis</code> module.
</p>
</Caption>
</center>

<h2>How to use in a react app.</h2>
<h2>😳 Did you say rendering?</h2>
<p>
If you're a d3.js afficionados and want to deal with as little react as
possible, you can still use the good old <code>axisBottom()</code> and{' '}
<code>axisLeft()</code> methods of d3 and wrap them in a
<code>useEffect()</code> hook.
<TakeHome>We have a challenge</TakeHome>: in a React environment where
rendering is managed by React, how can we <b>delegate</b> part of the
rendering process to D3?
</p>
<p>Here is an example below:</p>
<p>
This is possible using a react <code>useEffect()</code>!
</p>
<p>Here is an example:</p>
<ChartOrSandbox
vizName={'AxisBasicD3'}
VizComponent={AxisBasicD3Demo}
Expand All @@ -69,17 +102,35 @@ export default function Home() {
'This axis is rendered using d3. The d3 necessary functions are called from a useEffect'
}
/>
{/* -
-
-
-
-
*/}
<h2>How to Use D3 to Render Axes in a React App</h2>
<p>Let's clarify the code from the example above.</p>

<h2>Explanation</h2>
<p>Everything starts with a ref</p>
<h3>
⛳️ Using a <code>ref</code>
</h3>
<p>
A <a href="https://react.dev/reference/react/useRef">ref</a> acts as a
pointer to a specific part of the DOM. We need to initialize a{' '}
<code>ref</code> and assign it to the SVG element we want to manipulate
with JavaScript later on.
</p>
<p>
To create the <code>ref</code>, use the following code:
</p>
<CodeBlock
code={`
const axesRef = useRef(null);
`.trim()}
/>
<p>
This ref is used to target a specific svg element where the axis will be
drawn:
Next, assign the <code>ref</code> to the <code>&lt;g&gt;</code> element
where D3 will render the axis:
</p>
<CodeBlock
code={`
Expand All @@ -88,29 +139,93 @@ const axesRef = useRef(null);
height={boundsHeight}
ref={axesRef}
transform={...do the translate}
/>`.trim()}
/>
`.trim()}
/>

<h3>
🔧 Implementing a <code>useEffect</code> to modify the <code>ref</code>
</h3>
<p>
And then a <code>useEffect</code> is used to call the d3 functions that
render the axis.
Now, we need a{' '}
<a href="https://react.dev/reference/react/useEffect">useEffect</a> that
selects this <code>ref</code> and applies changes to it.
</p>
<p>
The <code>useEffect</code> hook allows us to run a function each time
the component mounts and whenever specified variables are updated.
</p>
<CodeBlock
code={`
useEffect(() => {
const svgElement = d3.select(axesRef.current);
svgElement.selectAll("*").remove();
const xAxisGenerator = d3.axisBottom(xScale);
svgElement
.append("g")
.attr("transform", "translate(0," + boundsHeight + ")")
.call(xAxisGenerator);
// Now do some stuff to this svgElement...
}, [xScale, width]); // Rerun this function when xScale or chart width changes to redraw the axis
`.trim()}
/>

const yAxisGenerator = d3.axisLeft(yScale);
svgElement.append("g").call(yAxisGenerator);
}, [xScale, yScale, boundsHeight]);
<h3>✏️ Let's draw</h3>
<p>
Now we can use some d3.js code inside the <code>useEffect</code>. This
will draw a bottom axis!
</p>
<div className="relative">
<Sidenote
text={
<p>
This code is often considered <b>imperative</b>, whereas React
code is typically <b>declarative</b>.
</p>
}
/>
<CodeBlock
code={`
const svgElement = d3.select(axesRef.current);
// remove potential previous axes
svgElement.selectAll("*").remove();
// d3 code to render a bottom axis:
const xAxisGenerator = d3.axisBottom(xScale);
svgElement
.append("g")
.attr("transform", "translate(0," + boundsHeight + ")")
.call(xAxisGenerator);
`.trim()}
/>
/>
</div>
<p>Done! 🎉</p>

<h2>So, React or D3.js for Axes?</h2>
<p>
Both options have their merits, each with its own set of pros and cons.
Personally, <TakeHome>I prefer the React component approach</TakeHome>{' '}
for creating axes. Here’s why:
</p>
<p>
<br />
</p>
<p>
🎨 <strong>Styling:</strong> You can customize axis elements
individually, allowing for precise styling.
</p>
<p>
🔄 <strong>Lifecycle:</strong> When using D3 to create axes, they
operate outside of React's lifecycle events, making it challenging to
ensure they update at the right times.
</p>
<p>
♻️ <strong>Reusability:</strong> React emphasizes the creation of
reusable components. Building axes with D3 each time goes against this
philosophy, which simplifies development.
</p>
<p>
🛠️ <strong>Maintainability / Readability:</strong> Other developers in
your organization will likely find it easier to understand the SVG
markup of the AxisBottom component compared to the D3.js functions from
the d3-axis module.
</p>
</LayoutCourse>
);
}
74 changes: 50 additions & 24 deletions pages/course/axis/bottom-axis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,19 +240,41 @@ xScale.ticks(10) // [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
</div>
),
},
{
title: <span>Add axis title</span>,
content: (
<div className="max-w-96">
<p>
Adding a title is simply a matter of including a{' '}
<code>text</code> element in your SVG!
</p>
<p>
However, handling text in SVG can be a hassle, as it doesn’t
natively support wrapping.
</p>
<p>
Personally, I prefer to place the title manually within the
main SVG area rather than in the <b>AxisBottom</b> component,
but it's entirely up to you!
</p>
</div>
),
},
]}
/>

<h2>Using the component</h2>
<h2>We got axes! 🪓</h2>
<p>
Once you have the bottom and left axis component described above you
just need to call them properly. You need to compute the bounds area by
substracting the margins to the total svg area.
If you've followed the previous exercises, you now know how to add a
bottom axis to your graph.
</p>
<p>
Don't forget to add an additional translation to the bottom axis to
render it... at the bottom.
Adding a <b>left</b> axis works in <b>much the same way</b>! Wrap it in
an <code>AxisLeft</code>
component, and you're good to go!
</p>
<p>Take a moment to review the example code below:</p>

<ChartOrSandbox
vizName={'AxisBasic'}
VizComponent={AxisBasicDemo}
Expand All @@ -267,40 +289,44 @@ xScale.ticks(10) // [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
const snippet1 = `
// AxisBottom.tsx
import { ScaleLinear } from 'd3';
type AxisBottomProps = {
xScale: ScaleLinear<number, number>;
pixelsPerTick: number;
};
// tick length
const TICK_LENGTH = 6;
export const AxisBottom = ({ xScale, pixelsPerTick }) => {
export const AxisBottom = ({ xScale, pixelsPerTick }: AxisBottomProps) => {
const range = xScale.range();
const ticks = useMemo(() => {
const width = range[1] - range[0];
const numberOfTicksTarget = Math.floor(width / pixelsPerTick);
return xScale.ticks(numberOfTicksTarget).map((value) => ({
value,
xOffset: xScale(value),
}));
}, [xScale]);
const width = range[1] - range[0];
const numberOfTicksTarget = Math.floor(width / pixelsPerTick);
return (
<>
{/* Main horizontal line */}
<path
d={["M", range[0], 0, "L", range[1], 0].join(" ")}
fill="none"
<line
x1={range[0]}
y1={0}
x2={range[1]}
y2={0}
stroke="currentColor"
fill="none"
/>
{/* Ticks and labels */}
{ticks.map(({ value, xOffset }) => (
<g key={value} transform={\`translate(\${xOffset}, 0)\`}>
{xScale.ticks(numberOfTicksTarget).map((value) => (
<g key={value} transform={\`translate(\${xScale(value)}, 0)\`}>
<line y2={TICK_LENGTH} stroke="currentColor" />
<text
key={value}
style={{
fontSize: "10px",
textAnchor: "middle",
transform: "translateY(20px)",
fontSize: '10px',
textAnchor: 'middle',
transform: 'translateY(20px)',
}}
>
{value}
Expand Down
Loading

0 comments on commit b76bb5a

Please sign in to comment.