diff --git a/stacked_area_chart/graphic.js b/stacked_area_chart/graphic.js new file mode 100644 index 0000000..98f3cc1 --- /dev/null +++ b/stacked_area_chart/graphic.js @@ -0,0 +1,79 @@ +var d3 = { + ...require("d3-shape/dist/d3-shape.min") +}; + +var pym = require("./lib/pym"); +require("./lib/webfonts"); + +var pymChild; +var renderAreaChart = require("./renderAreaChart"); + +//Initialize graphic +var onWindowLoaded = function() { + var series = formatData(window.DATA); + render(series); + + window.addEventListener("resize", () => render(series)); + + pym.then(child => { + pymChild = child; + child.sendHeight(); + }); +}; + +//Format graphic data for processing by D3. +var formatData = function(data) { + data.forEach(function(d) { + if (d.date instanceof Date) return; + var [m, day, y] = d.date.split("/").map(Number); + y = y > 50 ? 1900 + y : 2000 + y; + d.date = new Date(y, m - 1, day); + + let total_amount = 0; + for (item in d) { + if (item != "date") { + total_amount += +d[item]; + } + } + d.total_amount = total_amount; + }); + + // Restructure tabular data for easier charting. + var dataKeys = Object.keys(data[0]); + var removeItems = ["date","total_amount"]; + for (var i = 0; i < removeItems.length; i++) { + + let index = dataKeys.indexOf(removeItems[i]); + if (index > -1) { + dataKeys.splice(index,1) + } + } + + var stackedData = d3.stack().keys(dataKeys)(data); + + return stackedData; +}; + +// Render the graphic(s). Called by pym with the container width. +var render = function(data) { + // Render the chart! + var container = "#stacked-area-chart"; + var element = document.querySelector(container); + var width = element.offsetWidth; + renderAreaChart({ + container, + width, + data, + dateColumn: "date", + valueColumn: "amt" + }); + + // Update iframe + if (pymChild) { + pymChild.sendHeight(); + } +}; + +//Initially load the graphic +// (NB: Use window.load to ensure all images have loaded) +window.onload = onWindowLoaded; diff --git a/stacked_area_chart/graphic.less b/stacked_area_chart/graphic.less new file mode 100644 index 0000000..9acbab9 --- /dev/null +++ b/stacked_area_chart/graphic.less @@ -0,0 +1,25 @@ +@import "./lib/base"; + +.value text { + font-size: 12px; + font-weight: bold; + fill: #999; +} + +@media screen and (max-width: 500px) { + .value text { + font-size: 10px; + } +} + +@media screen and (min-width: 500px) { + .key { + display: none; + } +} + +@media screen and (max-width: 500px) { + .key.one-line { + display: none; + } +} \ No newline at end of file diff --git a/stacked_area_chart/index.html b/stacked_area_chart/index.html new file mode 100644 index 0000000..ee498f4 --- /dev/null +++ b/stacked_area_chart/index.html @@ -0,0 +1,37 @@ +<%= await t.include("lib/_head.html") %> + +<% if (COPY.labels.headline) { %> +

<%= t.smarty(COPY.labels.headline) %>

+<% } %> + +<% if (COPY.labels.subhed) { %> +

<%= t.smarty(COPY.labels.subhed) %>

+<% } %> + + + +<% if (COPY.labels.footnote) { %> +
+

Notes

+

<%= COPY.labels.footnote %>

+
+<% } %> + + + + + + + +<%= await t.include("lib/_foot.html") %> \ No newline at end of file diff --git a/stacked_area_chart/manifest.json b/stacked_area_chart/manifest.json new file mode 100644 index 0000000..9601c97 --- /dev/null +++ b/stacked_area_chart/manifest.json @@ -0,0 +1,52 @@ +{ + "templateSheet": "1DLxMcQRpyp1rqGJTjC28jJH5Df1GYrJrJnBl2PW9-MU", + "files": [ + "*.html", + "!_*.html", + "graphic.js", + "graphic.less", + "*.png", + "*.jpg", + "*.gif", + "*.json", + "!manifest.json", + "*.geojson", + "*.csv" + ], + "sheet": "1hvYBq5P5ud1OEHxEDLqLbnUawnQXRmYKfK_3MWQcExs", + "installedPackagesAtCreation": { + "component-leaflet-map": "0.0.17", + "d3": "^5.7.0", + "d3-array": "^2.0.3", + "d3-axis": "^1.0.12", + "d3-color": "^1.2.3", + "d3-dispatch": "^1.0.5", + "d3-ease": "^1.0.5", + "d3-fetch": "^1.1.2", + "d3-force": "^2.0.1", + "d3-geo": "^1.11.3", + "d3-geo-projection": "^2.6.0", + "d3-hierarchy": "^1.1.9", + "d3-path": "^1.0.7", + "d3-sankey": "^0.7.1", + "d3-scale": "^2.2.2", + "d3-scale-chromatic": "^1.5.0", + "d3-selection": "^1.4.0", + "d3-shape": "^1.2.2", + "d3-svg": "^0.2.2", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.3", + "d3-timer": "^1.0.9", + "d3-transform": "^1.0.5", + "d3-transition": "^1.2.0", + "jquery": "^3.3.1", + "mapshaper": "^0.4.154", + "readjson": "^1.1.4", + "tablesort": "^5.1.0", + "textures": "^1.2.2", + "topojson": "^3.0.2", + "topojson-client": "^3.0.1", + "wherewolf": "^1.0.3", + "world-atlas": "^2.0.2" + } +} \ No newline at end of file diff --git a/stacked_area_chart/renderAreaChart.js b/stacked_area_chart/renderAreaChart.js new file mode 100644 index 0000000..99617a7 --- /dev/null +++ b/stacked_area_chart/renderAreaChart.js @@ -0,0 +1,225 @@ +var d3 = { + ...require("d3-axis/dist/d3-axis.min"), + ...require("d3-scale/dist/d3-scale.min"), + ...require("d3-selection/dist/d3-selection.min"), + ...require("d3-shape/dist/d3-shape.min"), + ...require("d3-interpolate/dist/d3-interpolate.min") +}; + +var { COLORS, classify, makeTranslate } = require("./lib/helpers"); +var { yearFull, yearAbbrev } = require("./lib/helpers/formatDate"); +var { isMobile } = require("./lib/breakpoints"); + +// Render a area chart. +module.exports = function(config) { + + // Setup + var { dateColumn, valueColumn } = config; + + var aspectWidth = isMobile.matches ? 4 : 16; + var aspectHeight = isMobile.matches ? 3 : 9; + + var margins = { + top: 5, + right: 75, + bottom: 20, + left: 30 + }; + + var ticksX = 10; + var ticksY = 10; + var roundTicksFactor = 5; + + // Mobile + if (isMobile.matches) { + ticksX = 5; + ticksY = 5; + margins.right = 25; + } + + // Calculate actual chart dimensions + var chartWidth = config.width - margins.left - margins.right; + var chartHeight = + Math.ceil((config.width * aspectHeight) / aspectWidth) - + margins.top - + margins.bottom; + + // Clear existing graphic (for redraw) + var containerElement = d3.select(config.container); + containerElement.html(""); + + var dates = config.data[0].map(d => { + return d.data.date + }) + + var extent = [dates[0], dates[dates.length - 1]]; + + var xScale = d3 + .scaleTime() + .domain(extent) + .range([0, chartWidth]); + + var values = config.data[0].map(d => d.data.total_amount); + + var floors = values.map( + v => Math.floor(v / roundTicksFactor) * roundTicksFactor + ); + var min = Math.min.apply(null, floors); + + if (min > 0) { + min = 0; + } + + var ceilings = values.map( + v => Math.ceil(v / roundTicksFactor) * roundTicksFactor + ); + var max = Math.max.apply(null, ceilings); + + if (min > 0) { + min = 0; + } + + var yScale = d3 + .scaleLinear() + .domain([min, max]) + .range([chartHeight, 0]); + + var colorScale = d3 + .scaleOrdinal() + .domain( + config.data.map(function(d) { + return d.name; + }) + ) + .range([ + COLORS.red3, + COLORS.yellow3, + COLORS.blue3, + COLORS.orange3, + COLORS.teal3 + ]); + + // Render the HTML legend. + + var oneLine = config.data.length > 1 ? "" : " one-line"; + + var legend = containerElement + .append("ul") + .attr("class", "key" + oneLine) + .selectAll("g") + .data(config.data) + .enter() + .append("li") + .attr("class", d => "key-item " + classify(d.key)); + + legend.append("b").style("background-color", d => colorScale(d.key)); + + legend.append("label").text(d => d.key); + + // Create the root SVG element. + + var chartWrapper = containerElement + .append("div") + .attr("class", "graphic-wrapper"); + + var chartElement = chartWrapper + .append("svg") + .attr("width", chartWidth + margins.left + margins.right) + .attr("height", chartHeight + margins.top + margins.bottom) + .append("g") + .attr("transform", `translate(${margins.left},${margins.top})`); + + // Create D3 axes. + + var xAxis = d3 + .axisBottom() + .scale(xScale) + .ticks(ticksX) + .tickFormat(function(d, i) { + if (isMobile.matches) { + return "\u2019" + yearAbbrev(d); + } else { + return yearFull(d); + } + }); + + var yAxis = d3 + .axisLeft() + .scale(yScale) + .ticks(ticksY); + + // Render axes to chart. + + chartElement + .append("g") + .attr("class", "x axis") + .attr("transform", makeTranslate(0, chartHeight)) + .call(xAxis); + + chartElement + .append("g") + .attr("class", "y axis") + .call(yAxis); + + // Render grid to chart. + + var xAxisGrid = function() { + return xAxis; + }; + + var yAxisGrid = function() { + return yAxis; + }; + + chartElement + .append("g") + .attr("class", "x grid") + .attr("transform", makeTranslate(0, chartHeight)) + .call( + xAxisGrid() + .tickSize(-chartHeight, 0, 0) + .tickFormat("") + ); + + chartElement + .append("g") + .attr("class", "y grid") + .call( + yAxisGrid() + .tickSize(-chartWidth, 0, 0) + .tickFormat("") + ); + + // Render 0 value line. + + if (min < 0) { + chartElement + .append("line") + .attr("class", "zero-line") + .attr("x1", 0) + .attr("x2", chartWidth) + .attr("y1", yScale(0)) + .attr("y2", yScale(0)); + } + + // Render areas to chart. + + var areaGen = d3 + .area() + // .curve(d3.curveStepBefore) + .x(d => xScale(d.data[dateColumn])) + .y0(function (d) { + return yScale(d[0]); + }) + .y1(d => yScale(d[1])); + + chartElement + .append("g") + .attr("class","areas") + .selectAll("path") + .data(config.data) + .join("path") + .attr("fill", d => colorScale(d.key)) + .attr("d", areaGen) + +};