Notebooks 2.0 is here.

Public
Edited
Dec 7, 2023
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
shootings.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
import {Legend,legend, swatches} from "@d3/color-legend"
Insert cell
data = FileAttachment("shootings.csv").csv()
Insert cell
data_days = d3.rollups(
data,
group => group.length,
d => d.date
)
Insert cell
data_month = d3.rollups(
data,
group => group.length,
d => d.date.substring(0,7)
)
Insert cell
data_year = d3.rollups(
data,
group => group.length,
d => d.date.substring(0,4)
)
Insert cell
years = d3.map(data_year, d=>d[0])
Insert cell
year_race = {
data.forEach(d => d.year = d.date.split('-')[0]);

const countByYearRace = {};
data.forEach(d => {
const key = d.year + "_" + d.race;
if (!countByYearRace[key]) {
countByYearRace[key] = { year: d.year, race: d.race, count: 0 };
}
countByYearRace[key].count += 1;
});


let result = Object.values(countByYearRace);

result = result.sort((a, b) => {
if (a.year === b.year) {
return a.race.localeCompare(b.race);
}
return a.year.localeCompare(b.year);
});

return result
}
Insert cell
year_age = {
// Define age groups order
const ageGroupOrder = ['Children', 'Teenagers', 'Young Adults', 'Adults', 'Middle-aged Adults', 'Seniors', 'Elderly'];

// Helper function to categorize age
function categorizeAge(age) {
if (age <= 12) return 'Children';
if (age <= 19) return 'Teenagers';
if (age <= 29) return 'Young Adults';
if (age <= 39) return 'Adults';
if (age <= 54) return 'Middle-aged Adults';
if (age <= 64) return 'Seniors';
return 'Elderly';
}

// Extract year and categorize age
data.forEach(d => {
d.year = d.date.split('-')[0];
d.ageGroup = categorizeAge(d.age);
});

// Group and count
const countByYearAgeGroup = {};
data.forEach(d => {
const key = d.year + "_" + d.ageGroup;
if (!countByYearAgeGroup[key]) {
countByYearAgeGroup[key] = { year: d.year, ageGroup: d.ageGroup, count: 0 };
}
countByYearAgeGroup[key].count += 1;
});

// Convert to array and sort
let result = Object.values(countByYearAgeGroup);
result = result.sort((a, b) => {
if (a.year === b.year) {
return ageGroupOrder.indexOf(a.ageGroup) - ageGroupOrder.indexOf(b.ageGroup);
}
return a.year.localeCompare(b.year);
});

return result;
}
Insert cell
series_age ={//
const ageGroups = Array.from(new Set(year_age.map(d => d.ageGroup)));


const dataByYear = Array.from(d3.group(year_age, d => d.year), ([year, values]) => {
const yearData = { year };
ageGroups.forEach(ageGroup => {
yearData[ageGroup] = values.filter(v => v.ageGroup === ageGroup).reduce((sum, v) => sum + v.count, 0);
});
return yearData;
});

const series_age = d3.stack()
.keys(ageGroups)
.value((d, key) => d[key])
(dataByYear);

series_age.forEach(layer => {
layer.forEach(segment => {
const year = segment.data.year;
const ageData = year_age.find(d => d.year === year && d.ageGroup === layer.key);
segment.data = [year, new Map([[layer.key, ageData]])];
});
});


return series_age
}
Insert cell
series_race = d3.stack()
.keys(d3.union(year_race.map(d => d.race))) // distinct series keys, in input order
.value(([, D], key) => D.get(key).count) // get value for each series key and stack
(d3.index(year_race, d => d.year, d => d.race)); // group by stack then series key
Insert cell
Insert cell
Insert cell
Insert cell
typeof(years[0])
Insert cell
months =
{let months = [];
for (let i = 0; i < 12; i++) {
let date = new Date(2020, i, 1);
let month = date.toLocaleString('en-US', { month: 'long' });
months.push(month);
}
return months}
Insert cell
monthData = {let aggregatedData = [];


data.forEach(item => {
let date = new Date(item.date);
let year = date.getFullYear().toString();
let month = date.toLocaleString('en-US', { month: 'long' });

let yearData = aggregatedData.find(y => y.index === year);
if (!yearData) {
yearData = { index: year };
months.forEach(m => yearData[m] = 0);
aggregatedData.push(yearData);
}

yearData[month]++;
});


return aggregatedData
}
Insert cell
month_Changes = {let monthDataChanges = [];

for (let i = 0; i < monthData.length; i++) {
let yearData = { index: monthData[i].index };
for (let j = 0; j < months.length; j++) {
const month = months[j];
const previousMonth = j === 0 ? "December" : months[j - 1];
const previousYear = j === 0 ? i - 1 : i;

if (i === 0 && j === 0) {
yearData[month] = 0;
} else {
const previousValue = previousYear >= 0 ? monthData[previousYear][previousMonth] : 0;
const change = monthData[i][month] - previousValue;
yearData[month] = change;
}
}
monthDataChanges.push(yearData);
}

return monthDataChanges;
}
Insert cell
monthData[5].June = 38
Insert cell
max_value = 106
Insert cell
fun1 = ()=> {
const width = 700;
const height = width+130;
const margin = ({top: width/10, right: width/10, bottom: width/5, left: width/5});

const squareSize = 50;

const xRange = [margin.left, margin.left + squareSize * years.length+16];
const yRange = [height - margin.bottom+20, height - margin.bottom - squareSize * months.length];


const x = d3.scaleBand()
.domain(years)
.range(xRange)
.padding(0.03);

const y = d3.scaleBand()
.domain(months)
.range(yRange)
.padding(0.03);




const color = d3.scaleSequential()
.domain([0,max_value])
.interpolator(d3.interpolateYlOrBr)

const xAxis = g => g
.attr("transform", `translate(0, ${height - margin.bottom+25})`)
.call(d3.axisBottom(x).tickSizeOuter(0))
.call(g => g.selectAll(".domain").remove())
.selectAll("text")
.style("text-anchor", "start")
.style("font-size", "16px")
.attr("dx", "-.8em")
.attr("dy", ".8em")
const yAxis = g => g
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y).ticks(null, "s"))
.call(g => g.selectAll(".domain").remove())
.selectAll("text")
.style("font-size", "14px")
.text(d => d)


const svg = d3.select(DOM.svg(width, height))
.style("width", "100%")
.style("height", "auto")
.style("font", "1rem verdana");
const make_class = (item) => item.toLowerCase().split(' ').join('_').split('-').join('')
const make_id = d => `coords_${Math.floor(x(d.xval))}_${Math.floor(y(d.yval))}`
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.style("position", "absolute")
.style("text-align", "center")
.style("padding", "8px")
.style("font", "12px sans-serif")
.style("background", "lightsteelblue")
.style("border", "0px")
.style("border-radius", "8px")
.style("pointer-events", "none");


const rects = svg.append("g")
.selectAll("g")
.data(monthData)
.enter().append("g")
.attr("class", (d, i) => `${i} bar`)
.selectAll("g")
.data(d => {
const innerData = [];
for (const month in d) {
if (month !== "index") {
innerData.push({
xval: d.index,
yval: month,
count: d[month]
});
}
}
console.log(innerData)
return innerData;
})
.enter().append("g");




rects.append("rect")
.attr("x", d => x(d.xval))
.attr("y", d => y(d.yval))
.attr("width", squareSize)
.attr("height", squareSize)
.style("fill", d => d.count === 0 ? "lightgray" : color(d.count))
.on("mouseover", function(event, d) {
tooltip.transition()
.duration(200)
.style("opacity", 0.9);
tooltip.html(`${d.yval}<br/>${d.xval}<br/>Shootings: ${d.count}`)
.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");

d3.select(this)
.style("stroke", "white")
.style("stroke-width", 4);
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
d3.select(this).style("stroke", "none");
})
;



svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);

return svg.node();



}
Insert cell
max = 61
Insert cell
fun2 = ()=>{
const width = 700;
const height = width+ 130;
const margin = ({top: width/10, right: width/10, bottom: width/5, left: width/5-50});
const max_value = 61


const squareSize = 50;

const xRange = [margin.left, margin.left + squareSize * years.length+16];
const yRange = [height - margin.bottom+20, height - margin.bottom - squareSize * months.length];


const x = d3.scaleBand()
.domain(years)
.range(xRange)
.padding(0.03);

const y = d3.scaleBand()
.domain(months)
.range(yRange)
.padding(0.03);






const color = d3.scaleSequential()
.domain([max_value, -max_value])
.interpolator(d3.interpolatePiYG)


const xAxis = g => g
.attr("transform", `translate(0, ${height - margin.bottom+25})`)
.call(d3.axisBottom(x).tickSizeOuter(0))
.call(g => g.selectAll(".domain").remove())
.selectAll("text")
.style("text-anchor", "start")
.style("font-size", "16px")
.attr("dx", "-.8em")
.attr("dy", ".8em")
const yAxis = g => g
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y).ticks(null, "s"))
.call(g => g.selectAll(".domain").remove())
.selectAll("text")
.style("font-size", "14px")
.text(d => d)


const svg = d3.select(DOM.svg(width, height))
.style("width", "100%")
.style("height", "auto")
.style("font", "1rem verdana");
const make_class = (item) => item.toLowerCase().split(' ').join('_').split('-').join('')
const make_id = d => `coords_${Math.floor(x(d.xval))}_${Math.floor(y(d.yval))}`
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.style("position", "absolute")
.style("text-align", "center")
.style("padding", "8px")
.style("font", "12px sans-serif")
.style("background", "lightsteelblue")
.style("border", "0px")
.style("border-radius", "8px")
.style("pointer-events", "none");


const rects = svg.append("g")
.selectAll("g")
.data(month_Changes)
.enter().append("g")
.attr("class", (d, i) => `${i} bar`)
.selectAll("g")
.data(d => {
const innerData = [];
for (const month in d) {
if (month !== "index") {
innerData.push({
xval: d.index,
yval: month,
count: d[month]
});
}
}
console.log(innerData)
return innerData;
})
.enter().append("g");




rects.append("rect")
.attr("x", d => x(d.xval))
.attr("y", d => y(d.yval))
.attr("width", squareSize)
.attr("height", squareSize)
.style("fill", d => d.count === 0 ? "lightgray" : color(d.count))
.on("mouseover", function(event, d) {
tooltip.transition()
.duration(200)
.style("opacity", 0.9);
tooltip.html(`${d.yval}<br/>${d.xval}<br/>Changes: ${d.count}`)
.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");

d3.select(this)
.style("stroke", "white")
.style("stroke-width", 4);
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
d3.select(this).style("stroke", "none");
});
svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);

return svg.node();



}
Insert cell
legend1 = ()=>{
return Legend(d3.scaleSequential([0, max_value], d3.interpolateYlOrBr), {
title: "Shooting number",
})
}
Insert cell
legend2 = ()=>{
return Legend(d3.scaleSequential([max,-max], d3.interpolatePiYG), {
title: "Shooting number changes",
})
}
Insert cell
{
const ages = fun1();
const barchart_gender = fun2()
const legend_2 = legend2()
const legend_1 = legend1();
return html`

<div style="display: flex; width: 120%; max-width: 1500px; margin: auto; height: 100%; ">

<div style="width: 50%; padding: 0px; margin-left:3% ">
${legend_1}
</div>
<div style="width: 50%; padding: 0px;">
${legend_2}
</div>
</div>
<div style="display: flex; width: 120%; max-width: 1500px; margin: auto; height: 100%; ">

<div style="width: 50%; padding: 0px;">
${ages}
</div>
<div style="width: 50%; padding: 0px;">
${barchart_gender}
</div>
</div>
`;



}
Insert cell
{
const height = width;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const margin = ({top: 20, right: 30, bottom: 80, left: 40});
const x = d3.scaleBand()
.domain(data_month.map(d => d[0]))
.rangeRound([margin.left, width - margin.right])
.padding(0.1)

// const y = d3.scaleLinear()
// .domain([0, d3.max(data_month, d => d[1])])
// .rangeRound([height - margin.bottom, margin.top])

const y = d3.scaleLinear()
.domain([20, d3.max(data_month, d => d[1])]) // 将起始值设为 20
.rangeRound([height - margin.bottom, margin.top]);
// const xAxis = g => g
// .attr("transform", `translate(0,${height - margin.bottom})`)
// .call(d3.axisBottom(x)
// // .tickValues(d3.ticks(...d3.extent(x.domain()), width / 40).filter(v => x(v) !== undefined))
// .tickSizeOuter(0))

const xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x)
.tickValues(data_month.map(d => d[0]).filter(v => v.endsWith("-01") || v.endsWith("-07")))
.tickSizeOuter(0))

const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.style("color", "steelblue")
.call(d3.axisLeft(y).ticks(null, "s"))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(data_month.y))

svg.append("g")
.attr("fill", "steelblue")
.attr("fill-opacity", 0.8)
.selectAll("rect")
.data(data_month)
.join("rect")
.attr("x", d => x(d[0]))
.attr("width", x.bandwidth())
.attr("y", d => y(d[1]))
.attr("height", d =>(y(20) - y(d[1])));



svg.append("g")
.attr("fill", "none")
.attr("pointer-events", "all")
.selectAll("rect")
.data(data_month)
.join("rect")
.attr("x", d => x(d.year))
.attr("width", x.bandwidth())
.attr("y", 0)
.attr("height", height)
// .append("title")
// .text(d => `${d.year}
// ${d[0].toLocaleString("en")} new cars sold`);

svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);



return svg.node();
}
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