charts/rangechart.mjs (340 lines of code) (raw):

import dataTools from "./shared/dataTools" import ColorScale from "./shared/colorscale" import colorPresets from "./constants/colors" import { getURLParams, numberFormat, mustache, mobileCheck, setMinToMax, textPadding, textPaddingMobile, bufferize, tickTok, contains, wrap } from './shared/toolbelt'; import { addLabel, clickLogging } from './shared/arrows' import { drawShowMore } from "./shared/showmore" /** * The Rangechart class creates a chart with a central circle, with bars on each side to denote a range * such as the maximum and minimum, or error bars */ export default class Rangechart { constructor(settings) { this.settings = settings this.init() } init() { // console.log('enableshowmore',this.settings.enableShowMore) drawShowMore(this.settings.enableShowMore) this.render() } render() { let { modules, type, colors, height, width, featuresWidth, featuresHeight, svgWidth, svgHeight, isMobile, title, subtitle, source, marginleft, margintop, marginbottom, marginright, tooltip, data, datum, labels, userkey, keys, rowHeight, enableShowMore, colorScheme, dropdown, groupBy, minX, maxX, xFormat, xScale, yScale, parseTime, xAxisLabel, hideKey, xColumn, minMax, columns } = this.settings d3.select("#graphicContainer svg").remove() const chartKey = d3.select("#chartKey") chartKey.html("") datum = JSON.parse(JSON.stringify(data)) colors = new ColorScale() let defaultColour = colorPresets.guardian[0] isMobile = mobileCheck() let circleRadius = 10 let keysToChart = keys.filter(d => d != 'Color' && d != groupBy) // Check our data columns are in order from smallest (left) to largest (right) // As otherwise the layout will break let keysInOrder = [] keysToChart.forEach(key => { keysInOrder.push({"key":key, "value":datum[0][key]}) }) keysInOrder.sort((a, b) => d3.ascending(a.value, b.value)) // console.log("keysInOrder", keysInOrder) //Reset to get the new keys in order keysToChart = [] // Add the sorted keys back to keysToChart keysInOrder.forEach(d => { keysToChart.push(d.key) }) //// console.log("keysToChart", keysToChart) const keyColor = dataTools.getKeysColors({ keys: keysToChart, userKey: userkey, option: { colorScheme : colorScheme } }) colors.set(keyColor.keys, keyColor.colors) svgWidth = document .querySelector("#graphicContainer") .getBoundingClientRect().width svgHeight = datum.length * rowHeight + margintop + marginbottom featuresWidth = svgWidth - marginright - marginleft featuresHeight = svgHeight - margintop - marginbottom let range = [] for (var i = 0; i < datum.length; i++) { keysToChart.forEach((d) => { range.push(datum[i][d]) }) } datum.forEach(function(d) { if (xFormat.date) { keysToChart.forEach((lolly) => { if (contains(keysToChart, lolly)) { d[lolly] = parseTime( d[lolly] ) } }) } }) let extent = d3.extent(range) // Removed temporarily as getMinMax is only needed for charts where we want to use the same min and max on the positive and negative x axis, getMinMax // has been renamed to setMinToMa // const minMax = getMinMax(range.map(d => d)) // let max = minMax.max // let min = minMax.min // Bufferize now takes a percetange as the third argument let buffer = bufferize(extent[0], extent[1]) minX = (!isNaN(minX)) ? buffer[0] : +minX maxX = (!isNaN(maxX)) ? buffer[1] : +maxX const svg = d3 .select("#graphicContainer") .append("svg") .attr("width", svgWidth) .attr("height", svgHeight) .attr("id", "svg") .attr("overflow", "hidden") const features = svg .append("g") .attr("transform","translate(" + marginleft + "," + margintop + ")") var x = d3[xScale]() x.range([ 0, featuresWidth]); //features.append("g") //.attr("transform", "translate(0," + svgHeight + ")") //.call(d3.axisBottom(x)) // console.log("yScale", yScale) var y = d3[yScale]() .range([ 0, featuresHeight]) .domain(datum.map(function(d) { return d[groupBy]; })) .padding(0.9); (xFormat.date) ? x.domain(d3.extent(range)) : x.domain([minX, maxX]); //.nice() // if (minMax.status || x(0) > marginleft) { features.append('line') .style("stroke", "#767676") .style("stroke-width", 1) .attr("x1", x(0)) .attr("y1", margintop / 2) .attr("x2", x(0)) .attr("y2", svgHeight) .attr("opacity", 0.5); } features .selectAll(".barText") .data(datum) .enter() .append("text") .attr("class", "barText") .attr("x", function(d) { // let range = [] // for (var i = 0; i < keysToChart.length; i++) { // range.push(d[keysToChart[i]]) // } // return x(d3.min(range)) - 3 // return marginleft + 20 return x(d[keysToChart[1]]) }) .attr("text-anchor",function(d) { if (x(d[keysToChart[1]]) < 100) { return "start" } else { return "end" } //(minMax.status) ? "start" : "end" ; }) .attr("y", function(d) { return y(d[groupBy]) - (rowHeight / 3); }) .text((d) => d[groupBy]) const xTicks = tickTok(isMobile, x.domain(), featuresWidth) // Set the number of ticks const xAxis = g => g .attr("transform", `translate(0,${margintop / 2})`) .attr("class", "axisgroup") .call(d3.axisTop(x).tickSizeOuter(0)) .call(d3.axisTop(x) .tickSize(-svgHeight, 0, 0) .ticks(xTicks) .tickFormat((d) => { return xFormat.date ? d3.timeFormat("%b %Y")(d) : numberFormat(d) }) .tickPadding(10)) const yAxis = g => g .call(d3.axisLeft(y)) features .append("g") .attr("class", "x") .attr("transform", "translate(0," + svgHeight + ")") .call(xAxis) features.selectAll(".rangeBars") .data(datum) .enter() .append("rect") .attr("x", function(d) { return x(+d[keysToChart[0]]); }) .attr("width", (d) => ( x(+d[keysToChart[1]]) - x(+d[keysToChart[0]]))) .attr("y", function(d) { return y(d[groupBy]) - (circleRadius/2) }) .attr("height", circleRadius) .attr("fill", function(d,i) { let defs = svg.append("defs"); let gradient = defs.append("linearGradient") .attr("id", `svgGradient_left_${i}`) // .attr("gradientUnits", "userSpaceOnUse") // .attr("x1", start) // .attr("x2", finish) // .attr("y1", y(d[groupBy])) // .attr("y2",y(d[groupBy]) + 10) gradient.append("stop") .attr("class", "start") .attr("offset", "0%") .attr("stop-color", (d.Color) ? d.Color : defaultColour) .attr("stop-opacity", 0); gradient.append("stop") .attr("class", "end") .attr("offset", "100%") .attr("stop-color", (d.Color) ? d.Color : defaultColour) .attr("stop-opacity", 1); return `url(#svgGradient_left_${i})` }) features.selectAll(".rangeBars") .data(datum) .enter() .append("rect") .attr("x", function(d) { return x(+d[keysToChart[1]]); }) .attr("width", (d) => ( x(+d[keysToChart[2]]) - x(+d[keysToChart[1]]))) .attr("y", function(d) { return y(d[groupBy]) - (circleRadius/2) }) .attr("height", circleRadius) .attr("fill", function(d,i) { let defs = svg.append("defs"); let gradient = defs.append("linearGradient") .attr("id", `svgGradient_right_${i}`) // .attr("gradientUnits", "userSpaceOnUse") // .attr("x1", start) // .attr("x2", finish) // .attr("y1", y(d[groupBy])) // .attr("y2",y(d[groupBy]) + 10) gradient.append("stop") .attr("class", "start") .attr("offset", "0%") .attr("stop-color", (d.Color) ? d.Color : defaultColour) .attr("stop-opacity", 1); gradient.append("stop") .attr("class", "end") .attr("offset", "100%") .attr("stop-color", (d.Color) ? d.Color : defaultColour) .attr("stop-opacity", 0); return `url(#svgGradient_right_${i})` }) // features // .append("circle") // .attr("cx", `${ (featuresWidth) /2}`) // .attr("cy", `${ (featuresHeight) /2}`) // .attr("fill", "red") // .attr("r", 5) if (!hideKey) { if (keysToChart.length > 1) { keysToChart.forEach((k, i) => { const keyDiv = chartKey .append("div") .attr("class", "keyDiv") keyDiv .append("span") .attr("class", "keyCircle") .style("background-color", () => colors.get(k)) keyDiv .append("span") .attr("class", "keyText") .text(k) }) } } if (xAxisLabel) { svg .append("text") .attr("x", marginleft) .attr("y", margintop / 2) .attr("fill", "#767676") .attr("text-anchor", "start") .text(xAxisLabel) //.call(wrap, marginleft > 15 ? marginleft - 15 : marginleft); // Assuming `maxWidth` is defined } features.selectAll(".lolly") .data(datum) .enter() .append("circle") .attr("cx", function(d) { return x(+d[keysToChart[1]]); }) .attr("cy", function(d) { return y(d[groupBy]); }) .attr("r", circleRadius) .style("fill", (d, i) => { return (d.Color) ? d.Color : defaultColour }) .style("stroke", (d, i) => { return (d.Color) ? d.Color : defaultColour }) features.selectAll(".circleText") .data(datum) .enter() .append("text") .attr("x", function(d) { return x(+d[keysToChart[1]]); }) .attr("y", function(d) { return y(d[groupBy]) + circleRadius/2 - 2 }) .style("font-size", circleRadius) .style("font-weight", "bold") .style("fill", (d, i) => { return "#FFF"; }) .text(d => d[keysToChart[1]]) .attr("text-anchor", "middle") if (labels.length > 0) { const clickLoggingOn = getURLParams("labelling") ? true : false ; // console.log("clickLoggingOn", clickLoggingOn); // Move this to wrangle later once we re-factor the labelling stuff if (typeof labels[0].coords === 'string') { labels.forEach(function(d) { d.coords = JSON.parse(d.coords) d.sweepFlag = +d.sweepFlag d.largeArcFlag = +d.largeArcFlag d.radius = +d.radius }) } // console.log("annotations", labels) labels.forEach((config) => { addLabel(svg, config, svgWidth, svgHeight, { "left": marginleft, "right": marginright, "top": margintop, "bottom": marginbottom }, clickLoggingOn) }) } } }