Public
Edited
Apr 11
Importers
Insert cell
Insert cell
import {mapboxD3} from "@john-guerra/mapbox-d3"
Insert cell
d3 = require('d3@5')
Insert cell
import {mpd_districts_data_upload_geojson} from "e6e705ed437b46c6"
Insert cell
import {ss_cleaned_data_geojson} from "e6e705ed437b46c6"
Insert cell
import {mpd_district_data_cleaned} from "e6e705ed437b46c6"
Insert cell
import {mpd_district_names} from "e6e705ed437b46c6"
Insert cell
import {ss_alerts_per_year} from "e6e705ed437b46c6"
Insert cell
import {ss_data_upload_record_count} from "e6e705ed437b46c6"
Insert cell
import {ss_percent_by_district} from "e6e705ed437b46c6"
Insert cell
import {mpd_geojson_with_race} from "ebeb9669a8cadc1e"
Insert cell
Insert cell
Insert cell
Insert cell
// width = 800;
Insert cell
height = 600;
Insert cell
Insert cell
Insert cell
dcProjection = d3.geoAlbers()
.scale( 290000 )
.rotate( [77,0] )
.center( [0, 37.66] )
.translate( [width/2,height/2] );
Insert cell
// Create GeoPath function that uses built-in D3 functionality to turn
// lat/lon coordinates into screen coordinates
dc_geoPath = d3.geoPath()
.projection( dcProjection );
Insert cell
dcProjection1 = d3.geoEquirectangular()
.scale( 29000 )
.rotate( [77,0] )
.center( [0, 37.66] )
.translate( [width/2,height/2] );
Insert cell
dc_geoPath1 = d3.geoPath()
.projection( dcProjection1 );
Insert cell
// shotSpotterPoints = ShotSpotterDataGeoJSON.features.map(feature => {
// // Get coordinates from the feature
// const coords = feature.geometry.coordinates;
// // Project coordinates to screen space
// const projectedCoords = projection(coords);
// // Return an object with both original data and projected coordinates
// return {
// ...feature.properties, // Include all original properties
// x: projectedCoords[0],
// y: projectedCoords[1]
// };
// });
Insert cell
Insert cell
// Process and parse dates in your data
parsedData = day_to_visualize.map(d => {
// Make a copy to avoid modifying original data
const point = {...d};
// Parse the date string (adjust format as needed)
point.date = new Date(d.DATETIME);
return point;
});
Insert cell
sorted_parsed_data = // Sort data chronologically
parsedData.sort((a, b) => a.date - b.date);
Insert cell
parseDate = d3.isoParse;
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
// // Create a time scrubber control
// date = {
// // Create a range of dates from min to max (daily intervals)
// const dateRange = d3.timeDay.range(
// d3.min(shotSpotterPoints, d => new Date(d.DATETIME)),
// d3.timeDay.offset(d3.max(shotSpotterPoints, d => new Date(d.DATETIME)), 1)
// );
// // Create the scrubber control (using the same component as Walmart example)
// return Scrubber(dateRange, {
// format: date => date.toLocaleDateString("en-US", {
// year: "numeric",
// month: "short",
// day: "numeric"
// }),
// delay: 100,
// loop: false,
// autoplay: false,
// value: dateRange[0] // Start at the first date
// });
// }
Insert cell
// update = lgaChart.update(date)
Insert cell
Insert cell
mpdDistrictsGeoJSON.features;
Insert cell
update = lgaChart.update(date)
Insert cell
Insert cell
Insert cell
day_to_visualize = await FileAttachment("day_to_visualize.json").json()
Insert cell
Insert cell
// Define the hours for the single day (Feb 19, 2022)
hours = d3.range(0, 24)
Insert cell
viewof hour = Scrubber(d3.range(0, 24), {format: h => `${h}:00`, loop: true})
Insert cell
/**
* lgaChart - A D3.js visualization for displaying ShotSpotter alerts on a map
*
* This chart visualizes ShotSpotter alerts (gunshot detection) within Minneapolis Police
* Department districts. It creates an interactive map with district boundaries and animated
* dots representing gunshot alerts that can be filtered by date.
*
* @returns {SVGElement} - SVG element with an attached update method for time-based filtering
*/
lgaChart = {
// Extract district identifiers from GeoJSON data for color assignment
const districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);
// Create a color scale that assigns a unique color to each police district
// schemeAccent provides a set of distinct colors that work well together
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(districts);
// Create the main SVG container with specified width and height
let svg = d3.select(DOM.svg(width, height));
// Create separate group elements for different visualization layers
// This helps with organization and controlling z-index (draw order)
let districtOutlines = svg.append("g"); // Group for district boundaries
let shotSpotterAlerts = svg.append("g"); // Group for gunshot alert markers
// Create a geographic path generator with the provided projection
// This transforms geographic coordinates (lat/long) to screen coordinates (x,y)
var path = d3.geoPath().projection(projection);
// Create circles for each ShotSpotter alert
// Initially set to radius 0 (invisible) for later animation
const dots = shotSpotterAlerts.selectAll("circle")
.data(parsedData) // Bind parsed ShotSpotter data to elements
.enter().append("circle") // Create a circle for each data point
.attr("cx", d => d.x) // Position horizontally using projected x coordinate
.attr("cy", d => d.y) // Position vertically using projected y coordinate
.attr("r", 3) // Start with radius 0 (invisible) for animation
.attr("fill", "none") // Transparent fill
.attr("stroke", "darkred") // Red outline for visual impact
.attr("stroke-width", 0.5) // Thin stroke weight for less visual clutter
.attr("opacity", 0.7) // Slightly transparent to handle overlapping points
.append("title") // Add tooltip with additional information
.text(d => `Type: ${d.TYPE || "N/A"}\nDate: ${d.date.toLocaleString()}`);
// Draw police district boundaries from GeoJSON data
districtOutlines.selectAll('path')
.data(mpdDistrictsGeoJSON.features) // Bind district GeoJSON features
.enter().append('path') // Create path elements for each district
.attr('d', path) // Generate SVG path string from GeoJSON
.style('fill', "lightgrey") // Light fill color for districts
.style('stroke', '#fff'); // White borders between districts
// Initialize a date tracker for the animation system
// This stores the last date used in the update function
let previousDate = new Date(d3.min(parsedData, d => d.date));
previousDate.setDate(previousDate.getDate() - 1); // Start one day before earliest date
// Return the SVG element with an attached update method to enable interactivity
return Object.assign(svg.node(), {
/**
* Update function to animate dots based on a selected date
* This allows for time-based filtering of the ShotSpotter alerts
*
* @param {Date} date - The date to filter alerts up to
*/
update(hours) {
// Show new dots that occurred between previous update and current date
// This creates a progressive animation effect as the date changes
shotSpotterAlerts.selectAll("circle")
.filter(d => d.date > previousDate && d.date <= date) // Select only dots in the new time period
.transition() // Apply smooth transition
.duration(100) // Animation lasts 100ms
.attr("r", 3); // Make dots visible by increasing radius
// Optional: Hide dots outside the current timeframe
// This code would allow dots to disappear when moving backwards in time
shotSpotterAlerts.selectAll("circle")
.filter(d => d.date <= previousDate && d.date > date)
.transition()
.duration(100)
.attr("r", 0);
// Store the current date for the next update
previousDate = new Date(date);
}
});
}
Insert cell
projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpdDistrictsGeoJSON);
Insert cell
{ // First, project your coordinates
}
Insert cell
cartogram = {
let cartogram;

if (compute == "Live") {
cartogram = GoCart.makeCartogram(worlddataproj, value);
} else {
if (value == "pop") {
cartogram = FileAttachment("cartogram_pop@1.json").json();
}

if (value == "gdp") {
cartogram = FileAttachment("cartogram_gdp@1.json").json();
}
}
return cartogram;
}
Insert cell
Insert cell
// Use D3's nest function to group alerts by district
nestedAlerts = d3.nest()
.key(d => d.properties.SOURCE)
.entries(ShotSpotterCleaned2_Source);
Insert cell
// Calculate the percentage of alerts for each district
alertPercentagesNest = nestedAlerts.map(district => {
return {
district: district.key,
percentage: (district.values.length / totalAlerts) * 100
};
});

Insert cell
// Calculate the percentage of alerts for each district
alertPercentages = mpd_geojson_with_race.features.map(district => {
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;

return {
district: district.properties.DISTRICT,
poc_ratio: district.properties.ratio,
percentage: (districtAlerts / totalAlerts) * 100
};
});
Insert cell
Insert cell
import {districtRatios} from "ebeb9669a8cadc1e"
Insert cell
lgaChart33 = {
// Extract district names from the GeoJSON features
const districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);

// Create a color scale for the districts using a predefined color scheme
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(districts);
// // Create a sequential color scale based on the ratio values
// const color = d3.scaleSequential()
// .domain(d3.extent(districtRatios, d => d.ratio))
// .interpolator(d3.interpolateGreens);
const color = d3.scaleSequential()
.domain([d3.min(districtRatios, d => d.ratio), d3.max(districtRatios, d => d.ratio)])
.interpolator(d3.interpolateGreens);


// Create an SVG element with specified width and height
let svg = d3.select(DOM.svg(width, height));

// Append empty placeholder 'g' elements to the SVG for different layers
let districtOutlines = svg.append("g");
let shotSpotterAlerts = svg.append("g");
let bubbles = svg.append("g");

// Define the map projection to fit the GeoJSON data within the SVG dimensions
var projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpdDistrictsGeoJSON);

// Define the path generator using the projection
var path = d3.geoPath()
.projection(projection);

// Function to get the color for a district
function getColorForDistrict(district) {
const districtData = districtRatios.find(d => d.DISTRICT === district);
if (!districtData) {
console.warn(`District ${district} not found in districtRatios`);
return 'black'; // Default color if not found
}
return districtData.ratio;
}

// Draw district outlines on the map
districtOutlines.selectAll('path')
.data(mpdDistrictsGeoJSON.features)
.enter().append('path')
.attr('d', path)
.style('fill', d => {
// Convert district to string for comparison
const districtStr = d.properties.DISTRICT.toString();
const districtData = districtRatios.find(d2 => d2.DISTRICT === districtStr);
if (!districtData) {
console.warn(`District ${districtStr} not found in districtRatios`);
return '#black'; // Default color if not found
}
return color(districtData.ratio);
})
// .style('fill', d => color(d.properties.DISTRICT.toString()))
// .style('fill', d => getColorForDistrict((d.properties.DISTRICT).toS))
// .style("fill-opacity", .2) // set the fill opacity
.style('stroke', '#fff');

// Calculate the total number of ShotSpotter alerts
const totalAlerts = ShotSpotterDataGeoJSON.features.length;

// Calculate the percentage of alerts for each district
const alertPercentages = mpdDistrictsGeoJSON.features.map(district => {
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;

return {
district: district.properties.DISTRICT,
percentage: (districtAlerts / totalAlerts) * 100
};
});

// Create a scale for bubble sizes to represent alert percentages
const minPercentage = d3.min(alertPercentages, d => d.percentage);
const maxPercentage = d3.max(alertPercentages, d => d.percentage);

// Adjust the domain to avoid zero-sized bubbles
const sizeScale = d3.scaleSqrt()
.domain([minPercentage * 0.9, maxPercentage]) // Ensure minimum bubble size
.range([5, 100]); // Adjust range for better visibility

// Draw bubbles on the map to represent alert percentages
bubbles.selectAll("circle")
.data(alertPercentages)
.enter().append("circle")
.attr("cx", d => {
const district = mpdDistrictsGeoJSON.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[0];
})
.attr("cy", d => {
const district = mpdDistrictsGeoJSON.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[1];
})
.attr("r", d => sizeScale(d.percentage))
.style('fill', "none")
// .style('fill', d => colorScale(d.district))
.style("stroke", d => colorScale(d.district))
.style("stroke-width", 8); // set the stroke width

// Return the SVG node for further manipulation or rendering
return svg.node();
}

Insert cell
lgaChart30 = {
// Extract district names from the GeoJSON features
const districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);

// Create a color scale for the districts using a predefined color scheme
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(districts);
// Create a sequential color scale based on the ratio values
// Fix: Use a proper domain range and ensure consistent interpolator
const color = d3.scaleSequential()
.domain([0, 1]) // Set domain from 0 to 1 since ratios are between 0-1
.interpolator(d3.interpolateGreens);

// Create an SVG element with specified width and height
let svg = d3.select(DOM.svg(width, height));

// Append empty placeholder 'g' elements to the SVG for different layers
let districtOutlines = svg.append("g");
let shotSpotterAlerts = svg.append("g");
let bubbles = svg.append("g");

// Define the map projection to fit the GeoJSON data within the SVG dimensions
var projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpdDistrictsGeoJSON);

// Define the path generator using the projection
var path = d3.geoPath()
.projection(projection);

// Draw district outlines on the map
districtOutlines.selectAll('path')
.data(mpdDistrictsGeoJSON.features)
.enter().append('path')
.attr('d', path)
.style('fill', d => {
// Convert district to string for comparison
let districtNum = d.properties.DISTRICT;
let districtData = districtRatios.find(d2 => d2.DISTRICT === districtNum);
if (!districtData) {

return '#ccc'; // Use a light gray as default color if not found
}
return color(districtData.ratio);
})
.style('stroke', '#fff')
.style('stroke-width', 1);

// Calculate the total number of ShotSpotter alerts
const totalAlerts = ShotSpotterDataGeoJSON.features.length;

// Calculate the percentage of alerts for each district
const alertPercentages = mpdDistrictsGeoJSON.features.map(district => {
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;

return {
district: district.properties.DISTRICT,
percentage: (districtAlerts / totalAlerts) * 100
};
});

// Create a scale for bubble sizes to represent alert percentages
const minPercentage = d3.min(alertPercentages, d => d.percentage);
const maxPercentage = d3.max(alertPercentages, d => d.percentage);

// Adjust the domain to avoid zero-sized bubbles
const sizeScale = d3.scaleSqrt()
.domain([minPercentage * 0.9, maxPercentage]) // Ensure minimum bubble size
.range([5, 100]); // Adjust range for better visibility

// Draw bubbles on the map to represent alert percentages
bubbles.selectAll("circle")
.data(alertPercentages)
.enter().append("circle")
.attr("cx", d => {
const district = mpdDistrictsGeoJSON.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[0];
})
.attr("cy", d => {
const district = mpdDistrictsGeoJSON.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[1];
})
.attr("r", d => sizeScale(d.percentage))
.style('fill', "none")
.style("stroke", d => colorScale(d.district))
.style("stroke-width", 8);

// Add a color legend for the chloropleth map
const legendWidth = 200;
const legendHeight = 10;
const legendX = width - legendWidth - 20;
const legendY = height - 50;
// Create a gradient for the legend
const defs = svg.append("defs");
const linearGradient = defs.append("linearGradient")
.attr("id", "district-color-gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "0%");
linearGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", color(0));
linearGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", color(1));
// Draw the legend rectangle
svg.append("rect")
.attr("x", legendX)
.attr("y", legendY)
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#district-color-gradient)");
// Add legend title
svg.append("text")
.attr("x", legendX)
.attr("y", legendY - 5)
.style("font-size", "12px")
.text("Ratio Value");
// Add legend ticks and labels
const legendScale = d3.scaleLinear()
.domain([0, 1])
.range([0, legendWidth]);
const legendAxis = d3.axisBottom(legendScale)
.ticks(5)
.tickFormat(d3.format(".1f"));
svg.append("g")
.attr("transform", `translate(${legendX}, ${legendY + legendHeight})`)
.call(legendAxis);

// Return the SVG node for further manipulation or rendering
return svg.node();
}
Insert cell
import {poc_ratio_by_district} from "ebeb9669a8cadc1e"
Insert cell
classes = styles.join(" ")
Insert cell
viewof styles = Inputs.checkbox(["light", "blur"], {
value: ["blur"],
label: "Tooltip style classes"
})
Insert cell
chart = {
// Specify the chart’s dimensions.
const width = 928;
const height = 600;
const marginTop = 10;
const marginRight = 10;
const marginBottom = 20;
const marginLeft = 40;

// Prepare the scales for positional and color encodings.
// Fx encodes the state.
const fx = d3.scaleBand()
.domain(new Set(data.map(d => d.state)))
.rangeRound([marginLeft, width - marginRight])
.paddingInner(0.1);

// Both x and color encode the age class.
const ages = new Set(data.map(d => d.age));

const x = d3.scaleBand()
.domain(ages)
.rangeRound([0, fx.bandwidth()])
.padding(0.05);

const color = d3.scaleOrdinal()
.domain(ages)
.range(d3.schemeSpectral[ages.size])
.unknown("#ccc");

// Y encodes the height of the bar.
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.population)]).nice()
.rangeRound([height - marginBottom, marginTop]);

// A function to format the value in the tooltip.
const formatValue = x => isNaN(x) ? "N/A" : x.toLocaleString("en")

// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// Append a group for each state, and a rect for each age.
svg.append("g")
.selectAll()
.data(d3.group(data, d => d.state))
.join("g")
.attr("transform", ([state]) => `translate(${fx(state)},0)`)
.selectAll()
.data(([, d]) => d)
.join("rect")
.attr("x", d => x(d.age))
.attr("y", d => y(d.population))
.attr("width", x.bandwidth())
.attr("height", d => y(0) - y(d.population))
.attr("fill", d => color(d.age));

// Append the horizontal axis.
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(fx).tickSizeOuter(0))
.call(g => g.selectAll(".domain").remove());

// Append the vertical axis.
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y).ticks(null, "s"))
.call(g => g.selectAll(".domain").remove());

// Return the chart with the color scale as a property (for the legend).
return Object.assign(svg.node(), {scales: {color}});
}
Insert cell
lgaChart36 = {
// Extract district names from the GeoJSON features
const districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);
// Create a color scale for the districts using a predefined color scheme
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(districts);
// Use the poc_ratio_by_district array directly for the scale
// Create an SVG element with specified width and height
// let svg = d3.select(DOM.svg(width, height));


const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
// .attr("viewBox", [-width / 2, -height / 2, width, height])
.style("background", "#2D2537");

d3.selectAll("div.tooltip").remove(); // clear tooltips from before

const g = svg
.append("g")

const tooltip = d3
.select("body")
.append("div")
.attr("class", `tooltip ${classes}`)
.style("position", "absolute")
.style("opacity", 0)
// .style("visibility", "hidden")
.text("I'm a circle!");
// const tooltip = g
// .append("text")
// .attr("class", "tooltip")
// .attr("fill", "black")
// .style("pointer-events", "none");
// Create a tooltip div that is hidden by default
const tooltip2 = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "1px")
.style("border-radius", "5px")
.style("padding", "10px")
.style("box-shadow", "0px 0px 6px rgba(0, 0, 0, 0.3)");
// Append a rectangle to serve as the background
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "lightgrey"); // Set the background color to lightgrey
// Append empty placeholder 'g' elements to the SVG for different layers
let districtOutlines = svg.append("g");
let shotSpotterAlerts = svg.append("g");
let bubbles = svg.append("g");
// Define the map projection to fit the GeoJSON data within the SVG dimensions
var projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpdDistrictsGeoJSON);
// Define the path generator using the projection
var path = d3.geoPath()
.projection(projection);
// Calculate the total number of ShotSpotter alerts
const totalAlerts = ShotSpotterDataGeoJSON.features.length;
// Calculate the percentage of alerts for each district
const alertPercentages = mpd_geojson_with_race.features.map(district => {
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;
return {
district: district.properties.DISTRICT,
ratio: district.properties.ratio,
percentage: (districtAlerts / totalAlerts) * 100
};
});
// Create a scale for bubble sizes to represent alert percentages
const minPercentage = d3.min(alertPercentages, d => d.percentage);
const maxPercentage = d3.max(alertPercentages, d => d.percentage);
// Adjust the domain to avoid zero-sized bubbles
const sizeScale = d3.scaleSqrt()
//format to only 2 decimal places
.domain([minPercentage, maxPercentage]) // Ensure minimum bubble size
.range([1, 100]); // Adjust range for better visibility
// Format for percentage display in tooltip
const formatPercent = d3.format(".2f");

// Draw district outlines on the map with tooltips
districtOutlines.selectAll('path')
.data(mpd_geojson_with_race.features)
.enter().append('path')
.attr('d', path)
.attr("fill", "white")
.style('stroke', "lightgrey")
.style('stroke-width', 3)
.on("mouseover", function(event, d) {
// Find the corresponding alert percentage data
const alertData = alertPercentages.find(a => a.district === d.properties.DISTRICT);
tooltip
.style("visibility", "visible")
.html(`
<strong>District:</strong> ${d.properties.DISTRICT}<br>
<strong>POC Ratio:</strong> ${(d.properties.ratio * 100).toFixed(2)}%<br>
<strong>Alerts:</strong> ${formatPercent(alertData.percentage)}% of total
`);
})
.on("mousemove", function(event) {
tooltip
.style("top", (event.pageY - 10) + "px")
.style("left", (event.pageX + 10) + "px");
})
.on("mouseout", function() {
tooltip.style("visibility", "hidden");
});
// Draw bubbles on the map to represent alert percentages
bubbles.selectAll("circle")
.data(alertPercentages)
.enter().append("circle")
.attr("cx", d => {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[0];
})
.attr("cy", d => {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[1];
})
.attr("r", d => sizeScale(d.percentage))
.style('fill', d => colorFunction(d.ratio))
.style("opacity", 0.8)
.style("stroke", "lightgrey")
.style('stroke-width', 3)

bubbles
.on("mouseover", (_evt, d) => {
tooltip
.style("opacity", 1)
.html(`x = ${d.ratio}<br>`)
.style("border-color", colorFunction(d.ratio));
})
.on("mousemove", (evt, d) => {
tooltip
.style("top", evt.pageY - 10 + "px")
.style("left", evt.pageX + 10 + "px");
})
.on("mouseout", () => {
tooltip.style("opacity", 0);
});


// Return the SVG node for further manipulation or rendering
return svg.node();
}
Insert cell
GoCart = Promise.all([
require("go-cart-wasm@0.3.0"),
require.resolve("go-cart-wasm@0.3.0/dist/cart.wasm")
]).then(([initGoCart, url]) => initGoCart({ locateFile: () => url }))
Insert cell
whiteHat = {
// Extract district names from the GeoJSON features
const districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);
// Create a color scale for the districts using a predefined color scheme
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(districts);

// Create an SVG element with specified width and height
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.style("background", "#2D2537");

const g = svg.append("g");

// Append a rectangle to serve as the background
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "lightgrey"); // Set the background color to lightgrey

// Append empty placeholder 'g' elements to the SVG for different layers
let districtOutlines = svg.append("g");
let shotSpotterAlerts = svg.append("g");
let bubbles = svg.append("g");

// Define the map projection to fit the GeoJSON data within the SVG dimensions
var projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpdDistrictsGeoJSON);

// Define the path generator using the projection
var path = d3.geoPath()
.projection(projection);

// Calculate the total number of ShotSpotter alerts
const totalAlerts = ShotSpotterDataGeoJSON.features.length;

// Calculate the percentage of alerts for each district
const alertPercentages = mpd_geojson_with_race.features.map(district => {
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;
return {
district: district.properties.DISTRICT,
ratio: district.properties.ratio,
percentage: (districtAlerts / totalAlerts) * 100
};
});

// Create a scale for bubble sizes to represent alert percentages
const minPercentage = d3.min(alertPercentages, d => d.percentage);
const maxPercentage = d3.max(alertPercentages, d => d.percentage);

// Adjust the domain to avoid zero-sized bubbles
const sizeScale = d3.scaleSqrt()
.domain([minPercentage, maxPercentage]) // Ensure minimum bubble size
.range([1, 100]); // Adjust range for better visibility

// Draw district outlines on the map
districtOutlines.selectAll('path')
.data(mpd_geojson_with_race.features)
.enter().append('path')
.attr('d', path)
.attr("fill", "white")
.style('stroke', "lightgrey")
.style('stroke-width', 4);

// Draw bubbles on the map to represent alert percentages
bubbles.selectAll("circle")
.data(alertPercentages)
.enter().append("circle")
.attr("cx", d => {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[0];
})
.attr("cy", d => {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[1];
})
.attr("r", d => sizeScale(d.percentage))
.style('fill', d => colorFunction(d.ratio))
.style("opacity", 0.8)
.style("stroke", "lightgrey")
.style('stroke-width', 3);

// Return the SVG node for further manipulation or rendering
return svg.node();
}

Insert cell
alertPercentages1 = mpd_geojson_with_race.features.map(district => {
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;
return {
district: district.properties.DISTRICT,
ratio: district.properties.ratio,
percentage: (districtAlerts / totalAlerts) * 100
};
});
Insert cell
whiteHatTool = {
// Extract district names from the GeoJSON features
const districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);
// Create a color scale for the districts using a predefined color scheme
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(districts);
// Create an SVG element with specified width and height
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.style("background", "#2D2537");
const g = svg.append("g");
// Append a rectangle to serve as the background
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "lightgrey"); // Set the background color to lightgrey
// Append empty placeholder 'g' elements to the SVG for different layers
let districtOutlines = svg.append("g");
let shotSpotterAlerts = svg.append("g");
let bubbles = svg.append("g");
// Define the map projection to fit the GeoJSON data within the SVG dimensions
var projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpdDistrictsGeoJSON);
// Define the path generator using the projection
var path = d3.geoPath()
.projection(projection);
// Calculate the total number of ShotSpotter alerts
const totalAlerts = ShotSpotterDataGeoJSON.features.length;
// Calculate the percentage of alerts for each district
const alertPercentages = mpd_geojson_with_race.features.map(district => {
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;
return {
district: district.properties.DISTRICT,
ratio: district.properties.ratio,
percentage: (districtAlerts / totalAlerts) * 100
};
});
// Create a scale for bubble sizes to represent alert percentages
const minPercentage = d3.min(alertPercentages, d => d.percentage);
const maxPercentage = d3.max(alertPercentages, d => d.percentage);
// Adjust the domain to avoid zero-sized bubbles
const sizeScale = d3.scaleSqrt()
.domain([minPercentage, maxPercentage]) // Ensure minimum bubble size
.range([2, 100]); // Adjust range for better visibility
// Draw district outlines on the map
districtOutlines.selectAll('path')
.data(mpd_geojson_with_race.features)
.enter().append('path')
.attr('d', path)
.attr("fill", "white")
.style('stroke', "lightgrey")
.style('stroke-width', 4);
// Draw bubbles on the map to represent alert percentages
bubbles.selectAll("circle")
.data(alertPercentages)
.enter().append("circle")
.attr("cx", d => {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[0];
})
.attr("cy", d => {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[1];
})
.attr("r", d => sizeScale(d.percentage))
.style('fill', d => colorFunction(d.ratio))
.style("opacity", 0.75)
.style("stroke", "lightgrey")
.style('stroke-width', 3)
// Add a simple title attribute which will show as a native browser tooltip
.append("title")
.text(d => `Police District: ${d.district}\n
Percentage of All ShotSpotter Alerts that Occurred in District ${d.district}: ${d.percentage.toFixed(2)}%\n
Percentage of District ${d.district} Population that identifies as a Person of Color: ${d.ratio.toFixed(2) * 100}%`);
// Return the SVG node for further manipulation or rendering
return svg.node();
}
Insert cell
whiteHatTool;
Insert cell
Insert cell
class Tooltip {
constructor() {
this._date = htl.svg`<text y="-22"></text>`;
this._close = htl.svg`<text y="-12"></text>`;
this.node = htl.svg`<g pointer-events="none" display="none" font-family="sans-serif" font-size="10" text-anchor="middle">
<rect x="-27" width="54" y="-30" height="20" fill="white"></rect>
${this._date}
${this._close}
<circle r="2.5"></circle>
</g>`;
}
show(d) {
this.node.removeAttribute("display");
this.node.setAttribute("transform", `translate(${x(d.date)},${y(d.close)})`);
}
hide() {
this.node.setAttribute("display", "none");
}
}
Insert cell
lgaChart35 = {
// Extract district names from the GeoJSON features
const districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);

// Create a color scale for the districts using a predefined color scheme
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(districts);
// Use the poc_ratio_by_district array directly for the scale


// Create an SVG element with specified width and height
let svg = d3.select(DOM.svg(width, height));

// Create tooltip instance
const tooltip = new Tooltip();
svg.node().appendChild(tooltip.node);
// Append a rectangle to serve as the background
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "lightgrey"); // Set the background color to black



// Append empty placeholder 'g' elements to the SVG for different layers
let districtOutlines = svg.append("g");
let shotSpotterAlerts = svg.append("g");
let bubbles = svg.append("g");

// Define the map projection to fit the GeoJSON data within the SVG dimensions
var projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpdDistrictsGeoJSON);

// Define the path generator using the projection
var path = d3.geoPath()
.projection(projection);



// Calculate the total number of ShotSpotter alerts
const totalAlerts = ShotSpotterDataGeoJSON.features.length;

// Calculate the percentage of alerts for each district
const alertPercentages = mpd_geojson_with_race.features.map(district => {
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;

return {
district: district.properties.DISTRICT,
ratio: district.properties.ratio,
percentage: (districtAlerts / totalAlerts) * 100
};
});

// Create a scale for bubble sizes to represent alert percentages
const minPercentage = d3.min(alertPercentages, d => d.percentage);
const maxPercentage = d3.max(alertPercentages, d => d.percentage);

// Adjust the domain to avoid zero-sized bubbles
const sizeScale = d3.scaleSqrt()
.domain([minPercentage, maxPercentage]) // Ensure minimum bubble size
.range([1, 100]); // Adjust range for better visibility

// Draw bubbles on the map to represent alert percentages
bubbles.selectAll("circle")
.data(alertPercentages)
.enter().append("circle")
.attr("cx", d => {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[0];
})
.attr("cy", d => {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
return path.centroid(district)[1];
})
.attr("r", d => sizeScale(d.percentage))
.style('fill', d => colorFunction(d.ratio))
.style("opacity", .8) // set the element opacity
.style("stroke", "lightgrey")
.style('stroke-width', 3)
.on("mouseover", function(event, d) {
const district = mpd_geojson_with_race.features.find(f => f.properties.DISTRICT === d.district);
const centroid = path.centroid(district);
tooltip.show(d, centroid);
})
.on("mouseout", function() {
tooltip.hide();
});

// Draw district outlines on the map
districtOutlines.selectAll('path')
.data(mpd_geojson_with_race.features)
.enter().append('path')
.attr('d', path)
.attr("fill", "white")
// .attr("fill", d => colorFunction(d.properties.ratio))
.style('stroke', "lightgrey")
.style('stroke-width', 3);

// Return the SVG node for further manipulation or rendering
return svg.node();
}

Insert cell
interpolateSepia = t => {
// t ranges from 0 to 1
return d3.rgb(
Math.round(165 - (t * 100)), // Red component
Math.round(120 - (t * 80)), // Green component
Math.round(80 - (t * 65)) // Blue component
).toString();
};
Insert cell
colorFunction = d3.scaleSequential()
.domain(d3.extent(poc_ratio_by_district)) // Gets min and max values
.interpolator(d3.interpolateGreys);
Insert cell
// // Create the color scale with the custom sepia interpolator
// colorFunction = d3.scaleSequential()
// .domain(d3.extent(poc_ratio_by_district))
// .interpolator(interpolateSepia)
// .nice();
Insert cell
// Extract district names from the GeoJSON features
districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);
Insert cell
// Calculate the percentage of alerts for each district
alertPercentageslocal = mpd_districts_data_upload_geojson.features.map(district => {
const districtAlerts = ss_cleaned_data_geojson.features.filter(alert =>
alert.properties.SOURCE === district.properties.DISTRICT
).length;

return {
district: district.properties.DISTRICT,
percentage: (districtAlerts / totalAlerts) * 100 // Calculate percentage
};
});
Insert cell
cartogram = {
// Create a color scale for the districts using a predefined color scheme
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(mpd_district_names);

// Create an SVG element with specified width and height
let svg = d3.select(DOM.svg(width, height));

// Append empty placeholder 'g' elements to the SVG for different layers
let mpd_district_layer = svg.append("g");
let shotSpotterAlerts_layer = svg.append("g");
let bubbles_layer = svg.append("g");

// Define the map projection to fit the GeoJSON data within the SVG dimensions
let projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpd_district_data_cleaned);

// Define the path generator using the projection
let path = d3.geoPath()
.projection(projection);

// Draw district outlines on the map
mpd_district_layer.selectAll('path')
.data(mpd_district_data_cleaned.features)
.enter().append('path')
.attr('d', path)
.style('fill', d => colorScale(d.properties.DISTRICT))
.style('stroke', '#fff');

// Create a scale for bubble sizes to represent alert percentages
const minPercentage = d3.min(alertPercentageslocal, d => d.percentage);
const maxPercentage = d3.max(alertPercentageslocal, d => d.percentage);

// Adjust the domain to avoid zero-sized bubbles
const sizeScale = d3.scaleSqrt()
.domain([minPercentage * 0.9, maxPercentage]) // Ensure minimum bubble size
.range([5, 100]); // Adjust range for better visibility

// Draw bubbles on the map to represent alert percentages
bubbles_layer.selectAll("circle")
.data(alertPercentageslocal)
.enter().append("circle")
.attr("cx", d => {
const district = mpd_district_data_cleaned.features.find(f => f.properties.DISTRICT === d.district);
const [x, y] = path.centroid(district); // Get the centroid of the district
return x; // Set x-coordinate
})
.attr("cy", d => {
const district = mpd_district_data_cleaned.features.find(f => f.properties.DISTRICT === d.district);
const [x, y] = path.centroid(district); // Get the centroid of the district
return y; // Set y-coordinate
})
.attr("r", d => sizeScale(d.percentage)) // Set radius based on percentage
.style('fill', d => colorScale(d.district)) // Fill with district color
.style("stroke", "white"); // Outline color

// Return the SVG node for further manipulation or rendering
return svg.node();
}

Insert cell
Insert cell
shotCountsByYear = {};
Insert cell
uniqueYears = (ShotSpotterDataGeoJSON.features // Access features in the data
.map(feature => feature.properties?.DATETIME) // Extract the DATETIME values
.filter(datetime => datetime) // Filter out undefined or null DATETIME values
.map(datetime => new Date(datetime).getFullYear()) // Convert to year
.reduce((unique, year) => unique.add(year), new Set()) // Use a Set to get unique years
.values()) // Convert the Set to an iterable
Insert cell
Array.from(uniqueYears)
Insert cell
cartogram.update(year)
Insert cell
centroid = {
const path = d3.geoPath();
return feature => path.centroid(feature);
}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
// d3 = require('d3@5')
Insert cell
ShotSpotterDataGeoJSON = await FileAttachment("Shot_Spotter_Gun_Shots.geojson").json()
Insert cell
ShotSpotterCleaned1_Source = ShotSpotterDataGeoJSON.features.forEach(feature => {
if (feature.properties.SOURCE === "WashingtonDC5D") {
feature.properties.SOURCE = 5; // Update the value of SOURCE
}
});
Insert cell
mpdDistrictsGeoJSON = FileAttachment("Police_Districts.geojson").json()
Insert cell
lgaChart322 = {
// Extract district names from the GeoJSON features
const districts = mpdDistrictsGeoJSON.features.map(d => d.properties.DISTRICT);

// Create a color scale for the districts using a predefined color scheme
const colorScale = d3.scaleOrdinal(d3.schemeAccent).domain(districts);
// Create a sequential color scale based on the ratio values
const color = d3.scaleSequential()
.domain([0, 1])
.interpolator(d3.interpolateGreens);

// Create an SVG element with specified width and height
let svg = d3.select(DOM.svg(width, height));

// Append empty placeholder 'g' elements to the SVG for different layers
let districtOutlines = svg.append("g");
let shotSpotterAlerts = svg.append("g");
let bubbles = svg.append("g");

// Define the map projection to fit the GeoJSON data within the SVG dimensions
var projection = d3.geoMercator()
.fitExtent([[20, 20], [width, height]], mpdDistrictsGeoJSON);

// Define the path generator using the projection
var path = d3.geoPath()
.projection(projection);
// Normalize district ratios data for easier lookup
// This converts all district IDs to strings for consistent comparison
const normalizedRatios = {};
districtRatios.forEach(d => {
normalizedRatios[String(d.DISTRICT)] = d.ratio;
});
// Draw district outlines on the map
districtOutlines.selectAll('path')
.data(mpdDistrictsGeoJSON.features)
.enter().append('path')
.attr('d', path)
.style('fill', d => {
const districtKey = d.properties.DISTRICT;
const ratio = normalizedRatios[districtKey];
if (ratio === undefined) {
return '#ccc'; // Default color if no match
}
return color(ratio);
})
.style('stroke', '#fff')
.style('stroke-width', 1);

// Calculate the total number of ShotSpotter alerts
const totalAlerts = ShotSpotterDataGeoJSON.features.length;

// Calculate the percentage of alerts for each district
const alertPercentages = mpdDistrictsGeoJSON.features.map(district => {
const districtId = district.properties.DISTRICT;
const districtAlerts = ShotSpotterCleaned2_Source.filter(alert =>
String(alert.properties.SOURCE) === String(districtId)
).length;

return {
district: districtId,
percentage: (districtAlerts / totalAlerts) * 100
};
});

// Create a scale for bubble sizes to represent alert percentages
const minPercentage = d3.min(alertPercentages, d => d.percentage);
const maxPercentage = d3.max(alertPercentages, d => d.percentage);

// Adjust the domain to avoid zero-sized bubbles
const sizeScale = d3.scaleSqrt()
.domain([minPercentage * 0.9, maxPercentage]) // Ensure minimum bubble size
.range([5, 100]); // Adjust range for better visibility

// Draw bubbles on the map to represent alert percentages
bubbles.selectAll("circle")
.data(alertPercentages)
.enter().append("circle")
.attr("cx", d => {
const district = mpdDistrictsGeoJSON.features.find(f =>
String(f.properties.DISTRICT) === String(d.district)
);
return district ? path.centroid(district)[0] : 0;
})
.attr("cy", d => {
const district = mpdDistrictsGeoJSON.features.find(f =>
String(f.properties.DISTRICT) === String(d.district)
);
return district ? path.centroid(district)[1] : 0;
})
.attr("r", d => sizeScale(d.percentage))
.style('fill', "none")
.style("stroke", d => colorScale(d.district))
.style("stroke-width", 8);

// Add a color legend for the chloropleth map
const legendWidth = 200;
const legendHeight = 10;
const legendX = width - legendWidth - 20;
const legendY = height - 50;
// Create a gradient for the legend
const defs = svg.append("defs");
const linearGradient = defs.append("linearGradient")
.attr("id", "district-color-gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "0%");
linearGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", color(0));
linearGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", color(1));
// Draw the legend rectangle
svg.append("rect")
.attr("x", legendX)
.attr("y", legendY)
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#district-color-gradient)");
// Add legend title
svg.append("text")
.attr("x", legendX)
.attr("y", legendY - 5)
.style("font-size", "12px")
.text("Ratio Value");
// Add legend ticks and labels
const legendScale = d3.scaleLinear()
.domain([0, 1])
.range([0, legendWidth]);
const legendAxis = d3.axisBottom(legendScale)
.ticks(5)
.tickFormat(d3.format(".1f"));
svg.append("g")
.attr("transform", `translate(${legendX}, ${legendY + legendHeight})`)
.call(legendAxis);

// Return the SVG node for further manipulation or rendering
return svg.node();
}
Insert cell
import {Legend} from "@d3/color-legend"
Insert cell
Insert cell
Insert cell
vl.markArea()
.width(1100)
.height(2000)
.data(TransformedData2_CleanTYPEAttribute)
.encode(
vl.y()
.fieldT("DATETIME")
.timeUnit("yearmonth")
.axis({ domain: false, format: "%Y", tickSize: 10 }),
vl.x()
.count()
.axis(null)
.stack("center"),
vl.color()
.fieldN("SOURCE")
.scale({ scheme: "category20b" })
)
.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