diff --git a/column-chart-stacked-with-line-sm/chart.css b/column-chart-stacked-with-line-sm/chart.css index f84cb23..5d6c9ac 100644 --- a/column-chart-stacked-with-line-sm/chart.css +++ b/column-chart-stacked-with-line-sm/chart.css @@ -1,4 +1,4 @@ -.dataLabels{ +.dataLabels { font-weight: 600; font-size: 14px; } @@ -12,12 +12,10 @@ margin-right: 0; } - - -#legend{ - display: grid; - align-items: center; - padding-bottom: 20px; +.diamondStyle { + stroke-width: 2px; + stroke-linejoin: round; + fill: white; } .dataLine { @@ -25,4 +23,4 @@ stroke-linecap: round; stroke-linejoin: round; fill: none; -} \ No newline at end of file +} diff --git a/column-chart-stacked-with-line-sm/config.js b/column-chart-stacked-with-line-sm/config.js index 01088f2..5a15c2a 100644 --- a/column-chart-stacked-with-line-sm/config.js +++ b/column-chart-stacked-with-line-sm/config.js @@ -1,83 +1,86 @@ config = { - "essential": { - "graphic_data_url": "data.csv", - "colour_palette": ["#206095", "#27A0CC", "#871A5B", "#A8BD3A", "#F66068"], - "line_colour": "#222222", - "sourceText": "Office for National Statistics", - "accessibleSummary": "Here is the screenreader text describing the chart.", - "xAxisTickFormat": { - "sm": "%Y", - "md": "%Y", - "lg": "%b-%y" - }, - "xAxisNumberFormat": ".0f", - "yAxisTickFormat": ".0f", - "dateFormat": "%b-%y", - //the format your date data has in data.csv - "yDomain": "auto", - // either "auto" or an array for the x domain e.g. [0,100] - "line_series": "Category 5", - "yAxisLabel": "y axis label", - "stackOffset": "stackOffsetNone", - // options include - // stackOffsetNone means the baseline is set at zero - // stackOffsetExpand to do 100% charts - // stackOffsetDiverging for data with positive and negative values - "stackOrder": "stackOrderNone" - // other options include - // stackOrderNone means the order is taken from the datafile - // stackOrderAppearance the earliest series (according to the maximum value) is at the bottom - // stackOrderAscending the smallest series (according to the sum of values) is at the bottom - // stackOrderDescending the largest series (according to the sum of values) is at the bottom - // stackOrderReverse reverse the order as set from the data file - }, - "optional": { - "margin": { - "sm": { - "top": 50, - "right": 20, - "bottom": 50, - "left": 70 - }, - "md": { - "top": 50, - "right": 20, - "bottom": 50, - "left": 70 - }, - "lg": { - "top": 50, - "right": 20, - "bottom": 50, - "left": 70 - } - }, - "chartGap": 10, - "chart_every": { - "sm": 1, - "md": 2, - "lg": 2 - }, - "aspectRatio": { - "sm": [1, 1], - "md": [1, 1], - "lg": [4, 3] - }, - "xAxisTicksEvery": { // this is the interval of ticks on the x axis - always including the first and last date - "sm": 4, - "md": 4, - "lg": 3 - }, - "yAxisTicks": { - "sm": 10, - "md": 8, - "lg": 4 - }, - "legendColumns": 4, - "mobileBreakpoint": 510, - "mediumBreakpoint": 600, - "dropYAxis": true - }, - "elements": { "select": 0, "nav": 0, "legend": 0, "titles": 0 }, - "chart_build": {} + essential: { + graphic_data_url: "data.csv", + showMarkers: true, + showLine: true, + colour_palette: ["#206095", "#A8BD3A", "#871A5B", "#F66068", "#05341A"], + line_colour: "#222222", + sourceText: "Office for National Statistics", + accessibleSummary: "Here is the screenreader text describing the chart.", + xAxisTickFormat: { + sm: "%b", + md: "%b", + lg: "%b", + }, + xAxisNumberFormat: ".0f", + yAxisTickFormat: ".0f", + dateFormat: "%b-%y", + //the format your date data has in data.csv + yDomain: "auto", + // either "auto" or an array for the x domain e.g. [0,100] + line_series: "Total", + yAxisLabel: "y axis label", + stackOffset: "stackOffsetNone", + // options include + // stackOffsetNone means the baseline is set at zero + // stackOffsetExpand to do 100% charts + // stackOffsetDiverging for data with positive and negative values + stackOrder: "stackOrderNone", + // other options include + // stackOrderNone means the order is taken from the datafile + // stackOrderAppearance the earliest series (according to the maximum value) is at the bottom + // stackOrderAscending the smallest series (according to the sum of values) is at the bottom + // stackOrderDescending the largest series (according to the sum of values) is at the bottom + // stackOrderReverse reverse the order as set from the data file + }, + optional: { + margin: { + sm: { + top: 50, + right: 5, + bottom: 50, + left: 40, + }, + md: { + top: 50, + right: 20, + bottom: 50, + left: 40, + }, + lg: { + top: 50, + right: 30, + bottom: 50, + left: 40, + }, + }, + chartGap: 10, + chart_every: { + sm: 2, + md: 2, + lg: 2, + }, + aspectRatio: { + sm: [1.5, 1], + md: [1.5, 1], + lg: [1.5, 1], + }, + xAxisTicksEvery: { + // this is the interval of ticks on the x axis - always including the first and last date + sm: 12, + md: 2, + lg: 2, + }, + yAxisTicks: { + sm: 4, + md: 4, + lg: 6, + }, + legendColumns: 4, + mobileBreakpoint: 510, + mediumBreakpoint: 600, + dropYAxis: true, + }, + elements: { select: 0, nav: 0, legend: 0, titles: 0 }, + chart_build: {}, }; diff --git a/column-chart-stacked-with-line-sm/data.csv b/column-chart-stacked-with-line-sm/data.csv index af31ce4..e71709f 100644 --- a/column-chart-stacked-with-line-sm/data.csv +++ b/column-chart-stacked-with-line-sm/data.csv @@ -1,41 +1,41 @@ -date,series,Category 1,Category 2,Category 3,Category 4 with a long name,Category 5 -Jan-20,series1,20.85373832,17.6949027,84.86276061,31.5783842,48.20562268 -Feb-20,series1,75.10385606,92.78801932,98.42939789,95.19411478,38.58584155 -Mar-20,series1,64.12511024,36.80002708,38.20913822,4.832285581,31.60393462 -Apr-20,series1,58.04407128,91.82819229,44.15710484,93.53981204,32.06639495 -May-20,series1,49.29571885,61.7405718,2.633201818,12.00880742,86.95296893 -Jun-20,series1,26.42919394,68.3881695,38.34804204,45.46625342,24.65361361 -Jul-20,series1,66.02170789,25.88544503,67.54775376,45.4008622,68.65015727 -Aug-20,series1,24.56805884,89.47462279,49.36685439,50.05168647,65.708543 -Sep-20,series1,27.48215082,14.64375439,66.97898751,58.11839376,9.358939682 -Oct-20,series1,48.97683167,80.16796002,18.79872569,90.21841354,57.08565289 -Jan-20,series2,0.933950623,9.624031575,72.95935918,6.77595291,28.53102882 -Feb-20,series2,42.67837059,46.59217919,67.77733609,8.925781009,1.795089023 -Mar-20,series2,47.00071225,9.037706465,10.9849663,0.452189724,20.59393944 -Apr-20,series2,24.20432653,59.86689017,42.15415468,72.6617888,31.66840009 -May-20,series2,41.54685317,23.16632079,2.389611457,4.813825608,12.61856841 -Jun-20,series2,3.798081223,45.01552316,1.591733076,42.1961274,5.81108836 -Jul-20,series2,56.69771887,25.70328174,31.2276681,4.687134985,23.4460375 -Aug-20,series2,2.291284423,20.75322693,45.39235296,32.55708764,31.58368269 -Sep-20,series2,8.979986448,11.44169148,40.6953067,33.53376474,1.417492362 -Oct-20,series2,14.49825282,18.73452297,9.162783215,80.32986673,6.273620969 -Jan-20,series3,87.72957303,78.20844009,73.61438423,71.32424048,79.91439476 -Feb-20,series3,58.16540724,32.99819999,9.856849706,97.34373835,79.99683494 -Mar-20,series3,8.772748019,40.13236012,92.61020539,33.55972913,77.76240631 -Apr-20,series3,73.95082667,61.47976532,30.56231134,9.679967748,56.50124322 -May-20,series3,72.50402495,56.47240048,94.8410951,44.48195369,10.88876233 -Jun-20,series3,54.39548057,12.38889185,93.59330285,73.79213895,21.36077763 -Jul-20,series3,38.83710414,19.76902548,41.46335831,5.805762559,71.14340522 -Aug-20,series3,3.418394354,94.12353895,46.79323662,49.58736749,99.44353072 -Sep-20,series3,70.22092796,56.52766708,18.36433592,13.47723194,86.93400296 -Oct-20,series3,68.36646707,53.41870414,87.80039611,88.98978628,12.66577263 -Jan-20,series4,13.46519787,57.03145807,89.4723652,25.4734746,64.31632125 -Feb-20,series4,97.75793294,10.90663142,64.13005332,79.10043408,97.75434054 -Mar-20,series4,67.45128621,23.08017243,64.4892647,78.80150895,8.03937043 -Apr-20,series4,27.47475335,43.02540187,25.78801009,86.70448892,21.11512093 -May-20,series4,11.57834567,82.57230791,77.34477933,79.81166212,10.9685112 -Jun-20,series4,95.83571673,48.28154456,84.4705485,90.31647127,1.451346358 -Jul-20,series4,82.09708714,98.10893307,79.22420574,47.50524785,86.11139071 -Aug-20,series4,5.29225365,91.58355761,51.62819774,88.4223957,66.89798742 -Sep-20,series4,96.14198802,93.32172592,93.58391111,19.84140874,83.39377318 -Oct-20,series4,53.9721427,68.97760975,51.39568127,53.4496707,79.33609547 +date,series,Category 1,Category 2,Category 3,Category 4,Total +Jan-20,Group 1,20.85373832,17.6949027,84.86276061,31.5783842,48.20562268 +Feb-20,Group 1,75.10385606,92.78801932,98.42939789,95.19411478,38.58584155 +Mar-20,Group 1,64.12511024,36.80002708,38.20913822,4.832285581,31.60393462 +Apr-20,Group 1,58.04407128,91.82819229,44.15710484,93.53981204,32.06639495 +May-20,Group 1,49.29571885,61.7405718,2.633201818,12.00880742,86.95296893 +Jun-20,Group 1,26.42919394,68.3881695,38.34804204,45.46625342,24.65361361 +Jul-20,Group 1,66.02170789,25.88544503,67.54775376,45.4008622,68.65015727 +Aug-20,Group 1,24.56805884,89.47462279,49.36685439,50.05168647,65.708543 +Sep-20,Group 1,27.48215082,14.64375439,66.97898751,58.11839376,9.358939682 +Oct-20,Group 1,48.97683167,80.16796002,18.79872569,90.21841354,57.08565289 +Jan-20,Group 2,0.933950623,9.624031575,72.95935918,6.77595291,28.53102882 +Feb-20,Group 2,42.67837059,46.59217919,67.77733609,8.925781009,1.795089023 +Mar-20,Group 2,47.00071225,9.037706465,10.9849663,0.452189724,20.59393944 +Apr-20,Group 2,24.20432653,59.86689017,42.15415468,72.6617888,31.66840009 +May-20,Group 2,41.54685317,23.16632079,2.389611457,4.813825608,12.61856841 +Jun-20,Group 2,3.798081223,45.01552316,1.591733076,42.1961274,5.81108836 +Jul-20,Group 2,56.69771887,25.70328174,31.2276681,4.687134985,23.4460375 +Aug-20,Group 2,2.291284423,20.75322693,45.39235296,32.55708764,31.58368269 +Sep-20,Group 2,8.979986448,11.44169148,40.6953067,33.53376474,1.417492362 +Oct-20,Group 2,14.49825282,18.73452297,9.162783215,80.32986673,6.273620969 +Jan-20,Group 3,87.72957303,78.20844009,73.61438423,71.32424048,79.91439476 +Feb-20,Group 3,58.16540724,32.99819999,9.856849706,97.34373835,79.99683494 +Mar-20,Group 3,8.772748019,40.13236012,92.61020539,33.55972913,77.76240631 +Apr-20,Group 3,73.95082667,61.47976532,30.56231134,9.679967748,56.50124322 +May-20,Group 3,72.50402495,56.47240048,94.8410951,44.48195369,10.88876233 +Jun-20,Group 3,54.39548057,12.38889185,93.59330285,73.79213895,21.36077763 +Jul-20,Group 3,38.83710414,19.76902548,41.46335831,5.805762559,71.14340522 +Aug-20,Group 3,3.418394354,94.12353895,46.79323662,49.58736749,99.44353072 +Sep-20,Group 3,70.22092796,56.52766708,18.36433592,13.47723194,86.93400296 +Oct-20,Group 3,68.36646707,53.41870414,87.80039611,88.98978628,12.66577263 +Jan-20,Group 4,13.46519787,57.03145807,89.4723652,25.4734746,64.31632125 +Feb-20,Group 4,97.75793294,10.90663142,64.13005332,79.10043408,97.75434054 +Mar-20,Group 4,67.45128621,23.08017243,64.4892647,78.80150895,8.03937043 +Apr-20,Group 4,27.47475335,43.02540187,25.78801009,86.70448892,21.11512093 +May-20,Group 4,11.57834567,82.57230791,77.34477933,79.81166212,10.9685112 +Jun-20,Group 4,95.83571673,48.28154456,84.4705485,90.31647127,1.451346358 +Jul-20,Group 4,82.09708714,98.10893307,79.22420574,47.50524785,86.11139071 +Aug-20,Group 4,5.29225365,91.58355761,51.62819774,88.4223957,66.89798742 +Sep-20,Group 4,96.14198802,93.32172592,93.58391111,19.84140874,83.39377318 +Oct-20,Group 4,53.9721427,68.97760975,51.39568127,53.4496707,79.33609547 diff --git a/column-chart-stacked-with-line-sm/script.js b/column-chart-stacked-with-line-sm/script.js index 3e304bc..af970ab 100644 --- a/column-chart-stacked-with-line-sm/script.js +++ b/column-chart-stacked-with-line-sm/script.js @@ -1,335 +1,429 @@ -import { initialise, wrap, addSvg, calculateChartWidth, addChartTitleLabel, addAxisLabel, addSource } from "../lib/helpers.js"; - -let graphic = d3.select('#graphic'); -let legend = d3.select('#legend'); +import { + initialise, + wrap, + addSvg, + calculateChartWidth, + diamondShape, + addChartTitleLabel, + addAxisLabel, +} from "../lib/helpers.js"; + +let graphic = d3.select("#graphic"); +let legend = d3.select("#legend"); let pymChild = null; let graphic_data, size, svg; function drawGraphic() { - - //Set up some of the basics and return the size value ('sm', 'md' or 'lg') - size = initialise(size); - - // Nest the graphic_data by the 'series' column - let nested_data = d3.group(graphic_data, (d) => d.series); - - // Create a container div for each small multiple - let chartContainers = graphic - .selectAll('.chart-container') - .data(Array.from(nested_data)) - .join('div') - .attr('class', 'chart-container'); - - let xDataType; - - if (Object.prototype.toString.call(graphic_data[0].date) === '[object Date]') { - xDataType = 'date'; - } else { - xDataType = 'numeric'; - } - - // console.log(xDataType) - - function drawChart(container, seriesName, data, chartIndex) { - - const chartEvery = config.optional.chart_every[size]; - const chartsPerRow = config.optional.chart_every[size]; - let chartPosition = chartIndex % chartsPerRow; - - let margin = { ...config.optional.margin[size] }; - - let chartGap = config.optional?.chartGap || 10; - - let chart_width = calculateChartWidth({ - screenWidth: parseInt(graphic.style('width')), - chartEvery: chartsPerRow, - chartMargin: margin, - chartGap: chartGap - }) - - // If the chart is not in the first position in the row, reduce the left margin - if (config.optional.dropYAxis) { - if (chartPosition !== 0) { - margin.left = chartGap; - } - } - - const aspectRatio = config.optional.aspectRatio[size]; - - //height is set by the aspect ratio - let height = - aspectRatio[1] / aspectRatio[0] * chart_width; - - //set up scales - const y = d3.scaleLinear().range([height, 0]); - - const x = d3 - .scaleBand() - .paddingOuter(0.0) - .paddingInner(0.1) - .range([0, chart_width]) - .round(false); - - const colour = d3 - .scaleOrdinal() - .domain(graphic_data.columns.slice(2)) - .range(config.essential.colour_palette); - - //use the data to find unique entries in the date column - x.domain([...new Set(graphic_data.map((d) => d.date))]); - - //set up yAxis generator - const yAxis = d3.axisLeft(y) - .tickSize(-chart_width) - .tickPadding(10) - .ticks(config.optional.yAxisTicks[size]) - .tickFormat((d) => config.optional.dropYAxis !== true ? d3.format(config.essential.yAxisTickFormat)(d) : - chartPosition == 0 ? d3.format(config.essential.yAxisTickFormat)(d) : ""); - - const stack = d3 - .stack() - .keys(graphic_data.columns.slice(2).filter(d => (d) !== config.essential.line_series)) - .offset(d3[config.essential.stackOffset]) - .order(d3[config.essential.stackOrder]); - - const series = stack(data); - const seriesAll = stack(graphic_data); - - let xTime = d3.timeFormat(config.essential.xAxisTickFormat[size]) - - //set up xAxis generator - const xAxis = d3 - .axisBottom(x) - .tickSize(10) - .tickPadding(10) - .tickValues(xDataType == 'date' ? graphic_data - .map(function (d) { - return d.date.getTime() - }) //just get dates as seconds past unix epoch - .filter(function (d, i, arr) { - return arr.indexOf(d) == i - }) //find unique - .map(function (d) { - return new Date(d) - }) //map back to dates - .sort(function (a, b) { - return a - b - }) - .filter(function (d, i) { - return i % config.optional.xAxisTicksEvery[size] === 0 && i <= graphic_data.length - config.optional.xAxisTicksEvery[size] || i == data.length - 1 //Rob's fussy comment about labelling the last date - }) : x.domain().filter((d, i) => { return i % config.optional.xAxisTicksEvery[size] === 0 && i <= graphic_data.length - config.optional.xAxisTicksEvery[size] || i == data.length - 1 }) - ) - .tickFormat((d) => xDataType == 'date' ? xTime(d) - : d3.format(config.essential.xAxisNumberFormat)(d)); - - // //Labelling the first and/or last bar if needed - // if (config.optional.showStartEndDate == true) { - // xAxis.tickValues(x.domain().filter(function (d, i) { - // return !(i % config.optional.xAxisTicksEvery[size]) - // }).concat(x.domain()[0], x.domain()[x.domain().length - 1])) - // } - - //create svg for chart - const svg = addSvg({ - svgParent: container, - chart_width: chart_width, - height: height + margin.top + margin.bottom, - margin: margin - }) - - if (config.essential.yDomain == 'auto') { - y.domain(d3.extent(seriesAll.flat(2))); //flatten the arrays and then get the extent - } else { - y.domain(config.essential.yDomain); - } - - //Getting the list of colours used in this visualisation - let colours = [...config.essential.colour_palette].slice(0, graphic_data.columns.slice(2).length - 1) - - //gets array of arrays for individual lines - let lines = []; - for (let column in graphic_data[0]) { - if (column == 'date' || column == 'series') continue; - lines[column] = data.map(function (d) { - return { - 'name': d.date, - 'amt': d[column] - }; - }); - } - - // console.log("linesflat: ", Object.entries(lines).flat(3)) - - let counter; - // do some code to overwrite blanks with the last known point - let keys = Object.keys(lines); - for (let i = 0; i < keys.length; i++) { - // console.log(lines[keys[i]]) - lines[keys[i]].forEach(function (d, j) { - if (d.amt != "null") { - counter = j; - } else { - d.name = lines[keys[i]][counter].name - d.amt = lines[keys[i]][counter].amt - } - - }) - - } - - // console.log("keys: ", keys) - let bar_keys = keys.filter(d => d !== config.essential.line_series); - // console.log(bar_keys) - - // Set up the legend - let legenditem = d3 - .select('#legend') - .selectAll('div.legend--item') - .data( - d3.zip(bar_keys.reverse(), colours.reverse()) - ) - .enter() - .append('div') - .attr('class', 'legend--item'); - - legenditem - .append('div') - .attr('class', 'legend--icon--circle') - .style('background-color', function (d) { - return d[1]; - }); - - legenditem - .append('div') - .append('p') - .attr('class', 'legend--text') - .html(function (d) { - return d[0]; - }); - - if (size !== 'sm') { - d3.select('#legend') - .style('grid-template-columns', `repeat(${config.optional.legendColumns}, 1fr)`) - } - - svg - .append('g') - .attr('transform', 'translate(0,' + height + ')') - .attr('class', 'x axis') - .call(xAxis); - - svg - .append('g') - .attr('class', 'y axis numeric') //Can be numeric or categorical - .call(yAxis) - .selectAll('line') - .each(function (d) { - if (d == 0) { - d3.select(this).attr('class', 'zero-line'); - } - }) - .selectAll('text') - .call(wrap, margin.left - 10); - - //Columns - svg - .append('g') - .selectAll('g') - .data(series) - .join('g') - .attr('fill', (d, i) => config.essential.colour_palette[i]) - .selectAll('rect') - .data((d) => d) - .join('rect') - .attr('y', (d) => Math.min(y(d[0]), y(d[1]))) - .attr('x', (d) => x(d.data.date)) - .attr('height', (d) => Math.abs(y(d[0]) - y(d[1]))) - .attr('width', x.bandwidth()); - - //Lines - let thisCurve = d3.curveLinear - - let line = d3.line() - .defined((d) => d.amt !== 'null') - .curve(thisCurve) - .x((d) => x(d.name)) - .y((d) => y(d.amt)); - - let line_values = Object.entries(lines).filter(d => d[0] == config.essential.line_series); - - svg.append('g') - .selectAll('path') - .data(line_values) - .enter() - .append('path') - .attr("stroke", (d, i) => config.essential.line_colour) - .attr("class", "dataLine") - .attr('d', (d) => - line(d[1])) - .attr("transform", "translate(" + x.bandwidth() / 2 + ",0)"); - - // This does the chart title label - addChartTitleLabel({ - svgContainer: svg, - yPosition: -30, - text: seriesName, - wrapWidth: chart_width - }) - - // This does the y-axis label - addAxisLabel({ - svgContainer: svg, - xPosition: 5 - margin.left, - yPosition: -10, - text: chartIndex % chartEvery == 0 ? config.essential.yAxisLabel : "", - textAnchor: "start", - wrapWidth: chart_width - }); - }; - - // Draw the charts for each small multiple - chartContainers.each(function ([key, value], i) { - drawChart(d3.select(this), key, value, i); - }); - - d3.select('#legend') - .append('div') - .attr('class', 'legend--item line') - .append('div') - .attr('class', 'legend--icon--refline') - .style('background-color', config.essential.line_colour); - - d3.select('.legend--item.line') - .append('div') - .attr('class', 'legend--text') - .text(config.essential.line_series) - - //create link to source - addSource('source', config.essential.sourceText); - - //use pym to calculate chart dimensions - if (pymChild) { - pymChild.sendHeight(); - } + //Set up some of the basics and return the size value ('sm', 'md' or 'lg') + size = initialise(size); + + // Nest the graphic_data by the 'series' column + let nested_data = d3.group(graphic_data, (d) => d.series); + + // Create a container div for each small multiple + let chartContainers = graphic + .selectAll(".chart-container") + .data(Array.from(nested_data)) + .join("div") + .attr("class", "chart-container"); + + let xDataType; + + if ( + Object.prototype.toString.call(graphic_data[0].date) === "[object Date]" + ) { + xDataType = "date"; + } else { + xDataType = "numeric"; + } + + // Determine which columns to use for stacked bars based on showMarkers setting + let showMarkers = + config.essential.showMarkers !== undefined + ? config.essential.showMarkers + : true; + let stackColumns; + + if (showMarkers && config.essential.line_series) { + // exclude the line series from stacked bars (show as line + markers) + stackColumns = graphic_data.columns + .slice(2) + .filter((d) => d !== config.essential.line_series); + } else { + // include ALL data columns in stacked bars (no line/markers) + stackColumns = graphic_data.columns.slice(2); // This includes the line_series as a stacked bar + } + + let legendData = d3.zip(stackColumns, config.essential.colour_palette); + + function drawChart(container, seriesName, data, chartIndex) { + const chartEvery = config.optional.chart_every[size]; + const chartsPerRow = config.optional.chart_every[size]; + let chartPosition = chartIndex % chartsPerRow; + + let margin = { ...config.optional.margin[size] }; + + let chartGap = config.optional?.chartGap || 10; + + let chart_width = calculateChartWidth({ + screenWidth: parseInt(graphic.style("width")), + chartEvery: chartsPerRow, + chartMargin: margin, + chartGap: chartGap, + }); + + // If the chart is not in the first position in the row, reduce the left margin + if (config.optional.dropYAxis) { + if (chartPosition !== 0) { + margin.left = chartGap; + } + } + + const aspectRatio = config.optional.aspectRatio[size]; + + //height is set by the aspect ratio + let height = (aspectRatio[1] / aspectRatio[0]) * chart_width; + + //set up scales + const y = d3.scaleLinear().range([height, 0]); + + const x = d3 + .scaleBand() + .paddingOuter(0.0) + .paddingInner(0.1) + .range([0, chart_width]) + .round(false); + + const colour = d3 + .scaleOrdinal() + .domain(stackColumns) // Use stackColumns instead of all columns + .range(config.essential.colour_palette); + + //use the data to find unique entries in the date column + x.domain([...new Set(graphic_data.map((d) => d.date))]); + + //set up yAxis generator + const yAxis = d3 + .axisLeft(y) + .tickSize(-chart_width) + .tickPadding(10) + .ticks(config.optional.yAxisTicks[size]) + .tickFormat((d) => + config.optional.dropYAxis !== true + ? d3.format(config.essential.yAxisTickFormat)(d) + : chartPosition == 0 + ? d3.format(config.essential.yAxisTickFormat)(d) + : "" + ); + + // Use stackColumns for the stack instead of filtering out line_series + const stack = d3 + .stack() + .keys(stackColumns) + .offset(d3[config.essential.stackOffset]) + .order(d3[config.essential.stackOrder]); + + const series = stack(data); + const seriesAll = stack(graphic_data); + + let xTime = d3.timeFormat(config.essential.xAxisTickFormat[size]); + + //set up xAxis generator + const xAxis = d3 + .axisBottom(x) + .tickSize(10) + .tickPadding(10) + .tickValues( + xDataType == "date" + ? graphic_data + .map(function (d) { + return d.date.getTime(); + }) //just get dates as seconds past unix epoch + .filter(function (d, i, arr) { + return arr.indexOf(d) == i; + }) //find unique + .map(function (d) { + return new Date(d); + }) //map back to dates + .sort(function (a, b) { + return a - b; + }) + .filter(function (d, i) { + return ( + (i % config.optional.xAxisTicksEvery[size] === 0 && + i <= + graphic_data.length - + config.optional.xAxisTicksEvery[size]) || + i == data.length - 1 + ); //Rob's fussy comment about labelling the last date + }) + : x.domain().filter((d, i) => { + return ( + (i % config.optional.xAxisTicksEvery[size] === 0 && + i <= + graphic_data.length - + config.optional.xAxisTicksEvery[size]) || + i == data.length - 1 + ); + }) + ) + .tickFormat((d) => + xDataType == "date" + ? xTime(d) + : d3.format(config.essential.xAxisNumberFormat)(d) + ); + + //create svg for chart + const svg = addSvg({ + svgParent: container, + chart_width: chart_width, + height: height + margin.top + margin.bottom, + margin: margin, + }); + + if (config.essential.yDomain == "auto") { + y.domain(d3.extent(seriesAll.flat(2))); //flatten the arrays and then get the extent + } else { + y.domain(config.essential.yDomain); + } + + //gets array of arrays for individual lines + let lines = []; + for (let column in graphic_data[0]) { + if (column == "date" || column == "series") continue; + lines[column] = data.map(function (d) { + return { + name: d.date, + amt: d[column], + }; + }); + } + + let counter; + // do some code to overwrite blanks with the last known point + let keys = Object.keys(lines); + for (let i = 0; i < keys.length; i++) { + lines[keys[i]].forEach(function (d, j) { + if (d.amt != "null") { + counter = j; + } else { + d.name = lines[keys[i]][counter].name; + d.amt = lines[keys[i]][counter].amt; + } + }); + } + + svg + .append("g") + .attr("transform", "translate(0," + height + ")") + .attr("class", "x axis") + .call(xAxis); + + svg + .append("g") + .attr("class", "y axis numeric") //Can be numeric or categorical + .call(yAxis) + .selectAll("line") + .each(function (d) { + if (d == 0) { + d3.select(this).attr("class", "zero-line"); + } + }) + .selectAll("text") + .call(wrap, margin.left - 10); + + //Columns - draw all stacked bars + svg + .append("g") + .selectAll("g") + .data(series) + .join("g") + .attr("fill", (d, i) => config.essential.colour_palette[i]) + .selectAll("rect") + .data((d) => d) + .join("rect") + .attr("y", (d) => Math.min(y(d[0]), y(d[1]))) + .attr("x", (d) => x(d.data.date)) + .attr("height", (d) => Math.abs(y(d[0]) - y(d[1]))) + .attr("width", x.bandwidth()); + + // Only draw line and markers if showMarkers is true + if (showMarkers && config.essential.line_series) { + let thisCurve = d3.curveLinear; + + let line = d3 + .line() + .defined((d) => d.amt !== "null") + .curve(thisCurve) + .x((d) => x(d.name)) + .y((d) => y(d.amt)); + + let line_values = Object.entries(lines).filter( + (d) => d[0] == config.essential.line_series + ); + + // Draw line if showLine is true + if (config.essential.showLine === true) { + svg + .append("g") + .selectAll("path") + .data(line_values) + .enter() + .append("path") + .attr("stroke", (d, i) => config.essential.line_colour) + .attr("class", "dataLine") + .attr("d", (d) => line(d[1])) + .attr("transform", "translate(" + x.bandwidth() / 2 + ",0)"); + } + + // Draw diamond markers + svg + .append("g") + .selectAll("g") + .data(line_values) + .enter() + .append("g") + .attr("transform", "translate(" + x.bandwidth() / 2 + ",0)") + .selectAll("path") + .data((d) => d[1]) + .enter() + .append("path") + .attr("d", diamondShape(8)) + .attr("transform", (d) => `translate(${x(d.name)}, ${y(d.amt)})`) + .attr("stroke", config.essential.line_colour) + .attr("class", "diamondStyle"); + } + + // This does the chart title label + addChartTitleLabel({ + svgContainer: svg, + yPosition: -30, + text: seriesName, + wrapWidth: chart_width, + }); + + // This does the y-axis label + addAxisLabel({ + svgContainer: svg, + xPosition: 5 - margin.left, + yPosition: -10, + text: chartIndex % chartEvery == 0 ? config.essential.yAxisLabel : "", + textAnchor: "start", + wrapWidth: chart_width, + }); + } + + // Draw the charts for each small multiple + chartContainers.each(function ([key, value], i) { + drawChart(d3.select(this), key, value, i); + }); + + // Clear existing legend before creating new one + d3.select("#legend").selectAll("*").remove(); + + // Set up the legend for stacked bars + let legenditem = d3 + .select("#legend") + .selectAll("div.legend--item") + .data(legendData) + .enter() + .append("div") + .attr("class", "legend--item"); + + // Create SVG for legend icons instead of divs + legenditem + .append("svg") + .attr("class", "legend--icon--svg") + .attr("width", 16) + .attr("height", 16) + .append("circle") + .attr("r", 6) + .attr("cx", 8) + .attr("cy", 8) + .attr("fill", function (d) { + return d[1]; + }); + + legenditem + .append("div") + .append("p") + .attr("class", "legend--text") + .html(function (d) { + return d[0]; + }); + + // Only add line legend if showMarkers is true + if (showMarkers && config.essential.line_series) { + let lineLegendItem = d3 + .select("#legend") + .append("div") + .attr("class", "legend--item line"); + + // Create SVG for line legend icon + let lineSvg = lineLegendItem + .append("svg") + .attr("class", "legend--icon--svg") + .attr("width", 24) + .attr("height", 18); + + // Add horizontal line if showLine is true + if (config.essential.showLine === true) { + lineSvg + .append("line") + .attr("x1", 2) + .attr("x2", 21) + .attr("y1", 8) + .attr("y2", 8) + .attr("class", "dataLine") + .attr("stroke", config.essential.line_colour) + .attr("stroke-width", "3px"); + } + + // Add diamond marker on top + lineSvg + .append("path") + .attr("d", diamondShape(8)) + .attr("transform", "translate(12, 8)") + .attr("stroke", config.essential.line_colour) + .attr("class", "diamondStyle"); + + lineLegendItem + .append("div") + .attr("class", "legend--text") + .text(config.essential.line_series); + } + + if (size !== "sm") { + d3.select("#legend").style( + "grid-template-columns", + `repeat(${config.optional.legendColumns}, 1fr)` + ); + } + + //create link to source + d3.select("#source").text("Source: " + config.essential.sourceText); + + //use pym to calculate chart dimensions + if (pymChild) { + pymChild.sendHeight(); + } } d3.csv(config.essential.graphic_data_url).then((data) => { - //load chart data - graphic_data = data; - - let parseTime = d3.timeParse(config.essential.dateFormat); - - data.forEach((d, i) => { - - //If the date column is has date data store it as dates - if (parseTime(data[i].date) !== null) { - d.date = parseTime(d.date) - } - - }); - - //use pym to create iframed chart dependent on specified variables - pymChild = new pym.Child({ - renderCallback: drawGraphic - }); + //load chart data + graphic_data = data; + + let parseTime = d3.timeParse(config.essential.dateFormat); + + data.forEach((d, i) => { + //If the date column is has date data store it as dates + if (parseTime(data[i].date) !== null) { + d.date = parseTime(d.date); + } + }); + + //use pym to create iframed chart dependent on specified variables + pymChild = new pym.Child({ + renderCallback: drawGraphic, + }); }); diff --git a/column-chart-stacked-with-line/chart.css b/column-chart-stacked-with-line/chart.css index 8f43083..72a890e 100644 --- a/column-chart-stacked-with-line/chart.css +++ b/column-chart-stacked-with-line/chart.css @@ -8,5 +8,10 @@ stroke-linecap: round; stroke-linejoin: round; fill: none; +} -} \ No newline at end of file +.diamondStyle { + stroke-width: 2px; + stroke-linejoin: round; + fill: white; +} diff --git a/column-chart-stacked-with-line/config.js b/column-chart-stacked-with-line/config.js index 9016d91..f3c728b 100644 --- a/column-chart-stacked-with-line/config.js +++ b/column-chart-stacked-with-line/config.js @@ -1,77 +1,80 @@ config = { - "essential": { - "graphic_data_url": "data.csv", - "colour_palette": [ "#206095", "#27A0CC", "#871A5B", "#A8BD3A", "#F66068", "#003C57"], - "line_colour": "#222222", - "sourceText": "Office for National Statistics", - "accessibleSummary": "Here is the screenreader text describing the chart.", - "xAxisTickFormat": { - "sm": "%Y", - "md": "%Y", - "lg": "%b-%y" - }, - "yAxisTickFormat": ".0%", - "xAxisNumberFormat": ".0f", - "dateFormat": "%b-%y", - //the format your date data has in data.csv - "yDomain": "auto", - // either "auto" or an array for the x domain e.g. [0,100] - "line_series": "value1", - "yAxisLabel": "y axis label", - "stackOffset": "stackOffsetDiverging", - // options include - // stackOffsetNone means the baseline is set at zero - // stackOffsetExpand to do 100% charts - // stackOffsetDiverging for data with positive and negative values - "stackOrder": "stackOrderNone" - // other options include - // stackOrderNone means the order is taken from the datafile - // stackOrderAppearance the earliest series (according to the maximum value) is at the bottom - // stackOrderAscending the smallest series (according to the sum of values) is at the bottom - // stackOrderDescending the largest series (according to the sum of values) is at the bottom - // stackOrderReverse reverse the order as set from the data file - }, - "optional": { - "margin": { - "sm": { - "top": 30, - "right": 20, - "bottom": 50, - "left": 70 - }, - "md": { - "top": 30, - "right": 20, - "bottom": 50, - "left": 70 - }, - "lg": { - "top": 30, - "right": 20, - "bottom": 50, - "left": 70 - } - }, - "aspectRatio": { - "sm": [1, 1], - "md": [1, 1], - "lg": [2, 1] - }, - "xAxisTicksEvery": { - "sm": 4, - "md": 4, - "lg": 2 - }, - "yAxisTicks": { - "sm": 4, - "md": 8, - "lg": 10 - }, - "addFirstDate": false, - "addFinalDate": false, - "mobileBreakpoint": 510, - "mediumBreakpoint": 600 - }, - "elements": { "select": 0, "nav": 0, "legend": 0, "titles": 0 }, - "chart_build": {} + essential: { + graphic_data_url: "data.csv", + showMarkers: true, + showLine: true, + //line will only be shown if the markers are also shown + colour_palette: ["#206095", "#A8BD3A", "#871A5B", "#F66068", "#05341A"], + line_colour: "#222222", + sourceText: "Office for National Statistics", + accessibleSummary: "Here is the screenreader text describing the chart.", + xAxisTickFormat: { + sm: "%b %Y", + md: "%b %Y", + lg: "%b %Y", + }, + yAxisTickFormat: ".0%", + xAxisNumberFormat: ".0f", + dateFormat: "%b-%y", + //the format your date data has in data.csv + yDomain: "auto", + // either "auto" or an array for the x domain e.g. [0,100] + line_series: "Total", + yAxisLabel: "y axis label", + stackOffset: "stackOffsetDiverging", + // options include + // stackOffsetNone means the baseline is set at zero + // stackOffsetExpand to do 100% charts + // stackOffsetDiverging for data with positive and negative values + stackOrder: "stackOrderNone", + // other options include + // stackOrderNone means the order is taken from the datafile + // stackOrderAppearance the earliest series (according to the maximum value) is at the bottom + // stackOrderAscending the smallest series (according to the sum of values) is at the bottom + // stackOrderDescending the largest series (according to the sum of values) is at the bottom + // stackOrderReverse reverse the order as set from the data file + }, + optional: { + margin: { + sm: { + top: 30, + right: 5, + bottom: 50, + left: 40, + }, + md: { + top: 30, + right: 20, + bottom: 50, + left: 40, + }, + lg: { + top: 30, + right: 20, + bottom: 50, + left: 40, + }, + }, + aspectRatio: { + sm: [1.33, 1], + md: [1.5, 1], + lg: [1.5, 1], + }, + xAxisTicksEvery: { + sm: 6, + md: 4, + lg: 2, + }, + yAxisTicks: { + sm: 4, + md: 8, + lg: 10, + }, + addFirstDate: false, + addFinalDate: false, + mobileBreakpoint: 510, + mediumBreakpoint: 600, + }, + elements: { select: 0, nav: 0, legend: 0, titles: 0 }, + chart_build: {}, }; diff --git a/column-chart-stacked-with-line/data.csv b/column-chart-stacked-with-line/data.csv index f7a8c48..46a5690 100644 --- a/column-chart-stacked-with-line/data.csv +++ b/column-chart-stacked-with-line/data.csv @@ -1,4 +1,4 @@ -date,value1,value2,value3,value4,value5 +date,Total,Retail,Manufacturing,Transport,Other Jan-20,0.089,0.008,0.033,0.054,-0.073 Feb-20,0.064,-0.06,-0.083,0.084,-0.087 Mar-20,0.007,-0.007,-0.088,0.009,0.011 diff --git a/column-chart-stacked-with-line/script.js b/column-chart-stacked-with-line/script.js index 409c77d..9f592c9 100644 --- a/column-chart-stacked-with-line/script.js +++ b/column-chart-stacked-with-line/script.js @@ -1,274 +1,338 @@ -import { initialise, wrap, addSvg, addAxisLabel, addSource } from "../lib/helpers.js"; - -let graphic = d3.select('#graphic'); -let legend = d3.select('#legend'); +import { + initialise, + wrap, + addSvg, + diamondShape, + addAxisLabel, +} from "../lib/helpers.js"; + +let graphic = d3.select("#graphic"); +let legend = d3.select("#legend"); let pymChild = null; let graphic_data, size, svg; function drawGraphic() { - - //Set up some of the basics and return the size value ('sm', 'md' or 'lg') - size = initialise(size); - - const aspectRatio = config.optional.aspectRatio[size]; - let margin = config.optional.margin[size]; - let chart_width = - parseInt(graphic.style('width')) - margin.left - margin.right; - //height is set by the aspect ratio - let height = - aspectRatio[1] / aspectRatio[0] * chart_width; - - //set up scales - const y = d3.scaleLinear().range([height, 0]); - - const x = d3 - .scaleBand() - .paddingOuter(0.0) - .paddingInner(0.1) - .range([0, chart_width]) - .round(false); - - //use the data to find unique entries in the date column - x.domain([...new Set(graphic_data.map((d) => d.date))]); - - //set up yAxis generator - let yAxis = d3.axisLeft(y) - .tickSize(-chart_width) - .tickPadding(10) - .ticks(config.optional.yAxisTicks[size]) - .tickFormat(d3.format(config.essential.yAxisTickFormat)); - - let xDataType; - - if (Object.prototype.toString.call(graphic_data[0].date) === '[object Date]') { - xDataType = 'date'; - } else { - xDataType = 'numeric'; - } - - // console.log(xDataType) - - let xTime = d3.timeFormat(config.essential.xAxisTickFormat[size]) - - let tickValues = x.domain().filter(function (d, i) { - return !(i % config.optional.xAxisTicksEvery[size]) - }); - - //Labelling the first and/or last bar if needed - if (config.optional.addFirstDate == true) { - tickValues.push(graphic_data[0].date) - console.log("First date added") - } - - if (config.optional.addFinalDate == true) { - tickValues.push(graphic_data[graphic_data.length - 1].date) - console.log("Last date added") - } - - //set up xAxis generator - let xAxis = d3 - .axisBottom(x) - .tickSize(10) - .tickPadding(10) - .tickValues(tickValues) - .tickFormat((d) => xDataType == 'date' ? xTime(d) - : d3.format(config.essential.xAxisNumberFormat)(d)); - - const stack = d3 - .stack() - .keys(graphic_data.columns.slice(1).filter(d => (d) !== config.essential.line_series)) - .offset(d3[config.essential.stackOffset]) - .order(d3[config.essential.stackOrder]); - - let series = stack(graphic_data); - - //gets array of arrays for individual lines - let lines = []; - for (let column in graphic_data[0]) { - if (column == 'date') continue; - lines[column] = graphic_data.map(function (d) { - return { - 'name': d.date, - 'amt': d[column] - }; - }); - } - - - // console.log("linesflat: ", Object.entries(lines).flat(3)) - - let counter; - // do some code to overwrite blanks with the last known point - let keys = Object.keys(lines); - for (let i = 0; i < keys.length; i++) { - // console.log(lines[keys[i]]) - lines[keys[i]].forEach(function (d, j) { - if (d.amt != "null") { - counter = j; - } else { - d.name = lines[keys[i]][counter].name - d.amt = lines[keys[i]][counter].amt - } - - }) - - } - - // console.log("keys: ", keys) - - // Set up the legend - let legenditem = d3 - .select('#legend') - .selectAll('div.legend--item') - .data( - d3.zip(graphic_data.columns.slice(1).filter(d => (d) !== config.essential.line_series), config.essential.colour_palette) - ) - .enter() - .append('div') - .attr('class', 'legend--item'); - - legenditem - .append('div') - .attr('class', 'legend--icon--circle') - .style('background-color', function (d) { - return d[1]; - }); - - legenditem - .append('div') - .append('p') - .attr('class', 'legend--text') - .html(function (d) { - return d[0]; - }); - - d3.select('#legend') - .append('div') - .attr('class', 'legend--item line') - .append('div') - .attr('class', 'legend--icon--refline') - .style('background-color', config.essential.line_colour); - - d3.select('.legend--item.line') - .append('div') - .attr('class', 'legend--text') - .text(config.essential.line_series) - - - //create svg for chart - svg = addSvg({ - svgParent: graphic, - chart_width: chart_width, - height: height + margin.top + margin.bottom, - margin: margin - }) - - if (config.essential.yDomain == 'auto') { - // y.domain([ - // 0, - // d3.max(graphic_data, (d) => d3.max(keys, (c) => d[c]))]) - y.domain(d3.extent(series.flat(2))); - } else { - y.domain(config.essential.yDomain); - } - - svg - .append('g') - .attr('transform', 'translate(0,' + height + ')') - .attr('class', 'x axis') - .call(xAxis); - - svg - .append('g') - .attr('class', 'y axis numeric') - .call(yAxis) - .selectAll('line') - .each(function (d) { - if (d == 0) { - d3.select(this).attr('class', 'zero-line'); - } - }) - .selectAll('text') - .call(wrap, margin.left - 10); - - svg - .append('g') - .selectAll('g') - .data(series) - .join('g') - .attr('fill', (d, i) => config.essential.colour_palette[i]) - .selectAll('rect') - .data((d) => d) - .join('rect') - .attr('y', (d) => Math.min(y(d[0]), y(d[1]))) - .attr('x', (d) => x(d.data.date)) - .attr('height', (d) => Math.abs(y(d[0]) - y(d[1]))) - .attr('width', x.bandwidth()) - // .attr('fill', config.essential.colour_palette[0]); - - - let thisCurve = d3.curveLinear - - let line = d3.line() - .defined((d) => d.amt !== 'null') - .curve(thisCurve) - .x((d) => x(d.name)) - .y((d) => y(d.amt)); - // // //opposite sex - - let line_values = Object.entries(lines).filter(d => d[0] == config.essential.line_series) - - // console.log("lines: ", lines) - // console.log("Object.entries(lines)", Object.entries(lines)) - // console.log("filtered lines: ", Object.entries(lines).filter(d => d[0] == config.essential.line_series)) - - svg.append('g') - .selectAll('path') - .data(line_values) - .enter() - .append('path') - .attr("stroke", (d, i) => config.essential.line_colour) - .attr("class", "dataLine") - .attr('d', (d) => - line(d[1])) - .attr("transform", "translate(" + x.bandwidth() / 2 + ",0)"); - - // This does the y-axis label - addAxisLabel({ - svgContainer: svg, - xPosition: 5 - margin.left, - yPosition: -10, - text: config.essential.yAxisLabel, - textAnchor: "start", - wrapWidth: chart_width - }); - - //create link to source - addSource('source', config.essential.sourceText); - - //use pym to calculate chart dimensions - if (pymChild) { - pymChild.sendHeight(); - } + //Set up some of the basics and return the size value ('sm', 'md' or 'lg') + size = initialise(size); + + const aspectRatio = config.optional.aspectRatio[size]; + let margin = config.optional.margin[size]; + let chart_width = + parseInt(graphic.style("width")) - margin.left - margin.right; + //height is set by the aspect ratio + let height = (aspectRatio[1] / aspectRatio[0]) * chart_width; + + //set up scales + const y = d3.scaleLinear().range([height, 0]); + + const x = d3 + .scaleBand() + .paddingOuter(0.0) + .paddingInner(0.1) + .range([0, chart_width]) + .round(false); + + //use the data to find unique entries in the date column + x.domain([...new Set(graphic_data.map((d) => d.date))]); + + //set up yAxis generator + let yAxis = d3 + .axisLeft(y) + .tickSize(-chart_width) + .tickPadding(10) + .ticks(config.optional.yAxisTicks[size]) + .tickFormat(d3.format(config.essential.yAxisTickFormat)); + + let xDataType; + + if ( + Object.prototype.toString.call(graphic_data[0].date) === "[object Date]" + ) { + xDataType = "date"; + } else { + xDataType = "numeric"; + } + + let xTime = d3.timeFormat(config.essential.xAxisTickFormat[size]); + + let tickValues = x.domain().filter(function (d, i) { + return !(i % config.optional.xAxisTicksEvery[size]); + }); + + //Labelling the first and/or last bar if needed + if (config.optional.addFirstDate == true) { + tickValues.push(graphic_data[0].date); + } + + if (config.optional.addFinalDate == true) { + tickValues.push(graphic_data[graphic_data.length - 1].date); + } + + //set up xAxis generator + let xAxis = d3 + .axisBottom(x) + .tickSize(10) + .tickPadding(10) + .tickValues(tickValues) + .tickFormat((d) => + xDataType == "date" + ? xTime(d) + : d3.format(config.essential.xAxisNumberFormat)(d) + ); + + // Determine which columns to use for stacked bars based on line toggle + let stackColumns; + let showMarkers = + config.essential.showMarkers !== undefined + ? config.essential.showMarkers + : true; // Default to true for backwards compatibility + + if (showMarkers && config.essential.line_series) { + // exclude the line series from stacked bars + stackColumns = graphic_data.columns + .slice(1) + .filter((d) => d !== config.essential.line_series); + } else { + //include all data columns in stacked bars (no line) + stackColumns = graphic_data.columns.slice(1); + } + + const stack = d3 + .stack() + .keys(stackColumns) + .offset(d3[config.essential.stackOffset]) + .order(d3[config.essential.stackOrder]); + + let series = stack(graphic_data); + + //gets array of arrays for individual lines + let lines = []; + for (let column in graphic_data[0]) { + if (column == "date") continue; + lines[column] = graphic_data.map(function (d) { + return { + name: d.date, + amt: d[column], + }; + }); + } + + let counter; + // do some code to overwrite blanks with the last known point + let keys = Object.keys(lines); + for (let i = 0; i < keys.length; i++) { + lines[keys[i]].forEach(function (d, j) { + if (d.amt != "null") { + counter = j; + } else { + d.name = lines[keys[i]][counter].name; + d.amt = lines[keys[i]][counter].amt; + } + }); + } + + // Set up legend based on showMarkers setting + let legendData = d3.zip(stackColumns, config.essential.colour_palette); + + // Set up the legend for stacked bars + let legenditem = d3 + .select("#legend") + .selectAll("div.legend--item") + .data(legendData) + .enter() + .append("div") + .attr("class", "legend--item"); + + // Create SVG for legend icons instead of divs + legenditem + .append("svg") + .attr("class", "legend--icon--svg") + .attr("width", 16) + .attr("height", 16) + .append("circle") + .attr("r", 6) + .attr("cx", 8) + .attr("cy", 8) + .attr("fill", function (d) { + return d[1]; + }); + + legenditem + .append("div") + .append("p") + .attr("class", "legend--text") + .html(function (d) { + return d[0]; + }); + + // Only add line legend if showMarkers is true + if (showMarkers && config.essential.line_series) { + let lineLegendItem = d3 + .select("#legend") + .append("div") + .attr("class", "legend--item line"); + + // Create SVG for line legend icon + let lineSvg = lineLegendItem + .append("svg") + .attr("class", "legend--icon--svg") + .attr("width", 24) + .attr("height", 18); + + // Add horizontal line if showLine is true + if (config.essential.showLine === true) { + lineSvg + .append("line") + .attr("x1", 2) + .attr("x2", 21) + .attr("y1", 8) + .attr("y2", 8) + .attr("class", "dataLine") + .attr("stroke", config.essential.line_colour) + .attr("stroke-width", "3px"); + } + + // Add diamond marker on top + lineSvg + .append("path") + .attr("d", diamondShape(8)) + .attr("transform", "translate(12, 8)") + .attr("stroke", config.essential.line_colour) + .attr("class", "diamondStyle"); + + lineLegendItem + .append("div") + .attr("class", "legend--text") + .text(config.essential.line_series); + } + //create svg for chart + svg = addSvg({ + svgParent: graphic, + chart_width: chart_width, + height: height + margin.top + margin.bottom, + margin: margin, + }); + + if (config.essential.yDomain == "auto") { + y.domain(d3.extent(series.flat(2))); + } else { + y.domain(config.essential.yDomain); + } + + svg + .append("g") + .attr("transform", "translate(0," + height + ")") + .attr("class", "x axis") + .call(xAxis); + + svg + .append("g") + .attr("class", "y axis numeric") + .call(yAxis) + .selectAll("line") + .each(function (d) { + if (d == 0) { + d3.select(this).attr("class", "zero-line"); + } + }) + .selectAll("text") + .call(wrap, margin.left - 10); + + // Draw stacked bars + svg + .append("g") + .selectAll("g") + .data(series) + .join("g") + .attr("fill", (d, i) => config.essential.colour_palette[i]) + .selectAll("rect") + .data((d) => d) + .join("rect") + .attr("y", (d) => Math.min(y(d[0]), y(d[1]))) + .attr("x", (d) => x(d.data.date)) + .attr("height", (d) => Math.abs(y(d[0]) - y(d[1]))) + .attr("width", x.bandwidth()); + + // Only draw line if showMarkers is true - can't draw line without markers + if (showMarkers && config.essential.line_series) { + let thisCurve = d3.curveLinear; + + let line = d3 + .line() + .defined((d) => d.amt !== "null") + .curve(thisCurve) + .x((d) => x(d.name)) + .y((d) => y(d.amt)); + + let line_values = Object.entries(lines).filter( + (d) => d[0] == config.essential.line_series + ); + + if (config.essential.showLine === true) { + // Draw line + svg + .append("g") + .selectAll("path") + .data(line_values) + .enter() + .append("path") + .attr("stroke", (d, i) => config.essential.line_colour) + .attr("class", "dataLine") + .attr("d", (d) => line(d[1])) + .attr("transform", "translate(" + x.bandwidth() / 2 + ",0)"); + } + // Draw diamond markers + svg + .append("g") + .selectAll("g") + .data(line_values) + .enter() + .append("g") + .attr("transform", "translate(" + x.bandwidth() / 2 + ",0)") + .selectAll("path") + .data((d) => d[1]) + .enter() + .append("path") + .attr("d", diamondShape(8)) + .attr("transform", (d) => `translate(${x(d.name)}, ${y(d.amt)})`) + .attr("stroke", config.essential.line_colour) + .attr("class", "diamondStyle"); + } + + // This does the y-axis label + addAxisLabel({ + svgContainer: svg, + xPosition: 5 - margin.left, + yPosition: -10, + text: config.essential.yAxisLabel, + textAnchor: "start", + wrapWidth: chart_width, + }); + + //create link to source + d3.select("#source").text("Source: " + config.essential.sourceText); + + //use pym to calculate chart dimensions + if (pymChild) { + pymChild.sendHeight(); + } } d3.csv(config.essential.graphic_data_url).then((data) => { - //load chart data - graphic_data = data; - - let parseTime = d3.timeParse(config.essential.dateFormat); - - data.forEach((d, i) => { - - //If the date column is has date data store it as dates - if (parseTime(data[i].date) !== null) { - d.date = parseTime(d.date) - } - - }); - - //use pym to create iframed chart dependent on specified variables - pymChild = new pym.Child({ - renderCallback: drawGraphic - }); + //load chart data + graphic_data = data; + + let parseTime = d3.timeParse(config.essential.dateFormat); + + data.forEach((d, i) => { + //If the date column is has date data store it as dates + if (parseTime(data[i].date) !== null) { + d.date = parseTime(d.date); + } + }); + + //use pym to create iframed chart dependent on specified variables + pymChild = new pym.Child({ + renderCallback: drawGraphic, + }); });