diff --git a/README.md b/README.md index 9739f616..1622b2b1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ the team will have a look. | Bar chart stacked | [View](https://onsdigital.github.io/Charts/bar-chart-stacked/) | [``](https://github.com/ONSdigital/Charts/tree/main/bar-chart-stacked/) | [README](https://github.com/ONSvisual/Charts/blob/main/bar-chart-stacked/README.md) | | Bar chart stacked grouped | [View](https://onsdigital.github.io/Charts/bar-chart-stacked-grouped/) | [``](https://github.com/ONSdigital/Charts/tree/main/bar-chart-stacked-grouped/) | [README](https://github.com/ONSvisual/Charts/blob/main/bar-chart-stacked-grouped/README.md) | | Bar chart stacked sm | [View](https://onsdigital.github.io/Charts/bar-chart-stacked-sm/) | [``](https://github.com/ONSdigital/Charts/tree/main/bar-chart-stacked-sm/) | [README](https://github.com/ONSvisual/Charts/blob/main/bar-chart-stacked-sm/README.md) | +| Bar chart with context bubbles | [View](https://onsdigital.github.io/Charts/bar-chart-with-context-bubbles/) | [``](https://github.com/ONSdigital/Charts/tree/main/bar-chart-with-context-bubbles/) | [README](https://github.com/ONSvisual/Charts/blob/main/bar-chart-with-context-bubbles/README.md) | | Bar chart with dropdown | [View](https://onsdigital.github.io/Charts/bar-chart-with-dropdown/) | [``](https://github.com/ONSdigital/Charts/tree/main/bar-chart-with-dropdown/) | [README](https://github.com/ONSvisual/Charts/blob/main/bar-chart-with-dropdown/README.md) | | Beeswarm | [View](https://onsdigital.github.io/Charts/beeswarm/) | [``](https://github.com/ONSdigital/Charts/tree/main/beeswarm/) | [README](https://github.com/ONSvisual/Charts/blob/main/beeswarm/README.md) | | Column chart | [View](https://onsdigital.github.io/Charts/column-chart/) | [``](https://github.com/ONSdigital/Charts/tree/main/column-chart/) | [README](https://github.com/ONSvisual/Charts/blob/main/column-chart/README.md) | diff --git a/bar-chart-with-context-bubbles/README.md b/bar-chart-with-context-bubbles/README.md new file mode 100644 index 00000000..4d347cb4 --- /dev/null +++ b/bar-chart-with-context-bubbles/README.md @@ -0,0 +1 @@ +# Horizontal bar chart \ No newline at end of file diff --git a/bar-chart-with-context-bubbles/chart.css b/bar-chart-with-context-bubbles/chart.css new file mode 100644 index 00000000..5f9f52b3 --- /dev/null +++ b/bar-chart-with-context-bubbles/chart.css @@ -0,0 +1,9 @@ +.dataLabels{ + font-weight: 600; + font-size: 14px; +} + +.bubbleLabels{ + font-weight: 600; + font-size: 14px; +} diff --git a/bar-chart-with-context-bubbles/config.js b/bar-chart-with-context-bubbles/config.js new file mode 100644 index 00000000..9f022996 --- /dev/null +++ b/bar-chart-with-context-bubbles/config.js @@ -0,0 +1,52 @@ +config = { + "graphicDataURL": "data.csv", + "colourPalettePositive": ONScolours.emeraldGreen, + "colourPaletteNegative": ONScolours.coralPink, + "colourPaletteBubble": ONScolours.skyBlue, + "sourceText": "Office for National Statistics", + "accessibleSummary": "This chart has been hidden from screen readers. The main message of the chart is summarised in the chart title.", + "dataLabels": { + "show": true, + "numberFormat": ".1%" + }, + "dataLabelsBubbleFormat": ".0%", + "xDomain": "auto", + // either "auto" or an array for the x domain e.g. [0,100] + "xAxisLabel": "x axis label", + "xAxisTitle": "x axis title", + "bubbleLabel": "Context bubble label", + "margin": { + "sm": { + "top": 50, + "bubble": 100, + "bottom": 50, + "left": 150, + "right": 10 + }, + "md": { + "top": 50, + "bubble": 100, + "bottom": 50, + "left": 180, + "right": 10 + }, + "lg": { + "top": 50, + "bubble": 100, + "bottom": 50, + "left": 200, + "right": 10 + } + }, + "seriesHeight": { + "sm": 30, + "md": 30, + "lg": 30 + }, + "xAxisTicks": { + "sm": 4, + "md": 8, + "lg": 10 + }, + "elements": { "select": 0, "nav": 0, "legend": 0, "titles": 0 } +}; diff --git a/bar-chart-with-context-bubbles/data.csv b/bar-chart-with-context-bubbles/data.csv new file mode 100644 index 00000000..5a223be2 --- /dev/null +++ b/bar-chart-with-context-bubbles/data.csv @@ -0,0 +1,22 @@ +name,value,value1 +Total,0.022512891, +Information technology (IT),0.025372118,0.069091985 +Professional and technical,0.014008212,0.086069359 +Manufacturing,0.010507311,0.110469217 +Construction,0.006866247,0.068666705 +Administrative services,0.006428236,0.056393075 +Transport and storage,0.003780288,0.044110711 +"Agriculture, forestry and fishing",0.001529134,0.007096016 +Water supply,0.000345815,0.013388421 +Public administration and defence,0.000186062,0.053101109 +Education,-0.000206951,0.063176273 +Recreation and culture,-0.001420556,0.016436356 +Other services,-0.001540501,0.021190873 +Mining and quarrying,-0.002087217,0.011156108 +Hotels and catering,-0.002794911,0.032992818 +Wholesale and retail,-0.003061794,0.11328131 +Real estate,-0.005436802,0.043964401 +Energy,-0.008522412,0.016158477 +Health and social care,-0.009248773,0.083735503 +Finance and insurance,-0.012151287,0.089521284 +Between-industry reallocation,-0.000039328, \ No newline at end of file diff --git a/bar-chart-with-context-bubbles/fallback.png b/bar-chart-with-context-bubbles/fallback.png new file mode 100644 index 00000000..26c8f650 Binary files /dev/null and b/bar-chart-with-context-bubbles/fallback.png differ diff --git a/bar-chart-with-context-bubbles/index.html b/bar-chart-with-context-bubbles/index.html new file mode 100644 index 00000000..f3d1413c --- /dev/null +++ b/bar-chart-with-context-bubbles/index.html @@ -0,0 +1,41 @@ + + + + + + + Horizontal Bar Chart Template + + + + + + + + + + + + + +
+ + + + + + +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/bar-chart-with-context-bubbles/script.js b/bar-chart-with-context-bubbles/script.js new file mode 100644 index 00000000..20d1e338 --- /dev/null +++ b/bar-chart-with-context-bubbles/script.js @@ -0,0 +1,188 @@ +import { initialise, wrap, addSvg, addDataLabels, addSource, addAxisLabel } from "../lib/helpers.js"; + +let graphic = d3.select("#graphic"); +let pymChild = null; +let graphicData, size, svg; + + +function drawGraphic() { + // Set up some of the basics and return the size value ('sm', 'md' or 'lg') + // To override breakpoints, pass as: initialise(size, { mediumBreakpoint: 500 }) + size = initialise(size); + + let margin = config.margin[size]; + + let chartWidth = + parseInt(graphic.style("width")) - margin.left - margin.bubble - 20; + + let height = + config.seriesHeight[size] * graphicData.length + + 10 * (graphicData.length - 1) + + 12; + + //set up scales + const x = d3.scaleLinear().range([0, chartWidth]); + + const y = d3 + .scaleBand() + .paddingOuter(0.2) + .paddingInner(((graphicData.length - 1) * 10) / (graphicData.length * 30)) + .range([0, height]) + .round(true); + + //use the data to find unique entries in the name column + y.domain([...new Set(graphicData.map((d) => d.name))]); + + //set up yAxis generator + let yAxis = d3.axisLeft(y).tickSize(0).tickPadding(10); + + //set up xAxis generator + let xAxis = d3 + .axisBottom(x) + .tickSize(-height) + .tickFormat(d3.format(config.dataLabels.numberFormat)) + .ticks(config.xAxisTicks[size]); + + //create svg for chart + svg = addSvg({ + svgParent: graphic, + chartWidth: chartWidth, + height: height + margin.top + margin.bottom, + margin: { ...margin, right: margin.bubble + margin.right } + }) + + if (config.xDomain == "auto") { + if (d3.min(graphicData.map(({ value }) => Number(value))) >= 0) { + x.domain([0, d3.max(graphicData.map(({ value }) => Number(value)))]); //modified so it converts string to number + } else { + x.domain(d3.extent(graphicData.map(({ value }) => Number(value)))); + } + } else { + x.domain(config.xDomain); + } + + svg + .append("g") + .attr("transform", "translate(0," + height + ")") + .attr("class", "x axis") + .call(xAxis) + .selectAll("line") + .each(function (d) { + if (d == 0) { + d3.select(this).attr("class", "zero-line"); + } + }); + + svg + .append("g") + .attr("class", "y axis") + .call(yAxis) + .selectAll("text") + .call(wrap, margin.left - 20); + + svg + .selectAll("rect") + .data(graphicData) + .join("rect") + .attr("x", (d) => (d.value < 0 ? x(d.value) : x(0))) + .attr("y", (d) => y(d.name)) + .attr("width", (d) => Math.abs(x(d.value) - x(0))) + .attr("height", y.bandwidth()) + .attr("fill", (d) => d.value >= 0 ? config.colourPalettePositive : config.colourPaletteNegative); + + // Add context bubbles + const maxRadius = margin.bubble / 1.85; + const maxValueBubble = d3.max(graphicData, d => Math.abs(d.value1)); + + function calculateRadius(value) { + const areaScaleFactor = Math.pow(maxRadius, 2); + return Math.sqrt((Math.abs(value) / maxValueBubble) * areaScaleFactor / Math.PI); + } + + svg + .selectAll("circle") + .data(graphicData) + .join("circle") + .attr("cx", chartWidth + margin.bubble / 2) + .attr("cy", (d) => y(d.name) + y.bandwidth() / 2) + .attr("r", d => calculateRadius(d.value1)) + .attr("fill", config.colourPaletteBubble) + .attr("fill-opacity", 0.2) + .attr("stroke", config.colourPaletteBubble) + .attr("stroke-opacity", 0.4); + + svg + .selectAll("text.bubbleLabels") + .data(graphicData) + .join("text") + .attr("class", "bubbleLabels") + .attr("x", chartWidth + margin.bubble / 2) + .attr("dx", 0) + .attr("y", (d) => y(d.name) + y.bandwidth() / 2) + .attr("dominant-baseline", "middle") + .attr("text-anchor", "middle") + .attr("fill", "#414042") + .text((d) => { + if (!d.value1 || d.value1 === "" || d.value1 === "0%" || d.value1 === 0) return ""; + return isNaN(d.value1) ? "Not applicable" : d3.format(config.dataLabelsBubbleFormat)(d.value1); + }); + + + + if (config.dataLabels.show == true) { + addDataLabels({ + svgContainer: svg, + data: graphicData, + chartWidth: chartWidth, + labelPositionFactor: 7, + xScaleFunction: x, + yScaleFunction: y + }) + } //end if for datalabels + + //This does the x-axis title at the top for bars + addAxisLabel({ + svgContainer: svg, + xPosition: 0, + yPosition: -10, + text: config.xAxisTitle, + textAnchor: 'start' + }) + + //This does the x-axis label below the x-axis + addAxisLabel({ + svgContainer: svg, + xPosition: chartWidth, + yPosition: height + 35, + text: config.xAxisTitle, + textAnchor: 'end' + }) + + //This does the bubble label at the top + addAxisLabel({ + svgContainer: svg, + xPosition: chartWidth + margin.bubble / 2, + yPosition: -10, + text: config.bubbleLabel, + textAnchor: 'middle', + wrapWidth: margin.bubble + }) + + //create link to source + addSource('source', config.sourceText); + + //use pym to calculate chart dimensions + if (pymChild) { + pymChild.sendHeight(); + } +} + +d3.csv(config.graphicDataURL).then((data) => { + //load chart data + graphicData = data; + + //use pym to create iframed chart dependent on specified variables + pymChild = new pym.Child({ + renderCallback: drawGraphic, + }); +}); diff --git a/chart-menu/chartConfig.js b/chart-menu/chartConfig.js index 8e78d0ff..5751691e 100644 --- a/chart-menu/chartConfig.js +++ b/chart-menu/chartConfig.js @@ -54,6 +54,17 @@ const chartConfig = [ { name: "data.csv", path: "data.csv" } ] }, + { + name: "Bar chart with context bubbles", + url: "https://onsdigital.github.io/Charts/bar-chart-with-context-bubbles/", + tags: { + comparison: true, + "change-over-time": true + }, + dataFiles: [ + { name: "data.csv", path: "data.csv" } + ] + }, { name: "Beeswarm", url: "https://onsdigital.github.io/Charts/beeswarm/",