Public
Edited
Aug 26, 2024
Insert cell
Insert cell
d3 = require("d3@7")
Insert cell
Insert cell
data = d3.csvParse(await FileAttachment("AB_NYC_2019@2.csv").text(), d3.autoType)
Insert cell
Insert cell
function processDataSimpleJS(){
// This function shows how to process data with minimum reliance on built-in JS functions.

// Step 1: Iterate through the listings (array), for each neighborhood, we update the count of listings and the cumulative sum of listing price.
let neighborhoodGroups = {}; // object
for (let entry of data){ // iterate through elements in an array
if (entry.neighbourhood_group in neighborhoodGroups){
neighborhoodGroups[entry.neighbourhood_group].count = neighborhoodGroups[entry.neighbourhood_group].count + 1;
neighborhoodGroups[entry.neighbourhood_group].sumPrice = neighborhoodGroups[entry.neighbourhood_group].sumPrice + entry.price
} else {
let initGroup = {
name: entry.neighbourhood_group,
count: 1,
sumPrice: entry.price,
}
neighborhoodGroups[entry.neighbourhood_group] = initGroup;
}
}

// Step 2: Iterate through the object and compute the average price for each neighborhood.
let result = [];
for (let key in neighborhoodGroups){ // iterate through keys in an object
let processed = {
name: key,
average: neighborhoodGroups[key].sumPrice / neighborhoodGroups[key].count,
}
result.push(processed)
}

// Step 3: Sort the array (result) by the name
result.sort(function(a, b){
if(a.name < b.name) return -1
if(a.name > b.name) return 1
return 0;
});
// If you want to sort by value
//result.sort((a, b) => a.average - b.average)

return result
}
Insert cell
function processDataJS(){
// This function is the equivalent to above, but we rely on JS built-in functions.
// Each step matches with those in `processDataSimpleJS`.

// Step 1: This returns an object, each key is one neighborhood, its value is an array that stores all the listings in that neighborhood.
let neighborhoodGroups = Object.groupBy(data, (d) => d.neighbourhood_group)

// Step 2: Object.entries(iterable) returns [[key1, val1], [key2, val2], ...];
// Array.map(callback) iterates through each element (ele = [key, val] in this case) in the array, where something expects to be returned as the new value (we return an object to replace [key, val] at each index in this case). If you don't want the return behavior, check out Array.forEach(callback).
let result = Object.entries(neighborhoodGroups).map(([key, listings]) => {
let sumPrice = listings.map(d => d.price).reduce((sum, currentValue) => sum + currentValue, 0)
return ({
name: key,
average: sumPrice / listings.length
})
})

// Step 3: Sort the array (result) by the name
result.sort((a, b) => a.name.localeCompare(b.name))
// If you want to sort by value
//result.sort((a, b) => a.average - b.average)

return result
}
Insert cell
function processDataD3(){
// This function is the equivalent to those above, but we rely on d3.js functionalities.
// Each step matches with those in `processDataSimpleJS` and `processDataJS`.

// Steps 1 and 2
// d3.rollups(iterable, callback, key1) takes care of Step 1; The callback takes care of Step 2. The follow-up Array.map() exists for just formatting
let result = d3.rollups(data, listings => d3.mean(listings, d => d.price), d => d.neighbourhood_group).map(([key, average]) => ({name: key, average: average}))

// Step 3
result = d3.sort(result, (d) => d.name)
//result = d3.sort(result, d => d.average)

return result
}
Insert cell
chartData = processDataD3(data)
//chartData = processDataSimpleJS(data)
//chartData = processDataJS(data)
Insert cell
Insert cell
Insert cell
margin = ({top: 40, right: 40, bottom: 40, left: 120}) //in screen pixels
Insert cell
height = 400 //in screen pixels
Insert cell
chart = {
/* Step 1: Create the svg element that will be used to draw our chart,
the height and width (in observable) can be declared as global variables which we can edit as needed
for this example.
Right below this block the height variable is located, the width scales accordingly in
observable but you can tinker with it to see what it does.
*/
const svg = d3.create("svg").attr("viewBox", [0,0,width,height]);

/* Step 2: Set up our scales
For this example, we are viewing categorical data (neighborhood groups)
along with their associated average price (quantitative data).
We need a way to map our data to where it should be on the screen, specifically where it would be when rendered within the svg.
To achieve this, we use d3's scale functions, precisely their scaleLinear() for
our quantitative data and scaleBand() for the categorical data.
What the scale does is it maps a range of data values (domain) to a position within the svg (range).
*/
//Set up our X axis scale
/*
A good explanation on scaleLinear (https://observablehq.com/@d3/d3-scalelinear)
Usually, the domain will be in the observation’s unit (Celsius degrees, US dollars, seconds…),
and the range in screen or print unit (pixels, millimeters… or even CSS colors).
(Margin is a global var specified below this block, it is used a bounding rect
which we use to position and adjust our chart.
*/
const x = d3.scaleLinear()
.domain([0, d3.max(chartData, d => d.average)]) //our average price data ranges from 0 - Max
.range([margin.left, width - margin.right]);
//Set up our Y axis scale
/*
Band scales are convenient for charts with an ordinal or categorical dimension.
The domain is the list neighborhoods.
The range — the chart’s width in pixels — is evenly distributed among the neighborhoods, which are
separated by a small gap.
*/
const y = d3.scaleBand()
.domain(chartData.map(d => d.name))
.rangeRound([margin.top, height - margin.bottom])
.padding(0.1);
/* Step 3: Draw the chart
https://observablehq.com/@d3/selection-join
*/
svg.selectAll("rect") //Selects all defined elements in the DOM and hands off a reference to the next step in the chain.
.data(chartData) //connect chart data with DOM <rect/> elements
.join("rect") // appends a new SVG rect element for each element in our chartdata array.
.attr('x', (d) => x(0)) //since this is a horizontal bar chart we start the bottom of the chart at the left
.attr('y', (d) => y(d.name)) // bar y position
.attr("height", y.bandwidth()) // height of the bar so they all stack nicely near eachother
.attr("width", (d) => x(d.average) - x(0)) // height of the bar so they all stack nicely near eachother
.attr("fill","green"); // color of the bar

/* Step 4: labeling */
//initialize the location for the X Axis
const xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
// initialize the location of the Y axis
const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
// append each to the svg so they will be rendered
svg.append("g")
.call(xAxis)
// x-axis label
.call(g =>
g.select(".tick:last-of-type text").clone()
.attr("text-anchor", "middle")
.attr("x", -(width - margin.left - margin.right) / 2)
.attr("y", margin.bottom - 10)
.attr("font-weight", "bold")
.text("Average Price")
);

svg.append("g")
.call(yAxis)
// y-axis label
.call(g =>
g.select(".tick:last-of-type text").clone()
.attr("transform", `rotate(-90)`)
.attr("text-anchor", "middle")
.attr("x", (height/2) - margin.bottom - margin.top)
.attr("y", -margin.left + 20)
.attr("font-weight", "bold")
.text("Neighbourhood Group")
);
//return the svg for rendering in observable.
return svg.node();
}
Insert cell
Insert cell
Plot.plot({
marks: [
Plot.barX(chartData, {x: "average", y: "name", fill: 'green'}),
Plot.ruleX([0]),
Plot.axisX({label: "Average Price", marginBottom: 40, fontWeight: 'bold'}),
Plot.axisY({label: "Neighbourhood Group", marginLeft: 100, fontWeight: 'bold'}),
]
})
Insert cell
Insert cell
import {vl} from '@vega/vega-lite-api'
Insert cell
vl.markBar()
.data(data)
.encode(vl.y().field('neighbourhood_group').type('nominal'),
vl.x().field('price').aggregate('average').type('quantitative'),
vl.color().value('green'))
.render()
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more