diff --git a/pages/course/hover-effect/css-descendant-selector.tsx b/pages/course/hover-effect/css-descendant-selector.tsx index 71f9a885..c5e70f2d 100644 --- a/pages/course/hover-effect/css-descendant-selector.tsx +++ b/pages/course/hover-effect/css-descendant-selector.tsx @@ -13,9 +13,6 @@ import { ExerciseDoubleSandbox, } from '@/component/ExerciseDoubleSandbox'; import { ExerciseAccordion } from '@/component/ExerciseAccordion'; -import { Graph9 } from '@/viz/exercise/LollipopFirstSolution/Graph'; -import { Graph12 } from '@/viz/exercise/LollipopHoverEffectSolution/Graph'; -import { Graph22 } from '@/viz/exercise/Hover3CirclesDescandantSelectorIssueSolution/Graph'; const previousURL = '/course/hover-effect/css-pseudo-class'; const currentURL = '/course/hover-effect/css-descendant-selector'; @@ -41,7 +38,7 @@ export default function Home() { title={currentLesson.name} lessonStatus={currentLesson.status} readTime={currentLesson.readTime} - topBadge={'Lesson ' + currentLessonId} + selectedLesson={currentLesson} description={ <>

@@ -164,11 +161,35 @@ export default function Home() { content: , }, { - title: Scatterplot = problem 🚨, + title: Scatterplot = problem? 🚨, content: , }, ]} /> + +

+
+
+

+ +
+
+

+ Note: When you use the :hover pseudo-class on an SVG + area, it activates whenever the mouse enters the entire SVG + rectangle. +

+

+
+
+

+

+ However, if you apply :hover to a g{' '} + element, it will only trigger when the mouse hovers over one of the + elements within the g group! +

+
+
); } diff --git a/pages/course/hover-effect/css-pseudo-class.tsx b/pages/course/hover-effect/css-pseudo-class.tsx index 94761b90..52a2fbef 100644 --- a/pages/course/hover-effect/css-pseudo-class.tsx +++ b/pages/course/hover-effect/css-pseudo-class.tsx @@ -14,9 +14,6 @@ import { import { ExerciseAccordion } from '@/component/ExerciseAccordion'; import Link from 'next/link'; import { TakeHome } from '@/component/TakeHome'; -import { Graph9 } from '@/viz/exercise/HoverFirstTreemapSolution/Graph'; -import { Graph10 } from '@/viz/exercise/HoverDeathByStateSolution/Graph'; -import { Graph11 } from '@/viz/exercise/HoverDeathByStateFixSolution/Graph'; const previousURL = '/course/hover-effect/introduction'; const currentURL = '/course/hover-effect/css-pseudo-class'; @@ -42,7 +39,7 @@ export default function Home() { title={currentLesson.name} lessonStatus={currentLesson.status} readTime={currentLesson.readTime} - topBadge={'Lesson ' + currentLessonId} + selectedLesson={currentLesson} description={ <>

The simplest strategy.

diff --git a/pages/course/hover-effect/internal-state.tsx b/pages/course/hover-effect/internal-state.tsx index c3a4182e..ba1b1975 100644 --- a/pages/course/hover-effect/internal-state.tsx +++ b/pages/course/hover-effect/internal-state.tsx @@ -4,7 +4,6 @@ import { LayoutCourse } from '@/component/LayoutCourse'; import { lessonList } from '@/util/lessonList'; import { ChartOrSandbox } from '@/component/ChartOrSandbox'; import { CodeBlock } from '@/component/UI/CodeBlock'; -import { DonutChartHoverDemo } from '@/viz/DonutChartHover/DonutChartHoverDemo'; import { ScatterplotHoverHighlightDimDemo } from '@/viz/ScatterplotHoverHighlightDim/ScatterplotHoverHighlightDimDemo'; import { Badge } from '@/component/UI/badge'; import GraphGallery from '@/component/GraphGallery'; @@ -33,7 +32,7 @@ export default function Home() { title={currentLesson.name} lessonStatus={currentLesson.status} readTime={currentLesson.readTime} - topBadge={'Lesson ' + currentLessonId} + selectedLesson={currentLesson} description={ <>

diff --git a/pages/course/hover-effect/introduction.tsx b/pages/course/hover-effect/introduction.tsx index ec79008b..3418d351 100644 --- a/pages/course/hover-effect/introduction.tsx +++ b/pages/course/hover-effect/introduction.tsx @@ -3,8 +3,6 @@ import TitleAndDescription from '@/component/TitleAndDescription'; import { LayoutCourse } from '@/component/LayoutCourse'; import { lessonList } from '@/util/lessonList'; import { Sidenote } from '@/component/SideNote'; -import { ChartOrSandbox } from '@/component/ChartOrSandbox'; -import { ScatterplotHoverHighlightDimDemo } from '@/viz/ScatterplotHoverHighlightDim/ScatterplotHoverHighlightDimDemo'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/component/UI/tabs'; import { ScatterplotHoverHighlightPseudoClassDemo } from '@/viz/ScatterplotHoverHighlightPseudoClass/ScatterplotHoverHighlightPseudoClassDemo'; import { TreemapHoverEffectDemo } from '@/viz/TreemapHoverEffect/TreemapHoverEffectDemo'; diff --git a/pages/course/hover-effect/toggle-class-in-js.tsx b/pages/course/hover-effect/toggle-class-in-js.tsx index f09fc6ed..29bd81b9 100644 --- a/pages/course/hover-effect/toggle-class-in-js.tsx +++ b/pages/course/hover-effect/toggle-class-in-js.tsx @@ -5,6 +5,18 @@ import { lessonList } from '@/util/lessonList'; import { ChartOrSandbox } from '@/component/ChartOrSandbox'; import { CodeBlock } from '@/component/UI/CodeBlock'; import { DonutChartHoverDemo } from '@/viz/DonutChartHover/DonutChartHoverDemo'; +import { Sidenote } from '@/component/SideNote'; +import Link from 'next/link'; +import { Badge } from '@/component/UI/badge'; +import { Caption } from '@/component/UI/Caption'; +import { cn } from '@/util/utils'; +import { buttonVariants } from '@/component/UI/button'; +import { + Exercise, + ExerciseDoubleSandbox, +} from '@/component/ExerciseDoubleSandbox'; +import { ExerciseAccordion } from '@/component/ExerciseAccordion'; +import { Graph23 } from '@/viz/exercise/PieFirstSolution/Graph'; const previousURL = '/course/hover-effect/css-descendant-selector'; const currentURL = '/course/hover-effect/toggle-class-in-js'; @@ -30,75 +42,244 @@ export default function Home() { title={currentLesson.name} lessonStatus={currentLesson.status} readTime={currentLesson.readTime} - topBadge={'Lesson ' + currentLessonId} + selectedLesson={currentLesson} description={ <>

- In the previous lesson, we learned how to modify a hovered graph - item using the :hover CSS pseudo-class. + In the previous lesson, we explored how to dim elements that are{' '} + not being hovered over using a CSS-only approach.

- However, this approach has design limitations. To achieve a - more effective highlighting effect, it's better to simultaneously{' '} - dim the other graph items. -

-

- This can be accomplished using CSS alone, with the help of the CSS - descendant selector. + However, there are times when using JavaScript can provide more{' '} + precise control over the hover effect. A handy technique is + to toggle classes with JavaScript. Let’s take a look at how + to do this.

} /> - -

Toggle class in JS

+

🔘 Toggle Class in JavaScript

+

+ 1️⃣ Create a ref +

- Problem above: when mouse enter the chart area, triggers effect even if - no marker hovered over. + We’ve discussed the{' '} + useRef React hook + a few times now.

- Solution: CSS compound class selecter ( - - MDN doc - - ) + This hook allows us to target specific elements in the DOM and + manipulate them with JavaScript.

+ + + + + + `.trim()} + /> +

2️⃣ Toggle classes

- In CSS, a compound class selector combines multiple class names to - target elements that match all of the specified classes. + Once we have the containerRef set up, we can use it to make + changes to the SVG container from anywhere in the graph!

- We can use the same css as the above example, but add the highlight - class using javascript: + For example, we can add an onMouseEnter property to the + circle that will apply a hasHighlight class to the SVG + container:

{ - if (ref.current) { - ref.current.classList.add(styles.hasHighlight); - } -}} -onMouseLeave={() => { - if (ref.current) { - ref.current.classList.remove(styles.hasHighlight); - } -}} -`.trim()} + { + if (containerRef.current) { + containerRef.current.classList.add(styles.hasHighlight); + } + }} + onMouseLeave={() => { + if (containerRef.current) { + containerRef.current.classList.remove(styles.hasHighlight); + } + }} +/> + `.trim()} /> +

3️⃣ Use it for styling

+
+ + A compound class selector combines multiple class names to target + elements that match all of the specified classes. For + example: .class1.class2 . +

+ } + /> +

+ We can use CSS{' '} + + compound class selectors + {' '} + to apply different styles based on whether the{' '} + .hasHighlight class is present! +

+
+

+ For example, we can set the opacity of all "slices" in the container to{' '} + 1, except when the container has the{' '} + .hasHighlight class, in which case the opacity will be set + to 0.2: +

+ +

🍩 Application: Donut Chart with Hover Effect

+

+ A donut chart is a variation of the more well-known{' '} + pie chart. It is easy to create using the{' '} + pie() function from D3.js. +

+

+ The following example demonstrates the technique described earlier. When + a slice is hovered over, a class is added to the SVG container, + resulting in a CSS change for all the other slices. +

-

Pros & Cons

- +

+ Pros +

+
    +
  • Fine control over interactions via JavaScript
  • +
  • Performance-friendly: no re-rendering required
  • +
+

+ Cons +

+
    +
  • + Doesn't align with React’s central state management approach, which + can make managing state more challenging. +
  • +

More examples

The examples below all use this strategy to implement their hover effect.

+
+ + + Check the legend on the left hand side: it uses class toggle for its + hover effect! + + + Visit project + +
+ {/* - +- +- +- +- +- +- */} +

Exercices

+ Your first pie chart! 🍭, + content: , + }, + { + title: Pie chart with hover effect, + content: , + }, + ]} + /> ); } +const exercises: Exercise[] = [ + { + whyItMatters: ( + <> +

+ With the SVG and D3 foundations you’ve built, creating a new chart + type becomes a breeze! +

+ + ), + toDo: ( + <> +
    +
  • A dataset is provided in the sandbox folder.
  • +
  • + Build a Cleveland chart with this dataset. (This chart is a + variation of the lollipop plot and is helpful for comparing two + values across multiple groups.) +
  • +
  • Check the solution tab to see the intended chart appearance.
  • +
+ + ), + practiceSandbox: 'exercise/PieFirstPractice', + solutionSandbox: 'exercise/PieFirstSolution', + }, + + { + whyItMatters: ( + <> +

+ Descendant selectors are fantastic for creating advanced hover effects + with only CSS! +

+ + ), + toDo: ( + <> +
    +
  • + Add a class rowsContainer to the SVG container and a + class row to each row. +
  • +
  • + Using a CSS descendant selector, highlight the hovered row and dim + other rows. +
  • +
+ + ), + practiceSandbox: 'exercise/LollipopHoverEffectPractice', + solutionSandbox: 'exercise/LollipopHoverEffectSolution', + }, +]; diff --git a/public/video/pyramid-legend.mp4 b/public/video/pyramid-legend.mp4 new file mode 100644 index 00000000..fa97ef39 Binary files /dev/null and b/public/video/pyramid-legend.mp4 differ diff --git a/viz/exercise/Hover3CirclesDescendantSelectorIssueSolution/Graph.tsx b/viz/exercise/Hover3CirclesDescendantSelectorIssueSolution/Graph.tsx index cf9b462f..d755a52c 100644 --- a/viz/exercise/Hover3CirclesDescendantSelectorIssueSolution/Graph.tsx +++ b/viz/exercise/Hover3CirclesDescendantSelectorIssueSolution/Graph.tsx @@ -1,11 +1,25 @@ import styles from './graph.module.css'; -export const Graph = () => { +export const Graph43 = () => { return ( - - - - + + + + + + ); }; diff --git a/viz/exercise/LollipopFirstPractice copy/Graph.tsx b/viz/exercise/LollipopFirstPractice copy/Graph.tsx deleted file mode 100644 index fc5eca04..00000000 --- a/viz/exercise/LollipopFirstPractice copy/Graph.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import * as d3 from 'd3'; -import { data } from './data'; - -const width = 500; -const height = 500; -const MARGIN = { top: 30, right: 30, bottom: 30, left: 80 }; - -export const Graph = () => { - const boundsWidth = width - MARGIN.right - MARGIN.left; - const boundsHeight = height - MARGIN.top - MARGIN.bottom; - - // Y axis is for groups - const groups = data.sort((a, b) => b.value1 - a.value1).map((d) => d.name); - const yScale = d3.scaleBand().domain(groups).range([0, boundsHeight]); - - // X axis - const [min, max] = d3.extent(data.map((d) => d.value1)); - - const xScale = d3 - .scaleLinear() - .domain([0, max || 10]) - .range([0, boundsWidth]); - - // Build the shapes - const allShapes = data.map((d, i) => { - // TODO - return null; - }); - - return ( -
- - - {allShapes} - - -
- ); -}; diff --git a/viz/exercise/LollipopFirstPractice copy/data.ts b/viz/exercise/LollipopFirstPractice copy/data.ts deleted file mode 100644 index d26cd957..00000000 --- a/viz/exercise/LollipopFirstPractice copy/data.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const data = [ - { name: "Mark", value1: 90, value2: 72 }, - { name: "Robert", value1: 12, value2: 10 }, - { name: "Emily", value1: 34, value2: 14 }, - { name: "Marion", value1: 53, value2: 24 }, - { name: "Nicolas", value1: 98, value2: 58 }, - { name: "Mélanie", value1: 23, value2: 20 }, - { name: "Gabriel", value1: 18, value2: 10 }, - { name: "Jean", value1: 104, value2: 70 }, - { name: "Paul", value1: 2, value2: 1 }, -] diff --git a/viz/exercise/PieFirstPractice/Graph.tsx b/viz/exercise/PieFirstPractice/Graph.tsx new file mode 100644 index 00000000..f37a7434 --- /dev/null +++ b/viz/exercise/PieFirstPractice/Graph.tsx @@ -0,0 +1,19 @@ +import * as d3 from 'd3'; +import { data, DataItem } from './data'; + +const MARGIN = 30; +const width = 400; +const height = 400; +const colors = ['#98abc5', '#8a89a6', '#7b6888', '#6b486b', '#a05d56']; + +export const Graph = () => { + const radius = Math.min(width, height) / 2 - MARGIN; + + // TODO + + return ( + + {/* TODO */} + + ); +}; diff --git a/viz/exercise/PieFirstPractice/data.ts b/viz/exercise/PieFirstPractice/data.ts new file mode 100644 index 00000000..4069b832 --- /dev/null +++ b/viz/exercise/PieFirstPractice/data.ts @@ -0,0 +1,12 @@ +export type DataItem = { + name: string; + value: number; +}; + +export const data: DataItem[] = [ + { name: "Mark", value: 90 }, + { name: "Robert", value: 12 }, + { name: "Emily", value: 34 }, + { name: "Marion", value: 53 }, + { name: "Nicolas", value: 98 }, +] diff --git a/viz/exercise/LollipopFirstPractice copy/index.js b/viz/exercise/PieFirstPractice/index.js similarity index 100% rename from viz/exercise/LollipopFirstPractice copy/index.js rename to viz/exercise/PieFirstPractice/index.js diff --git a/viz/exercise/LollipopFirstPractice copy/package.json b/viz/exercise/PieFirstPractice/package.json similarity index 100% rename from viz/exercise/LollipopFirstPractice copy/package.json rename to viz/exercise/PieFirstPractice/package.json diff --git a/viz/exercise/PieFirstSolution/Graph.tsx b/viz/exercise/PieFirstSolution/Graph.tsx new file mode 100644 index 00000000..26a6ebae --- /dev/null +++ b/viz/exercise/PieFirstSolution/Graph.tsx @@ -0,0 +1,34 @@ +import * as d3 from 'd3'; +import { data, DataItem } from './data'; + +const MARGIN = 30; +const width = 400; +const height = 400; +const colors = ['#98abc5', '#8a89a6', '#7b6888', '#6b486b', '#a05d56']; + +export const Graph = () => { + const radius = Math.min(width, height) / 2 - MARGIN; + + const pieGenerator = d3.pie().value((d) => d.value); + const pie = pieGenerator(data); + + const arcPathGenerator = d3.arc(); + const arcs = pie.map((p) => + arcPathGenerator({ + innerRadius: 0, + outerRadius: radius, + startAngle: p.startAngle, + endAngle: p.endAngle, + }) + ); + + return ( + + + {arcs.map((arc, i) => { + return ; + })} + + + ); +}; diff --git a/viz/exercise/PieFirstSolution/data.ts b/viz/exercise/PieFirstSolution/data.ts new file mode 100644 index 00000000..4069b832 --- /dev/null +++ b/viz/exercise/PieFirstSolution/data.ts @@ -0,0 +1,12 @@ +export type DataItem = { + name: string; + value: number; +}; + +export const data: DataItem[] = [ + { name: "Mark", value: 90 }, + { name: "Robert", value: 12 }, + { name: "Emily", value: 34 }, + { name: "Marion", value: 53 }, + { name: "Nicolas", value: 98 }, +] diff --git a/viz/exercise/PieFirstSolution/index.js b/viz/exercise/PieFirstSolution/index.js new file mode 100644 index 00000000..fa564d27 --- /dev/null +++ b/viz/exercise/PieFirstSolution/index.js @@ -0,0 +1,6 @@ +// File used to render something in codesandbox only +import ReactDOM from 'react-dom'; +import { Graph } from './Graph'; + +const rootElement = document.getElementById('root'); +ReactDOM.render(, rootElement); diff --git a/viz/exercise/PieFirstSolution/package.json b/viz/exercise/PieFirstSolution/package.json new file mode 100644 index 00000000..84ed1985 --- /dev/null +++ b/viz/exercise/PieFirstSolution/package.json @@ -0,0 +1,29 @@ +{ + "name": "pie-chart-basic", + "version": "1.0.0", + "description": "", + "keywords": [], + "main": "index.js", + "dependencies": { + "react": "17.0.2", + "d3": "7.1.1", + "react-dom": "17.0.2", + "react-scripts": "4.0.0" + }, + "devDependencies": { + "@babel/runtime": "7.13.8", + "typescript": "4.1.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +}