diff options
| author | Christian Cleberg <[email protected]> | 2026-04-17 19:41:31 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-17 19:41:31 -0500 |
| commit | 623ae07ebacb9d596b225179727dc87641ad1cca (patch) | |
| tree | f1045282b3369bf3f4b0249cd649b47a5d010dad /assets/js/charts/bar.js | |
| parent | fd412c5197286518ff9491a1b51e6319c9a22904 (diff) | |
| download | world-incarceration-623ae07ebacb9d596b225179727dc87641ad1cca.tar.gz world-incarceration-623ae07ebacb9d596b225179727dc87641ad1cca.tar.bz2 world-incarceration-623ae07ebacb9d596b225179727dc87641ad1cca.zip | |
phase 2-6: complete D3/TopoJSON migration
Replace ZingChart + jQuery with D3 v7 + TopoJSON. Implements all
planned phases: choropleth map (NaturalEarth1 projection, YlOrRd
gradient), hover tooltip, zoom/pan/reset, country detail modal,
side-by-side compare, metric selector, label toggle, donut pie chart
with per-country drilldown bar chart. Upgrades Bootstrap JS bundle
from v4.5.3 (jQuery-dependent) to v5.0.0, fixes modal API calls
to use getInstance/constructor pattern compatible with BS 5.0.0+.
Removes jquery, zingchart ×3 library files.
Diffstat (limited to 'assets/js/charts/bar.js')
| -rw-r--r-- | assets/js/charts/bar.js | 99 |
1 files changed, 97 insertions, 2 deletions
diff --git a/assets/js/charts/bar.js b/assets/js/charts/bar.js index 4aa61ef..572cb50 100644 --- a/assets/js/charts/bar.js +++ b/assets/js/charts/bar.js @@ -1,2 +1,97 @@ -// Phase 4: horizontal bar chart for top-N countries -export function renderBarChart(container, data) {} +import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm"; + +const MARGIN = { top: 44, right: 90, bottom: 30, left: 150 }; + +// data: [{label, value (0–100 normalized), displayValue}] +// opts: { title?, onBack? } +export function renderBarChart(container, data, opts = {}) { + const { title, onBack } = opts; + const el = typeof container === "string" + ? document.querySelector(container) + : container; + + el.innerHTML = ""; + + const totalWidth = Math.max(el.clientWidth || 500, 400); + const totalHeight = data.length * 52 + MARGIN.top + MARGIN.bottom; + const width = totalWidth - MARGIN.left - MARGIN.right; + const height = totalHeight - MARGIN.top - MARGIN.bottom; + + const svg = d3.select(el).append("svg") + .attr("width", "100%") + .attr("viewBox", `0 0 ${totalWidth} ${totalHeight}`); + + if (title) { + svg.append("text") + .attr("x", totalWidth / 2) + .attr("y", 22) + .attr("text-anchor", "middle") + .style("fill", "#fff") + .style("font-size", "14px") + .style("font-weight", "bold") + .text(title); + } + + if (onBack) { + svg.append("text") + .attr("x", 8) + .attr("y", 22) + .style("fill", "#24ab48") + .style("font-size", "13px") + .style("cursor", "pointer") + .text("← Back") + .on("click", onBack); + } + + const g = svg.append("g") + .attr("transform", `translate(${MARGIN.left},${MARGIN.top})`); + + const yScale = d3.scaleBand() + .domain(data.map(d => d.label)) + .range([0, height]) + .padding(0.3); + + const xScale = d3.scaleLinear() + .domain([0, 100]) + .range([0, width]); + + // Y axis (country/metric labels) + g.append("g") + .call(d3.axisLeft(yScale).tickSize(0)) + .call(ax => ax.select(".domain").remove()) + .selectAll("text") + .style("fill", "#ccc") + .style("font-size", "12px"); + + // X axis (percentage) + g.append("g") + .attr("transform", `translate(0,${height})`) + .call(d3.axisBottom(xScale).ticks(5).tickFormat(d => d + "%")) + .call(ax => ax.select(".domain").remove()) + .selectAll("text") + .style("fill", "#666") + .style("font-size", "10px"); + + // Bars + g.selectAll(".bar") + .data(data) + .join("rect") + .attr("class", "bar") + .attr("y", d => yScale(d.label)) + .attr("height", yScale.bandwidth()) + .attr("x", 0) + .attr("width", d => xScale(Math.max(0, d.value))) + .attr("fill", "rgba(36, 171, 72, 0.8)"); + + // Value labels at end of bar + g.selectAll(".bar-label") + .data(data) + .join("text") + .attr("class", "bar-label") + .attr("x", d => xScale(Math.max(0, d.value)) + 4) + .attr("y", d => yScale(d.label) + yScale.bandwidth() / 2) + .attr("dy", "0.35em") + .style("fill", "#fff") + .style("font-size", "11px") + .text(d => d.displayValue); +} |
