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/",