Published
Edited
Dec 16, 2021
1 fork
11 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// To start, we'll reimport several of the global variables that were used in the last example.
import {
d3, rocketLaunchesTable,
xScale, yScale,
xAxis, yAxis,
height, width,
bar_color,
margin,
chart_lollipop } from "@hydrosquall/d3js-bar-charts-lollipops-and-nesting"

// If you prefer to work outside the Observable environment, I included a basic example of how to convert
// one of the old barcharts from last time into a regular HTML page, and hosted the example on blockbuilder:
// http://blockbuilder.org/hydrosquall/1352e7dc49aa697dc4b162df21dc297c
// Note that the chart will probably look a little different every day, since the list of rocket launches coming up is not a constant. The GIF contains what the launch set looked like as of 4/15/2018.
Insert cell
Insert cell
// This is a copy of the code from our last lesson:
// https://beta.observablehq.com/@hydrosquall/d3js-bar-charts-lollipops-and-nesting#chart_lollipop
chart_lollipop_local = {
const svg = d3.select(DOM.svg(width, height)); // This is just an Observable pattern. Think of it as "home".
const groups = svg.append("g")
.selectAll("g.lollipop")
.data(rocketLaunchesTable).enter()
.append("g")
.attr("class", "lollipop")
.attr('transform', `translate(10, 0)`); // Shift everything over to the left a bit
// Here our our original rectangles
let scaleFactor = 20;
groups.append("rect")
.attr("x", d => xScale(d.name))
.attr("y", d => yScale(d.num_missions))
.attr("height", d => yScale(0) - yScale(d.num_missions))
.attr("width", xScale.bandwidth() / scaleFactor) // shrink the rectangles to save pixels
.attr("fill", bar_color)
let circleOffset = xScale.bandwidth() / (2 * scaleFactor); // Center the circles
// We can add some circles on top!
groups.append("circle")
.attr("cx", d => xScale(d.name) + circleOffset)
.attr("cy", d => yScale(d.num_missions))
.attr("r", d => 5)
.attr("fill", bar_color)
.append("title")
.text(d => d.name)
svg.append("g")
.call(yAxis)
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart_lollipop_click_all_circles = {
const svg = d3.select(DOM.svg(width, height)); // This is just an Observable pattern. Think of it as "home".
const groups = svg.append("g")
.selectAll("g.lollipop")
.data(rocketLaunchesTable).enter()
.append("g")
.attr("class", "lollipop")
.attr('transform', `translate(10, 0)`); // Shift everything over to the left a bit
// Here our our original rectangles
let scaleFactor = 20;
groups.append("rect")
.attr("x", d => xScale(d.name))
.attr("y", d => yScale(d.num_missions))
.attr("height", d => yScale(0) - yScale(d.num_missions))
.attr("width", xScale.bandwidth() / scaleFactor) // shrink the rectangles to save pixels
.attr("fill", bar_color)
let circleOffset = xScale.bandwidth() / (2 * scaleFactor); // Center the circles
// We can add some circles on top!
groups.append("circle")
.attr("cx", d => xScale(d.name) + circleOffset)
.attr("cy", d => yScale(d.num_missions))
.attr("r", d => 5)
.attr("fill", bar_color)
.append("title")
.text(d => d.name)
// Let's add event listeners to each of the circles
groups.selectAll('circle')
// .attr('fill', 'grey') // Uncomment me to confirm we selected the circles we wanted to.
.on('click', function(d) { // note we have to use REGULAR function (not arrow syntax) for us
// to be able to use d3.select(this)
console.log(d); // we have access to the data object! check your console.
console.log(this); // we also have access to the DOM object...
d3.select(this)
.attr('fill', MATERIAL_BLUE) // we can change color!
.attr('r', d.num_missions * 3) // or size! or really, any visual property we want.
})
svg.append("g")
.call(yAxis)
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof text_color = html`<input type="text" value="#3333ff">`
Insert cell
Insert cell
md`Text size is currently ${text_size} px.`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart_lollipop_click_button = {
const svg = d3.select(DOM.svg(width, height)); // This is just an Observable pattern. Think of it as "home".
const groups = svg.append("g")
.selectAll("g.lollipop")
.data(rocketLaunchesTable).enter()
.append("g")
.attr("class", "lollipop")
.attr('transform', `translate(10, 0)`); // Shift everything over to the left a bit
// Here our our original rectangles
let scaleFactor = 20;
groups.append("rect")
.attr("x", d => xScale(d.name))
.attr("y", d => yScale(d.num_missions))
.attr("height", d => yScale(0) - yScale(d.num_missions))
.attr("width", xScale.bandwidth() / scaleFactor) // shrink the rectangles to save pixels
.attr("fill", bar_color)
let circleOffset = xScale.bandwidth() / (2 * scaleFactor); // Center the circles
// We can add some circles on top!
groups.append("circle")
.attr("cx", d => xScale(d.name) + circleOffset)
.attr("cy", d => yScale(d.num_missions))
.attr("r", d => lollipop_size)
.attr("fill", bar_color)
.append("title")
.text(d => d.name)
// Let's change all the circles with the same country of origin as the original one.
groups.selectAll('circle')
.on('mouseover', function(d) { // note we have to use REGULAR function (not arrow syntax) for us
// to be able to use d3.select(this)
const countryCode = d.location.country;
console.log(countryCode); // we also have access to the DOM object...
groups.selectAll('circle')
.filter(d=> d.location.country != countryCode)
.attr('opacity', 0.1);
}).on('mouseleave', function(d) {
groups.selectAll('circle')
.attr('opacity', 1);
});
svg.append("g")
.call(yAxis);
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart_lollipop_click_all_circles_transition = {
const svg = d3.select(DOM.svg(width, height)); // This is just an Observable pattern. Think of it as "home".
const groups = svg.append("g")
.selectAll("g.lollipop")
.data(rocketLaunchesTable).enter()
.append("g")
.attr("class", "lollipop")
.attr('transform', `translate(10, 0)`); // Shift everything over to the left a bit
// Here our our original rectangles
let scaleFactor = 20;
groups.append("rect")
.attr("x", d => xScale(d.name))
.attr("y", d => yScale(d.num_missions
))
.attr("height", d => yScale(0) - yScale(d.num_missions))
.attr("width", xScale.bandwidth() / scaleFactor) // shrink the rectangles to save pixels
.attr("fill", bar_color)
let circleOffset = xScale.bandwidth() / (2 * scaleFactor); // Center the circles
groups.append("circle")
.attr("cx", d => xScale(d.name) + circleOffset)
.attr("cy", d => yScale(d.num_missions))
.attr("r", d => 5)
.attr("fill", bar_color)
.append("title")
.text(d => d.name)
// Let's add event listeners to each of the circles
groups.selectAll('circle')
.on(`${listen_mode}`, function(d) {
console.log(d); // we have access to the data object! check your console.
console.log(this); // we also have access to the DOM object...
d3.select(this)
.transition()
// .ease(d3.easeLinear) // See also d3.easeCubicInOut, d3.easeQuadInOut
// .duration(750) // 750 is the default duration in milliseconds.
.attr('fill', MATERIAL_BLUE) // we can change color!
.attr('r', d.num_missions * 3) // or size! or really, any visual property we want.
})
svg.append("g")
.call(yAxis)
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// We'll see shortly why we need a new scale. This is based on the old code for xScale that we
// had previously used.
xScaleSorted = d3.scaleBand()
// .domain(rocketLaunchesTable.map(d => d.name)) // old domain (looks for strings)
.domain(d3.range(rocketLaunchesTable.map(d => d.name).length)) // new domain!
.range(xRange)
.padding(0.15) // how much space to put around each bar
Insert cell
chart_lollipop_sort = {
const svg = d3.select(DOM.svg(width, height)); // This is just an Observable pattern. Think of it as "home".
const groups = svg.append("g")
.selectAll("g.lollipop")
.data(rocketLaunchesTable).enter()
.append("g")
.attr("class", "lollipop")
.attr('transform', `translate(10, 0)`); // Shift everything over to the left a bit
let scaleFactor = 20;
groups.append("rect")
.attr("x", d => xScale(d.name))
.attr("y", d => yScale(d.num_missions))
.attr("height", d => yScale(0) - yScale(d.num_missions))
.attr("width", xScale.bandwidth() / scaleFactor) // shrink the rectangles to save pixels
.attr("fill", bar_color)
let circleOffset = xScale.bandwidth() / (2 * scaleFactor); // Center the circles
groups.append("circle")
.attr("cx", d => xScale(d.name) + circleOffset)
.attr("cy", d => yScale(d.num_missions))
.attr("r", d => 5)
.attr("fill", bar_color)
.append("title")
.text(d => d.name);
svg.append("g")
.call(yAxis);
// Let's make a function for sorting things
svg.on('click', ()=> {
svg.selectAll('circle')
.sort((a,b) => d3.ascending(a.num_missions, b.num_missions)) // Challenge: flip orders...
.transition()
.duration(1500)
.attr('cx', (d, i) => xScaleSorted(i));// adjust cx position based on the sort'
});
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart_lollipop_sort_individually = {
const svg = d3.select(DOM.svg(width, height)); // This is just an Observable pattern. Think of it as "home".
const groups = svg.append("g")
.selectAll("g.lollipop")
.data(rocketLaunchesTable).enter()
.append("g")
.attr("class", "lollipop")
.attr('transform', `translate(10, 0)`); // Shift everything over to the left a bit
// Here our our original rectangles
let scaleFactor = 20;
groups.append("rect")
.attr("x", d => xScale(d.name))
.attr("y", d => yScale(d.num_missions))
.attr("height", d => yScale(0) - yScale(d.num_missions))
.attr("width", xScale.bandwidth() / scaleFactor) // shrink the rectangles to save pixels
.attr("fill", bar_color)
let circleOffset = xScale.bandwidth() / (2 * scaleFactor); // Center the circles
// We can add some circles on top!
groups.append("circle")
.attr("cx", d => xScale(d.name) + circleOffset)
.attr("cy", d => yScale(d.num_missions))
.attr("r", d => 5)
.attr("fill", bar_color)
.append("title")
.text(d => d.name);
svg.append("g")
.call(yAxis);
let sortAscending = false;
const sortingFunction = (a,b) => sortAscending ? d3.ascending(a.num_missions, b.num_missions)
: d3.descending(a.num_missions, b.num_missions);
svg.on('click', ()=> {
const t = d3.transition()
.duration(1500);
const perElementDelay = 50;
sortAscending = !sortAscending; // toggle the sort order on every click
svg.selectAll('circle')
.sort(sortingFunction)
.transition(t)
.delay((d, i) => i * perElementDelay)
.attr('cx', (d, i) => xScaleSorted(i) + circleOffset );// adjust cx position based on the sort

svg.selectAll('rect')
.sort(sortingFunction)
.transition(t)
.delay((d, i) => i * perElementDelay)
.attr('x', (d, i) => xScaleSorted(i));// adjust x position based on the sort
});
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more