These are useful concepts to follow a successful carreer in data visualization. There are 4 branches:
All the lab progresses are in the commits.
[!WARNING] Don't merge. Every branch is a tutorial.
The course continues in svelte-MIT-vis.
Installation of tools:
This is a starting point of web development
Other useful courses are 6.813/6.831: User Interface Design & Implementation and 6.S063 Design for the web: Languages and User Interfaces
Relative URL concepts:
If a relative URL starts with a '/'
, it is relative to the root of the host. ..
means "go up one level" and .
refers to the current directory.
Going through web development, remember that it is possible to have <script>
elements without type="module", but we recommend you always use type="module" as it enables a few modern JS features and prevents certain errors.
Error handling in web development
HTML cheatsheet
Create a simple website and publish in GitHub. It should have: introduction text, a photo, some style, a navigation menu and forms.
Every HTML element produces a box.
CSS concepts
display:flex
or display:grid
HTML pages are trees
Styling
Targeting
Pseudo classes
Reactivity
Style your project page
svelte-portfolio/static/style.css
.projects {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
}
svelte-portfolio/static/style.css
article {
grid-template-rows: subgrid;
grid-row: span 1;
}
JS is a language that runs in the browser (but can also run on the server, in native applications, etc.)
The console for debugging
A useful command to interact with dev tools is $$
which give you a list of elements matching a selector
$$("*").length
Video
Read about JS data types and object properties.
In addition to object properties, their values can be functions
How to reference elements
Work with events
select
control when it changes.localStorage.colorScheme = event.target.value
npm is a package manager such as pip or brew
Build process
Traditional architecture
Component base Architecture (cba)
Components
Svelte Performance, Less client-side JS and Easier syntax
Create a vis project
$ npm create svelte@latest my-portfolio
$ npm install && npm install -D svelte@next && npm install -D @sveltejs/adapter-static
Start the server
npm run dev -- --open
To create a production version of your local app:
npm run build
You can preview the production build with npm run preview
.
Something to know
[!TIP] What does these commands lines do? npm install: reads dependencies from package.json and installs the packages listed there. npm install -D svelte@next: will replace the Svelte version already installed with the latest pre-release version (which we need for modern CSS support). npm install -D @sveltejs/adapter-static: will install the static adapter for SvelteKit, which we will use to deploy our website to GitHub Pages
[!WARNING] To deploy your app, you may need to install an adapter for your target environment.
First, copy your images/ folder as well as style.css and global.js to static/.
Adjust src/app.html to read upper files.
Porting your pages to routes:
projects/index.html → routes/projects/+page.svelte
cv/index.html → routes/cv/+page.svelte
contact/index.html → routes/contact/+page.svelte
Adding titles with <svelte:head>
element.
Adjusting navigation bar URLs, delete the trailing slash at the end of your relative URLs in global.js
.
Add and edit these files in your project then push to deploy
.github/workflows/deploy.yml
(https://vis-society.github.io/labs/4/download/deploy.yml)
svelte.config.js
(https://vis-society.github.io/labs/4/download/svelte.config.js).
Move your data projects to /src/lib/projects.json
[!NOTE] A useful code to extract data from dev tools. Put it in the console log.
$$(".projects > article").map (a => ({
title: $('h2', a).textContent,
image: $("img", a).getAttribute("src"),
description: $("p", a).textContent,
}));
Template your project data with
{#each projects as p}
<article>
...
</article>
{/each}
Load your data in src/routes/projects/+page.svelte.
Be able to indicate the total count of projects.
Create and use a Project component.
This line is useful only to declare a prop variable In Project.svelte
export let data = {};
Project.svelte goes into components folder
Use the <Project>
component to show the lastest 3 with slice.
svelte-portfolio/src/+page.svelte
<h1>
Latest Projects
<div class="projects">
{#each projects.slice(0, 3) as p}
<Project data={p} hLevel="3" />
{/each}
</div>
</h1>
Set the hierarchy of the component using props and move styles from svelte-portfolio/static/style.css to the component itself in <style>
element.
Create a Layout for nav.
+layout.svelte
global.js
to +layout.svelte
<slot />
inside the layout, the page is gonna to be displayed inside it.$page
from $app/stores
to load page properties and detect the current page link. [!IMPORTANT]
$
recalculates when dependencies change.
[!IMPORTANT]
bind:value={a}
updates the UI when the input changes
<script>
let a = 1, b = 2, c;
$: c = a + b;
</script>
<input type="number" id="a" bind:value={a}> +
<input type="number" id="b" bind:value={b}> =
<input type="number" id="c" bind:value={c} disabled>
“Bind” in this context means that the value of the variable will be automatically updated when the value of the html element changes, and vice versa.
Html element is not part of the svelte technology. So that's why document is undefined and console.log(document)
throws an error 500 internal error
.
[!TIP] Explanation: Our Svelte code is first ran in Node.js to generate the static parts of our HTML, and the more dynamic parts make it to the browser. However, Node.js has no document object, let alone a document.documentElement object.
When accessing properties of objects of questionable existence, we can use the optional chaining operator ?. instead of the dot operator to avoid errors
Set
colorScheme
variable to the "color-scheme" property in the document
let root = globalThis?.document?.documentElement;
$: root?.style.setProperty("color-scheme", colorScheme);
Use $ for reactivity statement.
Save the color-scheme to localStorage and get it again to keep the theme switcher in other pages
let localStorage = globalThis.localStorage ?? {};
$: localStorage.colorScheme = colorScheme;
For preventing FOUC (Flash of Unstyled content), add the script
code to the app.html
A strange behavior when theme changes after the page has loaded.
This code set the theme before the page has loaded.
app.html
<script type="module">
let root = document.documentElement;
let colorScheme = localStorage.colorScheme ?? "light dark";
root.style.setProperty("color-scheme", colorScheme);
</script>
Images of the results
Use an API to extract github stats data. An example fromhttps://api.github.com/users/your-username
.
fetch is an example of an asynchronous function. This means that it does not return the data directly, but rather a Promise that will eventually resolve to the data. In fact, fetch() returns a Promise that resolves to a Response object, which is a representation of the response to the request. To get meaningful data from a Response object, we need to call one of its methods, such as json(), which returns a Promise that resolves to the JSON representation of the response body.
Extract a promise
let promise = fetch("https://api.github.com/users/leaverou").then(
(response) => response.json(),
);
Use the #await
block for managing promises.
{#await promise}
Loading...
{:then data}
The data is {data}
{:catch error}
Something went wrong: {error.message}
{/await}
Use <dl>
, <dt>
and <dd>
tags and styles to custom display information like the picture.
Update the year of your projects.
No slides but useful information for this lab
<Sunburst {data} width=1152 height=1152 />
instead of Sunburst(data, {width: 1152, height: 1152, ...})
Create a Pie.svelte component in src/lib
.
import Pie from '$lib/Pie.svelte'
Create a circle svg.
<path>
element to draw a circled3.arc()
function to generate the path for our circle and update the d value in <svg
. Change innerRadius(r)
, where r
is the radius of the innercircle if you want a donut chart.d3.pie()
function .scaleOrdinal
with d3.schemeTableau10
to generate a color function and change the path property value colors[i]
by color(i)
because this is a function now.
<span>
is an inline element by default, so to get widths and heights to work you need to set this in the parentspan {
display: inline-block;
}
span
circles, they are too big
[
[ 2023, 3 ],
[ 2021, 2 ],
[ 2020, 3 ],
[ 2019, 3 ],
[ 2018, 1 ]
]
[
{ value: 3, label: 2023},
{ value: 2, label: 2021},
{ value: 3, label: 2020},
{ value: 3, label: 2019},
{ value: 1, label: 2018}
]
Results
6.4.1 Add an <input type="search">
for basic search functionality showing only visible projects
6.4.2 Make search case-insentive and search across all project metadata not just titles. We can use the Object.values()
function to get an array of all the values of a project.
6.4.3 Visualizing only visible objects with the use of reactivity in variables that change in the page and in the component. You can use multiple reactivity commands with {}
.
Worked more on interactive visualizations
SVG elements are still DOM elements so they can be styled with regular CSS but the available properties are not the same.
6.5.1 Highlighting hovered wedge. It is important to add another svg style to avoid a huge pie.
Build a selected wedge and a selected legend with a click. The point is to catch the sector of the pie, add a conditional selected
class and then manipulate by CSS.
Filtering the projects by clicking the pie or legend. So, hold the selected index (selectedYearIndex
) and bind it to the <Pie>
component's selectedIndex
prop.
Then define reactive variables to hold the selected year (selectedYear
) and filter the projects (filteredByYear
), use this to display them.
These issues are related to path
and span
elements:
Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler...
or
... with a click handler must have an ARIA role
To fix them, make them focusable changing attributes like tabindex="0"
, expose it as a button role="button"
and use a custom event listener called toggleWedge
.
An unexpected rectangular line will appear, hide it with outline:none
.
Finally modify the svg because keyboard users have no way to know which wedge they have currently focused, which is a terrible user experience.
IMPORTANT about SVG: Shapes are painted in the order they appear in the source code, and unlike in HTML, there is no way to change this order with CSS. So decorations like strokes or shadows will work nicely for one of the wedges and fail miserably for the others. Look after!
No slides
deploy.yml
has access to all Git history. fetch-depth: '0'
tells GitHub actions to fetch all history for all branches and tags via actions/checkout@v4
.onMount
runs after the DOM and d3.csv is used to import the csv file
Sumarizing per commit, computing aditional keys and using Object.defineProperty
to add a property key containig the raw data at the end of the object.
Aggregate stats with group, max, rollups
d3.group(lines, (d) => d.file).size
d3.max(lines, (d) => d.depth)
d3.rollups( data, (v) => d3.max(v, (v) => v.line), (d) => d.file, )
Set plot core dimensiones width, height and margins according to rule conventions.
Set scales of x and y domains.
Add circle
element and sets their positions using these scales.
Divide the plot into svg groups g
for "dots", "axisX" and "axisY".
Use transfom
property of g
element to translate both axis.
Adjust the axisY to look like hours, it use string.padStart()
which formats it as a two digit number and finally, we append ":00" to it to make it look like a time.
Add horizontal grid lines, this time with no text and use the axis.tickSize()
method to make the lines extend across the whole chart
d3.select(yAxisGridlines)
.call(d3.axisLeft(yScale)
.tickFormat("")
.tickSize(-usableArea.width))
Use a hoveredIndex
reactive variable to hold the index of the hovered commit. It holds the data we want to display in the tooltip.
Add mouseenter
and mouseleave
event listeners inside the scatter plot circles.
Add a class info
to do some styling with <dl>
, <dt>
, and <dd>
elements.
fixed means relative to the viewport and absolute means relative to its nearest ancestor. An example
.tooltip {
position: fixed;
top: 1em;
left: 1em;
}
Animations
circle {
transition: 10ms; // <- play with transition
&:hover {
transform: scale(1.5); // <- expand the dot scale
transform-origin: center; // <- fix hover weirdness thing (appears when you translate a visible object to a different position of the default top-left coordinate system, like a tooltip for example)
transform-box: fill-box; // <- fix weirdness thing(works in conjunction with the property above)
}
}
hovered weirdness
hovered fixed
Style the tooltip with this properties background-color
, box-shadow
, border-radius
, backdrop-filter
and padding
.
Making only appear when we are hovering over a dot by using the hidden attribute
<dl class="info" hidden={hoveredIndex === -1}>
and then use this attribute in css for hidding and transition effects.
dl.info {
/* ... other styles ... */
transition-duration: 500ms;
transition-property: opacity, visibility;
&[hidden]:not(:hover, :focus-within) {
opacity: 0;
visibility: hidden;
}
}
Results
Position the tooltip with the mouse based position using events. So in circle
mouseenter event get the event.x
and event.y
pixel coordinates and pass them to tooltip inside dl
element
<dl class="info" hidden={hoveredIndex === -1} style="top: {cursor.y}px; left: {cursor.x}px">
Usually avoid setting default CSS properties. So do settings directly like style in
dl
element.
The tooltip built only works if the mouse is near the center of the viewport. Near the edges the tooltip fails and it shows cutten. Here is an image of a cutten tooltip.
The solution is really complicated, it is better to save our time using a tool called @floating-ui/dom. So install it!. This works only with DOM elements.
Move event listeners code to dotInteraction (index, event)
async function.
bind:this
is a way to assign an element to a variable.
Fix accesibility issues, like tabindex, role etc Fix these issues
Add the number of lines as a third variable and map to the radius size.
Create a scale using d3.scaleSqrt()
method and apply some opacity to dots.
Linear scale vs Quadratic scale
Sort in descending order to overlap small over large dots for easily hovering. Use d3.sort(commits, d => -d.totalLines)
. The signs indicates, descending.
Results
See A Tour through the Interaction Zoo for viz examples.
d3.select(svg).call(d3.brush())
With brushing our tooltip doesn't work inicially because D3 adds a rectangle overlay over the entire chart that catches all mouse events. Because of this, our circles never get hovered, and thus our tooltips never show.
To fix this we need the overlay to come before the dots in the DOM tree.
D3 provides a selection.raise()
method that moves one or more elements to the end of their parent, maintaining their relative order.
d3.select(svg).selectAll(".dots, .overlay ~ *").raise();
The ~ is the CSS subsequent sibling combinator and it selects elements that come after the selector that precedes it (and share the same parent).
The overlay is not the only element added by d3.brush(). There is a <rect class="selection">
element that is used to depict the brush selection. So use CSS to style it!
Use global()
pseudo-class and animation to style your selection.
Follow this steps:
Figure it out what the user has selected in terms of shapes (dots) and data (commits). Using on()
to listen events fired by d3.brush()
.
When brushing, the selection
property is an array of two points that represent the top-left and bottom-right corners of the brush rectangle. Use a reactive variable called brushSelection
to store these points and use a function isCommitSelected
to check if a commit is inside this rectangule.
Then we use isCommitSelected
function to apply a selected
class to the dots that are selected via a class:selected directive.
Finally style the dot .selected
to change colors of the selected shapes.
Use brushSelection to count the commits
Let’s display stats about the proportion of languages in the lines edited in the selected commits.
You need a reactive variable called languageBreakdown
that aggregates the number of lines per language of the reactive variable selectedLines
which are the commits selected.
Then iterate over languageBreakdown
{#each languageBreakdown as [language, lines] }
<!-- Display stats here -->
{/each}
<dl>
element is outside #each section to make the grid work properly.
Results
Use the Pie component
Results
If you change public to private, you will miss your deploy. If you need to recover again change to public again and go to settings and choose Github Actions in Build and deployment section.
The project of this lab was moved to svelte-bikewatch.
Interpolation: Intermediate states from A to B.
Progression: What advances the timeline.
[!NOTE] The lab consists in converting meta page to an interactive narrative visualization that shows the progress of our codebase over time.
Currently, the only way to select the commits is by brusing the pie chart. But we need another way to make it easier.
Refactor brushedSelection
and isCommitedSelected
.