From 89d052ae4fc3fda9e0d9e24605ddaf01fd4a0a05 Mon Sep 17 00:00:00 2001 From: Shelly Cheng Date: Fri, 7 Jul 2023 00:16:03 -0700 Subject: [PATCH] add new template: dot chart multiple. closes #25 (#37) --- dot_chart_multiple/graphic.js | 72 ++++++++ dot_chart_multiple/graphic.less | 159 +++++++++++++++++ dot_chart_multiple/index.html | 44 +++++ dot_chart_multiple/manifest.json | 17 ++ dot_chart_multiple/renderDotChart.js | 244 +++++++++++++++++++++++++++ 5 files changed, 536 insertions(+) create mode 100644 dot_chart_multiple/graphic.js create mode 100644 dot_chart_multiple/graphic.less create mode 100644 dot_chart_multiple/index.html create mode 100644 dot_chart_multiple/manifest.json create mode 100644 dot_chart_multiple/renderDotChart.js diff --git a/dot_chart_multiple/graphic.js b/dot_chart_multiple/graphic.js new file mode 100644 index 0000000..9b63366 --- /dev/null +++ b/dot_chart_multiple/graphic.js @@ -0,0 +1,72 @@ +var pym = require("./lib/pym"); +var { isMobile } = require("./lib/breakpoints"); +require("./lib/webfonts"); + +// Global vars +var pymChild = null; +var renderDotChart = require("./renderDotChart"); + +// Initialize the graphic. +var onWindowLoaded = function() { + if (document.body.classList.contains('promo')) { + isPromo = true; + } + + render(window.DATA); + window.addEventListener("resize", () => render(window.DATA)); + + pym.then(child => { + pymChild = child; + pymChild.sendHeight(); + }); +}; + +// Render the graphic(s). Called by pym with the container width. +var render = function(data) { +// Render the chart! +var container = "#dot-chart"; +var element = document.querySelector(container); +element.innerHTML = ""; +var width = element.offsetWidth; + +// draw the charts +if (isMobile.matches) { + for (var i = 0; i < data.length; i++) { + var chartDiv = document.createElement('div'); + chartDiv.className = 'chart chart-' + i; + element.appendChild(chartDiv); + + // Render the chart! + renderDotChart({ + container: container + ' .chart-' + i, + width, + data: [ data[i] ], + idx: i, + labelColumn: "label", + minColumn: "min", + maxColumn: "max" + }); + } +} else { + // Render the chart! + renderDotChart({ + container, + width, + data, + labelColumn: "label_fmt", + minColumn: "min", + maxColumn: "max" + }); +} + +// 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/dot_chart_multiple/graphic.less b/dot_chart_multiple/graphic.less new file mode 100644 index 0000000..f5f3043 --- /dev/null +++ b/dot_chart_multiple/graphic.less @@ -0,0 +1,159 @@ +@import "./lib/base"; + +@overall: #666; +@overall-stroke: #333; +@democrats: #237bbd; +@dem-stroke: darken(@democrats, 6%); +@republicans: #d62021; +@rep-stroke: darken(@republicans, 6%); +@independents: lighten(#15b16e, 2%); +@ind-stroke: darken(@independents, 6%); + +h1 { + font-size: 18px; + margin-bottom: 15px; + text-align: left; + + strong { color: #333; } +} + +.graphic h4 { + .gotham(); + color: #666; + font-size: 13px; + margin: 0 0 11px 0; + // text-align: center; +} + +.chart + .chart { + margin-top: 33px; +} + +.graphic-wrapper { position: relative; } + +.key { + display: flex; + flex-wrap: wrap; + margin-top: 0; + margin-bottom: 15px; + justify-content: center; + + @media @screen-mobile { + margin-bottom: 25px; + } + + .key-item { + display: block; + margin: 0 11px 3px 11px; + + @media @screen-mobile { + &.overall { + flex-basis: 100%; + margin-left: auto; + margin-right: auto; + text-align: center; + } + } + + b { + border-radius: 50px; + overflow: hidden; + width: 11px; + height: 11px; + opacity: 0.7; + float: none; + display: inline-block; + } + &.overall { + b { background-color: @overall; } + + label { + font-weight: bold; + color: #333; + } + } + + &.republicans b { background: @republicans; } + &.democrats b { background: @democrats; } + &.independents b { background: @independents; } + } +} + +sup { + color: #999; + line-height: 0; + padding-left: 2px; +} + +.bars line { + fill: none; + stroke-width: 4px; + stroke: #ddd; +} + +circle { + fill: #D8472B; + fill-opacity: .75; + + .overall & { + fill: @overall; + fill-opacity: 1; + } + .democrats & { fill: @democrats; } + .republicans & { fill: @republicans; } + .independents & { fill: @independents; } +} + +line { + .overall & { + stroke: @overall-stroke; + } +} + +.value text { + fill: #666; + font-size: 11px; + text-anchor: middle; +} + +.value { + font-style: italic; + + .overall text { + fill: @overall; + text-anchor: middle; + font-style: normal; + font-weight: bold; + } + .democrats text { fill: @democrats; } + .republicans text { fill: @republicans; } + .independents text { fill: @ind-stroke; } + + text.hdr { + color: #333; + font-size: 12px; + font-weight: bold; + text-anchor: start; + } +} + +.labels li { + font-size: 13px; + + strong { color: #333; } +} + +.promo { + h1 { + .knockout-header(); + } + .labels li { + font-size: 14px; + } + .value text, + .shadow text { + font-size: 13px; + + .hdr { font-size: 14px; } + } +} diff --git a/dot_chart_multiple/index.html b/dot_chart_multiple/index.html new file mode 100644 index 0000000..96c576a --- /dev/null +++ b/dot_chart_multiple/index.html @@ -0,0 +1,44 @@ +<%= 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") %> diff --git a/dot_chart_multiple/manifest.json b/dot_chart_multiple/manifest.json new file mode 100644 index 0000000..4ff52cb --- /dev/null +++ b/dot_chart_multiple/manifest.json @@ -0,0 +1,17 @@ +{ + "templateSheet": "1BsblzjPNM7tvl71EOr5UneQKOTBMM7EhZS4R1dOAdb8", + "files": [ + "*.html", + "!_*.html", + "graphic.js", + "graphic.less", + "*.png", + "*.jpg", + "*.gif", + "*.json", + "!manifest.json", + "*.geojson", + "*.csv", + "!README.md" + ] + } \ No newline at end of file diff --git a/dot_chart_multiple/renderDotChart.js b/dot_chart_multiple/renderDotChart.js new file mode 100644 index 0000000..21fd414 --- /dev/null +++ b/dot_chart_multiple/renderDotChart.js @@ -0,0 +1,244 @@ +var d3 = { + ...require("d3-selection/dist/d3-selection.min"), + ...require("d3-axis/dist/d3-axis.min"), + ...require("d3-scale/dist/d3-scale.min") + }; + + var { makeTranslate, classify, formatStyle } = require("./lib/helpers"); + var { isMobile } = require("./lib/breakpoints"); + + // Render a bar chart. + module.exports = function(config) { + // Setup + + var { labelColumn, minColumn, maxColumn } = config; + + var categories = Object.keys(config['data'][0]).filter(function(d) { + if ([ "label", "label_fmt", "min", "max" ].indexOf(d) < 0) { + return d; + } + }); + + var barHeight = 80; + var barGap = 10; + var barOffset = 2; + var labelWidth = 270; + var labelMargin = 10; + var valueMinWidth = 30; + var dotRadius = 6; + + var tickValues = [ 0, 25, 50, 75, 100 ]; + + var margins = { + top: 0, + right: 20, + bottom: 20, + left: labelWidth + labelMargin + }; + + + if (isMobile.matches) { + barHeight = 45; + barOffset = 5; + labelMargin = 10; + labelWidth = 0; + margins['left'] = (labelWidth + labelMargin); + } + + // if (isPromo) { + // barHeight = 35; + // labelWidth = 100; + // } + + // Calculate actual chart dimensions + var chartWidth = config.width - margins.left - margins.right; + var chartHeight = (barHeight + barGap) * config.data.length; + + // Clear existing graphic (for redraw) + var containerElement = d3.select(config.container); + // containerElement.html(""); + + if (isMobile.matches) { + containerElement.append('h4') + .html(config['data'][0]['label_fmt']) + } + + // 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})`); + + // Render bar labels (desktop only) + if (!isMobile.matches) { + containerElement + .append("ul") + .attr("class", "labels") + .attr( + "style", + formatStyle({ + width: labelWidth + "px", + top: margins.top + "px", + left: "0" + }) + ) + .selectAll("li") + .data(config.data) + .enter() + .append("li") + .attr("style", (d, i) => + formatStyle({ + width: labelWidth + "px", + height: barHeight + "px", + left: "0px", + top: i * (barHeight + barGap) + "px;" + }) + ) + // .attr("class", d => classify(d[labelColumn])) + .append("span") + .html(d => d[labelColumn]); + } + + // Create D3 scale objects. + var min = tickValues[0]; + var max = tickValues[tickValues.length - 1]; + + var xScale = d3 + .scaleLinear() + .domain([min, max]) + .range([0, chartWidth]); + + // Create D3 axes. + var xAxis = d3 + .axisBottom() + .scale(xScale) + .tickValues(tickValues) + .tickFormat(d => d + "%"); + + // Render axes to chart. + chartElement + .append("g") + .attr("class", "x axis") + .attr("transform", makeTranslate(0, chartHeight)) + .call(xAxis); + + // Render grid to chart. + chartElement + .append("g") + .attr("class", "x grid") + .attr("transform", makeTranslate(0, chartHeight)) + .call( + xAxis + .tickSize(-chartHeight, 0, 0) + .tickFormat("") + ); + + // Render range bars to chart. + chartElement + .append("g") + .attr("class", "bars") + .selectAll("line") + .data(config.data) + .enter() + .append("line") + .attr("x1", d => xScale(d[minColumn])) + .attr("x2", d => xScale(d[maxColumn])) + .attr("y1", (d, i) => i * (barHeight + barGap) + (barHeight / 2) + barOffset) + .attr("y2", (d, i) => i * (barHeight + barGap) + (barHeight / 2) + barOffset); + + // Render dots to chart. + var dots = chartElement.append("g") + .attr("class", "dots") + .selectAll("g") + .data(categories) + .enter() + .append("g") + .attr("class", d => classify(d)); + + dots.selectAll("circle") + .data(d => config.data.map(o => o[d])) + .enter() + .append("circle") + .attr("cx", d => xScale(d)) + .attr("cy", (d, i) => i * (barHeight + barGap) + barHeight / 2 + barOffset) + .attr("r", dotRadius); + + // add dot annotations + chartElement.select('.dots .overall') + .selectAll('line') + .data(d => config.data.map(o => o["Overall"])) + .enter().append('line') + .attr('x1', function(d) { + return xScale(d); + }) + .attr('x2', function(d) { + return xScale(d); + }) + .attr('y1', function(d, i) { + return i * (barHeight + barGap) + (barHeight / 2) + barOffset - dotRadius; + }) + .attr('y2', function(d, i) { + return i * (barHeight + barGap) + (barHeight / 2) + barOffset - dotRadius - 8; + }); + + // Render bar values. + var cls = "value"; + var dotValues = chartElement.append('g') + .attr('class', cls) + .selectAll('g') + .data(categories) + .enter().append('g') + .attr('class', function(d) { + return classify(d); + }); + dotValues.selectAll('text') + .data(d => config.data.map(o => o[d])) + .enter().append('text') + .attr('x', function(d, i) { + if (d == 80) { + return xScale(d) - 5; + } + + if (d == 92) { + return xScale(d) + 10; + } + + if (d == 72) { + return xScale(d) - 10; + } + return xScale(d); + }) + .attr('y', function(d,i) { + var offset = 20; + var yPos = i * (barHeight + barGap) + (barHeight / 2) + barOffset; + return yPos + offset; + }) + .text(function(d) { + return d + '%'; + }); + + chartElement.selectAll('.value .overall text') + .attr('dy', -35) + .text(function() { + var thisText = d3.select(this).text(); + return 'Overall: ' + thisText; + }); + + // adjust label placement for some questions + // if (isMobile) { + // if (config['idx'] == 0) { + // chartElement.select('.value .democrats text') + // .attr('dx', 17); + // } + // } else { + // chartElement.select('.value .democrats text:nth-child(1)') + // .attr('dx', 15); + // } + }; + \ No newline at end of file