Lab 6: Visualizing categorical data with D3

In this lab, we will learn:

  • What is SVG and what does it look like?
  • What does D3 do?
  • How can we use D3 to draw a pie chart?
  • How can we create reactive visualizations of data that is changing?
  • How can we make interactive visualizations that change the data displayed on the page?
  • How can we make interactive visualizations accessible?
Table of contents

Check-off

You need to come to TA Office Hours to get checked off for this lab (any of them, no appointment needed), OR submit your work asynchronously by filling out this form.

If you choose to submit your work asynchronously and have an incorrect or incomplete part of the lab, you will not receive any credit for the lab (we do not offer partial credit on labs). You may not resubmit this form nor ask for a synchronous check off for the same lab.

Lab 6 Rubric

To successfully complete this lab check-off, ensure your work meets all of the following requirements:

General

  • Same functionality from Labs 4-5.
  • Succesfully deployed to GitHub Pages.
  • Year visible on each project card in both Home page and Projects page.

Pie Chart at top of Projects page

  • Has different colored slice for each year in your projects data.
  • Slices that are not being hovered on fade out.
  • Slice has the following on click/select:
    • Selected slice changes color to something not already being used by the chart.
    • Unselected slices are faded out.
    • Hovering on other slices still works when slice is selected.
    • Selecting another slice while a slice is selected works.
    • Clicking on the selected slice deselects it.
    • Project cards are filtered by year selected.
  • Uses actual project data
  • Only visualizes visible project data (i.e. changes when a query is entered in the search bar)

Legend for pie chart

  • Swatches are the same color as the slices.
  • Side by side with the pie chart.
  • Is responsive to window size changes.
  • When pie slice is selected:
    • Corresponding legend item text changes to the same color
    • Corresponding legend item swatch color changes to the same color
    • Other legend items are faded out with lighter text color

Search

  • Is case-insensitive
  • Searches across all project metadata, not just titles
  • Is styled to be the full width of the page

If your pie chart is transitioning abruptly/weirdly when filtering using the search bar, that is okay! There’s a way to fix the transition to make it smoother, but is outside the scope of this lab due to time constraints.

Slides

D3 was also covered in Monday’s lecture, so it can be helpful for you to review the material from it.

This lab is a little more involved than some of the previous labs, because it’s introducing the core technical material around data visualization. A robust understanding of these concepts will be invaluable as you work on your final projects, so spending time practicing them for the lab will be time will spent.

Step 0: Update project data and add years

If you have not yet done Step 4 of Lab 5, you should do it now.

Step 0.1: Show year in each project

Since we have the year data, we should show it in the project list. That way we can also more easily verify whether our code in the rest of the lab works correctly.

Edit the <Project> component (in src/lib/Project.svelte) to show the year of the project. You can use any HTML you deem suitable and style it however you want. I placed it under the project description (you’ll need to wrap both in the same <div> otherwise they will occupy the same grid cell and overlap), and styled it like this:

Three projects with the year shown in gray letters underneath the description

In case you like the above, the font-family is Baskerville (a system font) and I’m using font-variant-numeric: oldstyle-nums to make the numbers look a bit more like they belong in the text.

From this point onwards, there are only two files that we will be editing in this lab:

  1. src/lib/Pie.svelte (created in Step 1.1)
  2. src/routes/projects/+page.svelte (our projects page)

Step 1: Creating a pie chart with D3

Step 1.1: Create a <Pie> component and use it in your project page

To keep the code manageable, we will be creating our pie chart in a separate component, called <Pie>.

Create a new Pie.svelte file in src/lib and add some text in it, e.g. “Hello from Pie.svelte”. Then, in your projects page (src/routes/projects/+page.svelte), import the <Pie> component inside the script tags:

import Pie from '$lib/Pie.svelte';

and then use it like <Pie /> in the HTML part of your file. Add our new Pie component under the page title (i.e. between <h1> and <div>).

Save, and make sure the text you wrote in Pie.svelte is displayed in your project page to make sure all plumbing is working. Once you see the text you have written, you can delete the text in the Pie.svelte file.

Step 1.2: Create the SVG element

The first step to create a pie chart with D3 is to create an <svg> element in the <Pie> component.

We will give it a viewBox of -50 -50 100 100 which defines the coordinate system it will use internally. In this case, it will have a width and height of 100, and the (0, 0) point will be in the center (which is quite convenient for a pie chart!).

<svg viewBox="-50 -50 100 100">
</svg>

D3 is a library that translates high level visualization concepts into low level drawing commands. The technology currently used for these drawing commands is called SVG and is a language for drawing vector graphics. This means that instead of drawing pixels on a screen, SVG draws shapes and lines using descriptions of their geometry (e.g. centerpoints, radius, start and end coordinates, etc.). It looks very much like HTML, with elements delineated by tags, tags delineated by angle brackets, and attributes within those tags. However, instead of content-focused elements like <h1> and <p>, we have drawing-focused elements like <circle> and <path>. SVG code can live either in separate files (with an .svg extension) or be embedded in HTML via the <svg> elements.

Step 1.3 Drawing a circle with D3

Our SVG is currently just a blank container with no paths inside which doesn’t look very interesting on our page. Let’s use D3 to draw a circle inside our SVG.

First, we need to add D3 to our project so we can use it in our JS code. Open the VS Code terminal and run:

npm install d3

Ignore any warnings about peer dependencies.

So now that D3 is installed how do we use it?

First, in your Pie component, add in <script>... </script> tags to the top of your file so that we can write D3 and JS code. Then, add the following import statement:

import * as d3 from 'd3';

Now let’s use the d3.arc() function from the D3 Shape module to create the path for our circle. This works with two parts: first, we create an arc generator which is a function that takes data and returns a path string. We’ll configure it to produce arcs based on a radius of 50 by adding .innerRadius(0).outerRadius(50). If you instead want to create a donut chart, it’s as easy as changing the inner radius to something other than 0!

let arcGenerator = d3.arc().innerRadius(0).outerRadius(50);

We then generate an arc by providing a starting angle (0) and an ending angle in radians (2 * Math.PI) to create a full circle:

let arc = arcGenerator({
	startAngle: 0,
	endAngle: 2 * Math.PI
});

Did we need two statements? Not really, we only did so for readability. This would have been perfectly valid JS:

let arc = d3.arc().innerRadius(0).outerRadius(50)({
	startAngle: 0,
	endAngle: 2 * Math.PI
});

Now that we have our arc, we can add it to our SVG using the <path> element. The <path> element can draw any shape, but its syntax is a little unwieldy. It uses a string of commands to describe the shape, where each command is a single letter followed by a series of numbers that specify command parameters. All of this is stuffed into a single d attribute. Luckily, D3 can auto-generate the path for us.

<svg viewBox="-50 -50 100 100">
	<path d={arc} fill="red" />
</svg>

Since we have not given the graphic any explicit dimensions, by default it will occupy the entire width of its parent container and will have an aspect ratio of 1:1 (as defined by its coordinate system). It will look a bit like this:

We can add some CSS in the component’s <style> element to limit its size a bit and also add some spacing around it. You can add this styling to the Pie.svelte file between <style>...</style> tags at the bottom of your file:

svg {
	max-width: 20em;
	margin-block: 2em;

	/* Do not clip shapes outside the viewBox */
	overflow: visible;
}

This will make it look like this:

Step 1.4: Drawing a static pie chart with D3

Let’s draw a pie chart with two slices, one for each of the numbers 1 and 2, i.e. a 33% and 66% slice.

let data = [1, 2];

We’ll draw our pie chart as two <path> elements, one for each slice.

D3 provides the d3.pie() function for creating the slices in a pie chart. It will generate the path elements for us so that we don’t have to do it ourselves. Just like d3.arc(), d3.pie() is a function that returns another function, which we can use to generate the start and end angles for each slice in our pie chart instead of having to do it ourselves.

This …slice generator function takes an array of data values and returns an array of objects, each of whom represents a slice of the pie and contains the start and end angles for it. We feed these objects to our arcGenerator to create the paths for the slices. It looks like this:

let data = [1, 2];
let sliceGenerator = d3.pie();
let arcData = sliceGenerator(data);
let arcs = arcData.map(d => arcGenerator(d));

Add this code to the JS (between the <script> tags) part of your file.

Since we are now generating multiple paths (i.e. arcs), let’s wrap our <path> element with an {#each} block:

{#each arcs as arc}
  <path d={arc} fill="red" />
{/each}

The {} let us use javascript code inside the HTML parts of our file. Even though <svg> is not inside the <script> tags, we are still able to use javascript logic and control statements on the <svg> by using the {} notation.

Notice how we were able to use the same arcGenerator from step 1.3 to create two slices instead of one big circle! We were able to do this by changing the startAngle and endAngle of each slice. The circle from step 1.3 was actually just one really big slice!

If we reload at this point, all we see is …the same red circle. A bit anticlimactic, isn’t it?

However, if you inspect the circle, you will see it actually consists of two <path> elements. We just don’t see it, because they’re both the same color!

Let’s assign different colors to our slices, by adding a colors array and using it to set the fill attribute of our paths:

let colors = ['gold', 'purple'];

Then we add the fill attribute to our previous code generating each arc:

{#each arcs as arc, index}
  <path d={ arc } fill={ colors[index] } />
{/each}

The result should look like this:

Congrats on making a d3 pie chart!

Step 1.5: Adding more data

However, a 2 slice pie chart that’s manually coded is not that interesting. Let’s tweak the data array to add some more numbers:

let data = [1, 2, 3, 4, 5, 5];

Our pie chart did adapt, but all the new slices are black! They don’t even look like four new slices, but rather a huge black one. 😭

alt text

This is because we’ve only specified colors for the first two slices. We could manually specify more colors, but this doesn’t scale. Thankfully, D3 comes with both ordinal and sequential color scales that can generate colors for us based on our data.

For example to use the schemePaired color scale we use the d3.scaleOrdinal() function with that as an argument:

let colors = d3.scaleOrdinal(d3.schemeTableau10);

We also need to change colors[index] to colors(index) in our svg path template, since colors is now a function that takes an index and returns a color instead of an array that we can index into.

This is the result:

Success! 🎉

Step 2: Adding a legend

Our pie chart looks good, but there is no way to tell what each slice represents. Let’s fix that!

Step 2.1: Adding labels to our data

First, even our data does not know what it is — it does not include any labels, but only random quantities.

D3 allows us to specify more complex data, such as an array of objects. Let’s change our data variable from a simple array of numbers to a more interesting array of objects:

let data = [
	{ value: 1, label: "apples" },
	{ value: 2, label: "oranges" },
	{ value: 3, label: "mangos" },
	{ value: 4, label: "pears" },
	{ value: 5, label: "limes" },
	{ value: 5, label: "cherries" }
];

However, to use this data, we need to change our sliceGenerator to tell it which part of our data object is the numerical value we want to show on the pie chart:

let sliceGenerator = d3.pie().value(d => d.value);

d3.pie().value(d => d.value) is essentially saying for each data point d in the data array that we pass in, use the number associated with the value keyword in the object as the slice we want to visualize. For example, for the apples label, we want the slice representing apples to have a value of 1. The .value() function allows us to specify what part of the data object we want to represent as the size of the slices in the pie. If the data object was { count: 1, label: "apples"}, then we would use d3.pie().value(d => d.count) instead.

If everything is set up correctly, you should now see the same pie chart as before.

Step 2.2: Adding a legend

The colors D3 scales return are just regular CSS colors, so we can actually create a legend for our pie chart with plain HTML and CSS.

We can use a <ul> (unordered list) element to make our legend.

We use the same {#each} block to create a list item <li> for each slice, and use a CSS variable (e.g. --color) to pass the color to CSS for styling. For each list item, we are able to get the same color and label that we used for the slices in the chart by using the same color(index) function.

We represent the color swatch on the legend as an empty <span> tag with the class swatch. <span> is similar to the <div> tag but does not take up space on the page unless explicitly told to through CSS styling.

The <em> tag stands for emphasis and helps us easily distinguish between the label and value.

<ul class="legend">
	{#each data as d, index}
		<li style="--color: { colors(index) }">
			<span class="swatch"></span>
			{d.label} <em>({d.value})</em>
		</li>
	{/each}
</ul>

We want to insert the legend after the <svg> element so that we first see the pie chart on the page and then we see the legend. In your final projects, you may want to do something different!

At this point, it doesn’t look like a legend very much:

alt text

We need to add some CSS to make it look like an actual legend.

Step 2.3: Styling the legend

You can experiment with the styles to make it look the way you want, but we’re including some tips below. There are 4 main parts of the legend that you will probably want to style: 1. the color swatch 2. the layout of the list items 3. distinguishing between the legend container and the pie chart 4. where the legend is located in relation to the pie chart (step 2.4)

You should have already added a <style> section to the bottom of your file when we styled the <svg> component. You can add these styles to that same section the same way you would add styles to the .css file in Lab 2.

Making the swatch look like a swatch

The swatch is the <span class="swatch"></span> line. You can make the swatch look like a swatch by:

  1. Making it a square by giving it the same width and height. Since <span> is an inline element by default, to get widths and heights to work, you need to also set display: inline-block or display: inline-flex. (You can also apply display: flex or display: grid on its parent <li> instead).
  2. Giving it a background-color of var(--color)

You can use border-radius to add slight rounding to the corners or even make the swatch into a full circle by setting the border-radius to 50%.

Applying layout on the list to make it look like a legend

We want to organize the list items so that they look pretty. We can style the <ul> element to do this. I first applied display: grid to the <ul> element. Then I, used the auto-fill value in the grid-template-columns attribute so that the grid automatically makes the best use of its available space. You can use whichever values look reasonable for you, but here is what I applied: grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));.

This lays them all out on one line if there’s space, or multiple columns if not.

This is beginning to look better, but the swatches and labels are still not aligned quite right. To fix this, I styled the <li> elements. I added display: flex and align-items: center to vertically center align the text and the swatch. I then added spacing by setting an approprate value for the gap attribute.

Make sure the gap you specify for the <li>s is smaller than the gap you specify for the whole legend’s grid, to honor the design principle of Proximity.

Distinguishing between the legend and the rest of the page

It’s a little hard to distinguish between the legend and the rest of the page right now. To fix this, we can add a border around the legend. I used 1px solid black as my border but you are welcome to use a different value.

Now that we have a border, we should also add spacing between the border and the list items via padding and spacing between the legend and the rest of the page via margin.

The final result will vary depending on your exact CSS, but this was mine:

Step 2.4: Laying out our pie chart and legend side by side

Right now, our pie chart and legend are occupying a ton of space on our page. It’s more common to place the legend to the right of the pie chart, so let’s do that.

We can do that by wrapping both the pie chart and the legend with a shared container, and using a flex layout (i.e. setting display: flex for the .container class in the <style> section).

<div class="container">
	<svg viewBox="-50 -50 100 100">
		<!-- ... -->
	</svg>
	<ul class="legend">
		<!-- ... -->
	</ul>
</div>

You can experiment with the horizontal alignment (align-items) and spacing (gap) of the pie chart and legend, but I would recommend applying flex: 1 to the legend (the same .legend that we styled earlier with a border, padding, and margin), so that it occupies all available width.

If everything worked well, you should now see the pie chart and legend side by side and it should be responsive, i.e. adapt well to changes in the viewport width.

Step 3: Plotting our actual data

So far, we’ve been using meaningless hardcoded data for our pie chart. Let’s change that and plot our actual project data, namely our projects per year data.

Step 3.1: Making data a prop

Hardcoding data within the pie component would give us a pie chart component that can only be used for one specific pie chart. That’s not very useful! Instead, we want to make data a prop of the component, so we can pass it to the component from the page that uses it.

There are two parts to that change: First, changing our data declaration to an export (and its value to an empty array, which will be its default value):

export let data = [];

By adding the export keyword, we are allowing other files to access the data variable. This will allow us to pass in data from other files to the Pie component. Besides this new access permission, data is still the same variable it always was and we can treat it the same way we always did. There is no other change needed in our Pie component.

Second, we pass the same data we used in step 2.1 into the <Pie> component from our projects page. In src/routes/projects/+page.svelte (our projects page), first create a variable in the JS section with all the data we used before

let pieData = [
	{ value: 1, label: "apples" },
	{ value: 2, label: "oranges" },
	{ value: 3, label: "mangos" },
	{ value: 4, label: "pears" },
	{ value: 5, label: "limes" },
	{ value: 5, label: "cherries" }
];

and then pass it to the <Pie> component:

<Pie data={pieData} />

The data here that we are assigning to pieData is the same data variable that we exported in the first step! Since it’s just a variable, we could have called it anything we wanted as long as we are consistent in our naming. For example, we could have said export let someRandomDataArray = []; in Pie.svelte and then did <Pie someRandomDataArray={pieData} /> and it would be the same thing! We also technically don’t have to put our data into a new variable and can pass our array in directly to the <Pie> component, but imagine how messy that would look if we had more data.

If everything worked well, we should now see the same pie chart (and legend) as before.

Step 3.2: Passing project data via the data prop

Now that we’re passing the data from the Projects page, let’s actually pass our project data into the pie chart.

We will be displaying a chart of projects per year, so the labels would be the years, and the values the count of projects for that year. But how to get from our project data to that array?

D3 does not only provide functions to generate visual output, but also includes powerful helpers for manipulating data. Import d3 to your project page the same way we imported it in our Pie component.

In this case, we’ll use the d3.rollups() function to group our projects by year and count the number of projects in each bucket:

let rolledData = d3.rollups(projects, v => v.length, d => d.year);

This will give us an array of arrays that looks like this:

[
	[ "2024", 3 ],
	[ "2023", 4 ],
	[ "2022", 3 ],
	[ "2021", 2 ],
]

We will then convert this array to the type of array we need by using array.map(). Replace your previous pieData declaration with:

let pieData = rolledData.map(([year, count]) => {
	return { value: count, label: year };
});

That’s it! The result should look like this:

Step 4: Adding a search for our projects and only visualizing visible projects

At first glance, this step appears a little unrelated to the rest of this lab. However, it demonstrates how these visualizations don’t have to be static, but can reactively update with the data, a point we will develop further in the next lab.

Step 4.1: Adding a search field

Let’s add the ability to search by title to our projects page. First, declare a variable that will hold the search query in the <script> element of your projects page:

let query = "";

Then, add an <input type="search"> to the HTML, and bind the value to the query variable (binding means the input element and the variable will always have the same value):

<input type="search" bind:value={query}
       aria-label="Search projects" placeholder="🔍 Search projects…" />

You can print out the variable in your HTML via {query} to make sure the binding works as expected.

Don’t forget to style your search bar to take up the entire width of the page! You can use <style>...</style> tags at the bottom of the file to make the input have a width of 100%.

Step 4.2: Basic search functionality

To filter the project data, we will use the array.filter() function, which returns a new array containing only the elements that pass the test implemented by the provided function.

For example, this is how we’d search through project titles:

let filteredProjects = projects.filter(project => {
	if (query) {
		return project.title.includes(query);
	}

	return true;
});

Add filteredProjects to the JS part of your projects page.

return project.title.includes(query); by itself would have actually worked fine, since if the query is "", then every project title contains it anyway. However, there is no reason to let Svelte do extra computations if we don’t have to.

For this to work, we now need to use filteredProjects instead of projects in our template that displays the projects. Where we use our <Project> component, replace projects in the {#each} block with filteredProjects instead. We only want to create a card for the projects that pass our search filter, not for every project.

If you try this out, you’ll notice that no filtering is actually happening 😔. This is because we are only executing the filtering once, when our search query is empty.

For the filtering to re-run whenever the query changes, we need to make it a reactive statement by using the $: prefix:

$: filteredProjects = projects.filter(project => {
	if (query) {
		return project.title.includes(query);
	}

	return true;
});

If you try it now, filtering should work!

Finding projects by title is a good first step, but it could make it hard to find a project. Also, it’s case-sensitive, so e.g. searching for “svelte” won’t find “Svelte”.

Let’s fix both of these!

Make the search case-insensitive

To do this, we can simply convert both the query and the title to lowercase before comparing them by using the string.toLowerCase() function:

// let filteredProjects;
$: filteredProjects = projects.filter(project => {
	if (query) {
		return project.title.toLowerCase().includes(query.toLowerCase());
	}

	return true;
});

Notice we don’t need to declare filteredProjects separately — the reactive statement $: both defines and updates it when dependencies change.

Search across all project metadata, not just titles

For the second, we can use the Object.values() function to get an array of all the values of a project, and then join them into a single string, which we can then search in the same way:

$: filteredProjects = projects.filter(project => {
	let values = Object.values(project).join("\n").toLowerCase();
	return values.includes(query.toLowerCase());
});

Try it again. Both issues should be fixed at this point.

Step 4.4: Visualizing only visible projects

As it currently stands, our pie chart and legend are not aware of the filtering we are doing. Wouldn’t it be cool if we could see stats only about the projects we are currently seeing?

There are two parts to this:

  1. Calculate pieData based on filteredProjects instead of projects
  2. Make it update reactively.

The first part simply requires us to change the variable we used in d3.rollups(...) for our rolledData from projects to filteredProjects. When you do this, make sure filteredProjects is defined above the rolledData variables or you will get an error.

It might no be working just yet - keep going and it should automagically resolve itself.

The second part involves something we have not yet done: how do we turn something that consists of several lines into a reactive statement? So far we’ve only been prepending single commands with $:!

The answer is that we can use a block statement ({}) to contain multiple commands, and then prepend that with $::

// Make sure the variable definition is *outside* the block
let pieData;

    $: {
		// Initialize to an empty object every time this runs
        pieData = {};
        
		// Calculate rolledData and pieData based on filteredProjects here
        let rolledData = d3.rollups(filteredProjects, v => v.length, d => d.year);

		// We don't need 'let' anymore since we already defined pieData
        pieData = rolledData.map(([year, count]) => {
            return { value: count, label: year };
        });
    }

If you try the search out at this point, you will see that the legend is updating, but the pie chart is not.

This is because none of the calculations in the <Pie> component are actually reactive. We need to make them reactive by separating the variable declarations from the value calculations and using the $: prefix on the calculations just like we just did for pieData. We should do this in the Pie.svelte file where we handle all the logic for our <Pie> component.

We only need to make arcData and arcs reactive, since none of the rest needs to actually change when we filter.

// Define arcData and arcs outside the reactive block
let arcData;
let arcs;

    $: {
		// Reactively calculate arcData and arcs the same way we did before with sliceGenerator and arcGenerator
		arcData = TODO: FILL IN
		arcs = TODO: FILL IN
    }

Once we do that, our pie chart becomes beautifully reactive as well:

(Don’t worry about the weird transitions for the scope of this lab.)

Step 5: Turning the pie into filtering UI for our projects

Visualizations are not just output. Interactive visualizations allow you to interact with the data as well and explore it more effective ways.

In this step, we will turn our pie chart into a filtering UI for our projects, so we can click on the wedge or legend entry for a given year and only see projects from that year.

It will work a bit like this:

Ready? Let’s go!

Step 5.1: Highlighting hovered wedge

While there are some differences, SVG elements are still DOM elements. This means they can be styled with regular CSS, although the available properties are not all the same.

Let’s start by adding a hover effect to the slices. What about fading out all other slices when a slices is hovered? We can target the <svg> element when it contains a hovered <path> by using the :has() pseudo-class inside the <style> part of our Pie.svelte component file:

svg:has(path:hover) path:not(:hover) {
	opacity: 50%;
}

This gives us something like this:

Why not just use svg:hover instead of svg:has(path:hover)? Because the <svg> can be covered without any of the wedges being hovered, and then all wedges would be faded out.

We can even make it smooth by adding a transition property to the <path> elements:

path {
	transition: 300ms;
}

Which would look like this:

Before (left) and after (right) adding the transition

Step 5.2: Highlighting selected wedge

In this step, we will be able to click on a wedge and have it stay highlighted. Its color will change to indicate that it’s highlighted, and its legend item will also be highlighted. Pages using the component should be able to read what the selected wedge is, if any. Clicking on a selected wedge should deselect it.

First, create a selectedIndex prop and initialize it to -1 (a convention to mean “no index”):

export let selectedIndex = -1;

Then, add an on:click event on your <path> to set it to the index of the wedge that was clicked:

{#each arcs as arc, index}
	<path d={arc} fill={ colors(index) }
	      on:click={e => selectedIndex = index} />
{/each}

You may notice that you have a yellow squiggly here indicating an Accessibility warning. These built-in warnings are great reminders from Svelte to think about how accessible our applications are when we make them. We will come back to address warnings like these in Lab 10!

Right now, there is no observable difference when we click on a wedge, since we’re not doing anything with the selectedIndex. Let’s use it to conditionally apply a selected class, that we can then use in our CSS to style selected wedges differently:

{#each arcs as arc, index}
	<path d={arc} fill={ colors(index) }
	      class:selected={selectedIndex === index}
	      on:click={e => selectedIndex = index} />
{/each}

You should apply the exact same class:selected directive to conditionally apply a selected class to the legend items (<li>) as well so that the appropriate legend swatch is also selected.

Then let’s apply CSS to change the color of the selected wedge and legend item. Let’s also fade out any unselected wedges and legend items:

/* When a path is selected, make all non-selected paths 50% opacity */
svg:has(.selected) path:not(.selected) {
   opacity: 50%;
}

.selected {
	--color: oklch(60% 45% 0) !important;
	
	&:is(path) {
		fill: var(--color) !important;
	}
	
	&:is(li) {
		color: var(--color);
	}
}

ul:has(.selected) li:not(.selected) {
	color: gray;
}

Feel free to use any color you want, as long as it’s disctinct from the actual wedge colors.

Why the !important? Because we are trying to override the --color variable set via the style attribute, which has higher precedence than any selector.

But now, when we try to hover, everything is faded out and we can’t see the hover effects anymore. Let’s fix that by ensuring our path is always 100% opacity when hovered by using the !important again:

path:hover {
	opacity: 100% !important;
}

Lastly, we want to be able to deselect a wedge by clicking on it again.

We can conditionally set selectedIndex to -1 if it’s already the index of the selected wedge or to the index of the wedge if we are not currently selecting it, i.e. changing the on-click to

on:click={e => selectedIndex = selectedIndex === index ? -1 : index}

You can improve UX by indicating that a wedge is clickable through the cursor:

path {
	/* ... */
	cursor: pointer;
}

You should have something that looks like this now!

Step 5.3: Filtering the projects by the selected year

Selecting a wedge doesn’t really do that much right now. However, the ability to select a wedge becomes truly powerful when handled by the parent projects page.

In src/routes/projects/+page.svelte, add a variable to hold the selected index:

let selectedYearIndex = -1;

Then bind it to the <Pie> component’s selectedIndex prop:

<Pie data={pieData} bind:selectedIndex={selectedYearIndex} />

Make sure that it works by printing out the selected index in an expression ({selectedYearIndex}) somewhere on the page.

Now define a reactive variable to hold the selected year:

let selectedYear;
$: selectedYear = selectedYearIndex > -1 ? pieData[selectedYearIndex].label : null;

Similarly, print it out somewhere on the page to make sure it works before proceeding.

Now that we have the selected year, we can filter the projects by it!

Our first thought might be to do this filtering by adding another conditional in our array.filter() call from Step 4:

$: filteredProjects = projects.filter(project => {
	if (query) {
		// ...
	}

	if (selectedYear) {
		return project.year === selectedYear;
	}

	return true;
});

However, this will produce an error:

But even if it worked, it would make for some pretty jarring user experience: because we are using the same filteredProjects variable for the pie chart as well, it would make all other years disappear from the pie chart when a year is selected. The only way to select another year would be to deselect the current one.

Instead, we should use another variable to hold the result of filtering by year, e.g. filteredByYear:

$: filteredByYear = filteredProjects.filter(project => {
        if (selectedYear) {
            return project.year === selectedYear;
        }

        return true;
    });

Make sure that filteredByYear is defined under the filteredProjects variable since we need the results of filteredProjects in our filteredByYear variable! We want to use the if statement here to ensure that when no years are selected on the pie chart, we are returning all the projects. This case is slightly different from the if (query) check. In the query case, when there is no value, query="" which means all our projects will pass the filter function. Here, when there is no value for selectedYear, we set it to null. Our projects do not have null values, so they will not automatically pass the filter unless we explicity allow for it.

To display only our filtered by year projects, we need to replace filterdProjects with filteredByYear inside the each block located in the HTML part of our file (i.e {#each filteredProjects as p} <Project .../> {/each}). This change means we are only passing into our Project template the projects that pass our year and search filters.

To avoid the aforementioned circular dependency, make sure to leave filteredProjects as is when we define the reactive pieData variable in the JS section of the file. We do not want to update our pie chart whenever we select a year on the pie chart!

That’s it! It should work now.

Step 6: Setting up for Lab 7

This step takes you through several prepratory steps for Lab 7.

Step 6.1: Adding a new page with meta-analysis of the code in our project

In this lab, we will be computing and visualizing different stats about our codebase. We will display these in a new page on our website. Create a routes/meta/+page.svelte file and add some content in it (e.g. a heading, a description).

Add it to your navigation menu.

Step 6.2: Adding code analysis script

In this step you will install our code analysis script which will analyze the code of our app and display some statistics about it.

If you’re interested in the details of how this script works, you can examine its code in its repo. It’s just some JS code that runs in Node.js :) (and it’s not that long either!)

First, open the terminal and run this, to install the package that will do the analysis:

npm install elocuent -D

Now in your terminal, run this command:

npx elocuent -d static,src -o static/loc.csv

Or this, if you’ve used spaces for indentation (replace 2 with the number of spaces):

npx elocuent -d static,src -o static/loc.csv --spaces 2

If you’re on Windows, make sure you put static,src under quotation marks like this: "static,src"

Make sure your indentation is consistent across your code!

Two very popular tools to ensure a consistent code style are ESLint (JS only) and Prettier (JS, CSS, HTML) They have different philosophies: ESLint is a linting tool: you define what rules you want to follow, and it warns you when you don’t follow them (often it can fix them too, but you need to explicitly ask it to). Prettier is a code formatter: when you hit Save it auto-formats your code based on its predefined rules. Linters give you more control, whereas code formatters are more hands-off but also less flexible.

If everything went well, you should now have a file called loc.csv in the static directory. Its content should look like this (showing first 30 lines):

First 30 lines of loc.csv
file,line,type,commit,author,date,time,timezone,datetime,depth,length
src/app.html,1,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,0,15
src/app.html,2,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,0,16
src/app.html,3,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,1,5
src/app.html,4,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,2,22
src/app.html,5,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,2,26
src/app.html,6,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,2,55
src/app.html,7,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,2,68
src/app.html,8,html,3c2ea132,Lea Verou,2024-03-02,15:26:34,-05:00,2024-03-02T15:26:34-05:00,2,59
src/app.html,9,html,04217ac3,Lea Verou,2024-02-27,14:46:20,-05:00,2024-02-27T14:46:20-05:00,2,64
src/app.html,10,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,2,14
src/app.html,11,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,1,6
src/app.html,12,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,1,41
src/app.html,13,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,2,51
src/app.html,14,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,1,6
src/app.html,15,html,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,0,7
src/routes/+page.svelte,1,svelte,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,0,21
src/routes/+page.svelte,2,svelte,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,0,43
src/routes/+page.svelte,3,svelte,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,0,40
src/routes/+page.svelte,4,svelte,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,1,102
src/routes/+page.svelte,5,svelte,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,1,76
src/routes/+page.svelte,6,svelte,7d3b906,Lea Verou,2024-02-26,01:33:51,-05:00,2024-02-26T01:33:51-05:00,1,39
src/routes/+page.svelte,7,svelte,bdb6236e,Lea Verou,2024-02-26,03:26:16,-05:00,2024-02-26T03:26:16-05:00,0,4
src/routes/+page.svelte,8,svelte,bdb6236e,Lea Verou,2024-02-26,03:26:16,-05:00,2024-02-26T03:26:16-05:00,0,0
src/routes/+page.svelte,9,svelte,bdb6236e,Lea Verou,2024-02-26,03:26:16,-05:00,2024-02-26T03:26:16-05:00,0,8
src/routes/+page.svelte,10,js,04217ac3,Lea Verou,2024-02-27,14:46:20,-05:00,2024-02-27T14:46:20-05:00,0,42
src/routes/+page.svelte,11,js,5c703cf0,Lea Verou,2024-02-27,19:56:10,-05:00,2024-02-27T19:56:10-05:00,0,44
src/routes/+page.svelte,12,js,50612a03,Lea Verou,2024-03-05,11:11:52,-05:00,2024-03-05T11:11:52-05:00,0,68
src/routes/+page.svelte,13,js,50612a03,Lea Verou,2024-03-05,11:11:52,-05:00,2024-03-05T11:11:52-05:00,0,19
src/routes/+page.svelte,14,js,50612a03,Lea Verou,2024-03-05,11:11:52,-05:00,2024-03-05T11:11:52-05:00,1,8

You can find a description of the metadata stored here.

Why are we using CSV instead of e.g. JSON? CSV is more efficient for data that has many rows, since we don’t need to repeat the names of the properties for every row.

Do periodically re-run the script as you work through the lab to see the data update!

Step 6.3: Setting it up so that the CSV file is generated on every build

We want the CSV file to be generated every time we build our app, so that it’s always up-to-date. We can do that by adding a prebuild script to our package.json that runs npx elocuent. Right above this line in package.json:

"build": "vite build",

add:

"prebuild": "npx elocuent -d static,src -o static/loc.csv",

We also need make sure that our build environment (which we specify in deploy.yml) has access to all of our Git history. To do this, open .github/workflows/deploy.yml and modify the Checkout step so that it looks like this:

- name: Checkout
  uses: actions/checkout@v4
  with:
    fetch-depth: '0'

fetch-depth: '0' tells GitHub actions to fetch all history for all branches and tags. By default, the action will only fetch the latest commit, so your deployed scatterplot will only have one dot!

Now, every time we run npm run build, elocuent will be run first.

Step 6.4: Exclude CSV from committed files.

Since we are now generating the script on the server as well, there is no reason to include it in our commits. Add static/loc.csv to your .gitignore file.

If you have already committed it, you will need to first delete the file, commit & push the deletion and the addition to .gitignore, and only after that re-run the script to re-generate it.