diff --git a/app/css/style.css b/app/css/style.css
new file mode 100644
index 0000000..254e300
--- /dev/null
+++ b/app/css/style.css
@@ -0,0 +1,135 @@
+#panels.div-panels {
+ height: 100%;
+ width: 500px;
+}
+
+#selectors.div-selectors {
+ margin-bottom: 13px;
+}
+
+body {
+ font-family: 'Fira Sans', sans-serif;
+ font-size: 13px;
+ fill: #2c2825;
+}
+
+.selector {
+ font-family: 'Fira Sans', sans-serif;
+ font-size: 13px;
+ fill: #2c2825;
+}
+
+.title {
+ font-family: 'Fira Sans', sans-serif;
+ font-size: 16px;
+ fill: #2c2825;
+ font-weight: bold;
+}
+
+.bar rect {
+ shape-rendering: crispEdges;
+}
+
+.bar text {
+ fill: #2c2825;
+}
+
+.axis path,
+.axis line {
+ fill: none;
+ stroke: #686f73;
+ shape-rendering: crispEdges;
+}
+
+.axis text {
+ font-family: 'Fira Sans', sans-serif;
+ font-size: 13px;
+ fill: #686f73;
+}
+
+svg.amb,
+svg.walkin,
+svg.daycase_n,
+svg.emer_n,
+svg.emer_bds,
+svg.ordelec_n,
+svg.ordelec_bds,
+svg.surg_first,
+svg.surg_fup,
+svg.surg_proc,
+svg.non-surg_first,
+svg.non-surg_fup,
+svg.non-surg_proc {
+ background-color: #f2f2f2;
+ border: 1px solid #686f73;
+ margin: 2px;
+}
+
+div.tooltip {
+ position: absolute;
+ text-align: center;
+ vertical-align: middle;
+ line-height: normal;
+ width: 40px;
+ height: 20px;
+ padding: 10px;
+ font-family: 'Fira Sans', sans-serif;
+ font-size: 13px;
+ background: #686f73;
+ border: 0px;
+ border-radius: 8px;
+ pointer-events: none;
+ z-index: 1001;
+ color: #f2f2f2;
+}
+
+/* health status checkbox */
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 40px;
+ height: 20px;
+}
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.toggle {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #c2c5c7;
+ -webkit-transition: 0.4s;
+ transition: 0.4s;
+ border-radius: 10px;
+}
+
+.toggle:before {
+ position: absolute;
+ content: '';
+ height: 16px;
+ width: 18px;
+ left: 2px;
+ bottom: 2px;
+ top: 2px;
+ background-color: #ffffff;
+ -webkit-transition: 0.4s;
+ transition: 0.4s;
+ border-radius: 50%;
+}
+
+input:checked + .toggle {
+ background-color: #686f73;
+}
+
+input:checked + .toggle:before {
+ -webkit-transform: translateX(18px);
+ -ms-transform: translateX(18px);
+ transform: translateX(18px);
+}
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..e00bfd2
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose an area
+
+
+
Choose a service type
+
+
+
Choose a model horizon
+
+
+
+
Choose a projection variant
+
+
Show modelled change without adjustment for health-status
+
+
+
+
+
+
+
+
diff --git a/app/js/index.js b/app/js/index.js
new file mode 100644
index 0000000..7a5adf7
--- /dev/null
+++ b/app/js/index.js
@@ -0,0 +1,75 @@
+/* import variables */
+import { plotTitleSpacer, margin, width, height, transDur, blobDir, filePrefix, fileExt } from './main.js'
+/* import d3 functions */
+import { d3 } from './main.js'
+import { xScale, colorPal, tooltip } from './main.js'
+/* import udf functions */
+import { mouseOver, mouseOut } from './main.js'
+import { plotHsaGrps } from './main.js'
+import { updatePlots } from './main.js'
+import { switchPod } from './main.js'
+import { switchArea } from './main.js'
+import { toggleHsa } from './main.js'
+
+const selectedArea = 'E08000026'
+const selectedProjVar = '1'
+const selectedHorizon = '2025'
+const selectedPod = 'aae'
+
+const data = await d3.csv(blobDir + filePrefix + selectedArea + fileExt)
+
+const grpDat = d3.group(
+ data,
+ (d) => d.proj_id,
+ (d) => d.end_year,
+ (d) => d.pod
+)
+
+const projDat = grpDat
+ .get(selectedProjVar)
+ .get(selectedHorizon)
+ .get(selectedPod)
+
+plotHsaGrps(projDat)
+
+/* hack */
+d3.selectAll('.circle').style('opacity', 0)
+
+/* when the area dropdown changes, run switchArea() with the new value */
+d3.select('#selectArea').on('change', function () {
+ let selectedArea = d3.select(this).property('value')
+ let selectedProjVar = d3.select('#selectProjVar').property('value')
+ let selectedHorizon = d3.select('#selectHorizon').property('value')
+ let selectedPod = d3.select('#selectPod').property('value')
+ switchArea(selectedArea, selectedProjVar, selectedHorizon, selectedPod)
+})
+
+/* when the pod dropdown changes, run switchPod() with the new value */
+d3.select('#selectPod').on('change', function () {
+ let selectedPod = d3.select(this).property('value')
+ let selectedProjVar = d3.select('#selectProjVar').property('value')
+ let selectedHorizon = d3.select('#selectHorizon').property('value')
+ switchPod(data, selectedProjVar, selectedHorizon, selectedPod)
+})
+
+/* when the model horizon dropdown changes, run updatePlots() with the new value */
+d3.select('#selectHorizon').on('change', function () {
+ let selectedHorizon = d3.select(this).property('value')
+ let selectedProjVar = d3.select('#selectProjVar').property('value')
+ let selectedPod = d3.select('#selectPod').property('value')
+ updatePlots(grpDat, selectedProjVar, selectedHorizon, selectedPod)
+})
+
+/* when the projection variant dropdown changes, run updatePlots() with the new value */
+d3.select('#selectProjVar').on('change', function () {
+ let selectedProjVar = d3.select(this).property('value')
+ let selectedHorizon = d3.select('#selectHorizon').property('value')
+ let selectedPod = d3.select('#selectPod').property('value')
+ updatePlots(grpDat, selectedProjVar, selectedHorizon, selectedPod)
+})
+
+/* when the health status toggle changes show/hide circles */
+d3.select('#toggleHsa').on('change', function () {
+ const opacity = this.checked ? 1 : 0
+ toggleHsa(opacity)
+})
diff --git a/app/js/main.js b/app/js/main.js
new file mode 100644
index 0000000..2464601
--- /dev/null
+++ b/app/js/main.js
@@ -0,0 +1,332 @@
+/* load D3 v7 from jsDeliver */
+import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm'
+
+/* d3.histogram() function computes the binning and returns the coordinates of
+each bar. Bars can then be drawn using a classic .append("rect") */
+
+/* set dimensions and margins for small multiple plots */
+const plotTitleSpacer = 20
+const margin = { top: 20 + plotTitleSpacer, left: 20, right: 20, bottom: 10 }
+const width = 500 - margin.left - margin.right
+const height = 200 - margin.top - margin.bottom
+
+/* set file path */
+const blobDir =
+ 'https://strategyunit.blob.core.windows.net/population-aging-app/'
+const filePrefix = 'test_results_'
+const fileExt = '.csv'
+
+/* set transition duration for updatePlots fn */
+const transDur = 1800
+
+/* define axes - a 1D histogram only requires an x-axis */
+const xScale = d3.scaleLinear().range([margin.left, width])
+
+/* define a color palette (histogram bins) */
+/* color scale maps to counts of values in bins; domain([min, median, max]) */
+const colorPal = d3
+ .scaleDiverging((d) => d3.interpolateViridis(1 - d))
+ .domain([0, 40, 80])
+
+/* define a tooltip */
+const tooltip = d3
+ .select('body')
+ .append('div')
+ .attr('class', 'tooltip')
+ .attr('id', 'tooltip-hsa')
+ .style('opacity', 0)
+
+/* fn: mouseover for tooltip */
+function mouseOver(event, d) {
+ if (d3.select(this.parentNode).style('opacity') != 0) {
+ const svg = d3.select(this.parentNode.parentNode)
+ const [x, y] = d3.pointer(event, svg)
+ const text = d3.select('#tooltip-hsa')
+ text.text(d3.format(',.2f')(d.x) + '%')
+ tooltip
+ .style('left', x + 'px')
+ .style('top', y + 'px')
+ .transition()
+ .duration(1000)
+ .style('opacity', 1)
+ d3.select(this).transition().duration(100).attr('r', 8)
+ }
+}
+
+/* fn: mouseout for tooltip */
+function mouseOut() {
+ d3.select(this).transition().duration(1000).attr('r', 4)
+ tooltip.transition().delay(1000).style('opacity', 0)
+}
+
+/* fn: plot panel of histograms for hsagrps in a pod */
+function plotHsaGrps(projDat) {
+ /* data to plot */
+ let hsaGrps = Array.from(
+ d3.group(projDat, (d) => d.hsagrp),
+ ([key, value]) => ({ key, value })
+ )
+
+ /* create a separate svg object for each hsagrp and
+ use hsagrp name to set the class of each svg */
+ d3.select('.div-panels')
+ .selectAll('svg')
+ .data(hsaGrps)
+ .enter()
+ .append('svg')
+ .attr('class', function (d) {
+ return d.value[0].hsagrp
+ })
+ .attr('width', width + margin.left + margin.right)
+ .attr('height', height + margin.top + margin.bottom)
+
+ /* iterate over data array and create plot elements (x-axis, bars, and circles) */
+ hsaGrps.forEach(function (d) {
+ /* set xScale domain based on range of end_p values in each hsagrp */
+ let maxEndp = d3.max(d.value.map((d) => Number(d.end_p)))
+ xScale.domain([0, maxEndp]).nice(5)
+
+ /* define x-axis for each hsagrp */
+ let xAxis = d3.axisBottom().scale(xScale)
+
+ /* attempt to find optimal number of bins for each hsagrp */
+ let binDat = d3
+ .bin()
+ .thresholds(30)
+ .value((d) => Number(d.end_p))(d.value)
+
+ /* obtain coordinates for plotting circles */
+ let cx = d.value.map((d) => Number(d.end_p_nohsa))[0]
+ let cy = height / 1.66 / 2
+ let circleDat = [{ x: cx, y: cy }]
+
+ /* select the correct svg for each hsagrp */
+ let svg = d3.select('svg.' + d.value[0].hsagrp)
+
+ /* set bars group */
+ let bars = svg
+ .selectAll('rect')
+ .data(binDat)
+ .enter()
+ .append('g')
+ .attr('class', 'bar')
+ .attr('id', 'end-p-bar')
+
+ /* set circles group */
+ let circles = svg
+ .selectAll('circle')
+ .data(circleDat)
+ .enter()
+ .append('g')
+ .attr('class', 'circle')
+ .attr('id', 'nohsa-circle')
+
+ /* draw bars */
+ bars
+ .append('rect')
+ .attr('transform', function (d) {
+ return 'translate(' + xScale(d.x1) + ',' + margin.top + ')'
+ })
+ .attr('x', 0)
+ .attr('width', function (d) {
+ return xScale(d.x1) - xScale(d.x0)
+ })
+ .attr('height', height / 1.66)
+ .style('fill', function (d) {
+ return colorPal(d.length)
+ })
+
+ /* draw circles */
+ circles
+ .append('circle')
+ .attr('transform', 'translate(' + 0 + ',' + margin.top + ')')
+ .attr('cx', function (d) {
+ return xScale(d.x)
+ })
+ .attr('cy', function (d) {
+ return d.y
+ })
+ .attr('r', 4)
+ .style('fill', '#fd484e')
+ .on('mouseover', mouseOver)
+ .on('mouseout', mouseOut)
+
+ /* add x-axis */
+ svg
+ .append('g')
+ .attr('class', 'axis')
+ .attr('id', 'x-axis')
+ .attr('transform', `translate(${0}, ${margin.top + height / 1.66})`)
+ .call(xAxis)
+
+ /* add title */
+ svg
+ .append('text')
+ .attr('class', 'title')
+ .attr('id', 'hsagrp-title')
+ .attr('transform', `translate(${margin.left}, ${plotTitleSpacer})`)
+ .text(d.value[0].hsagrp_lab)
+ .style('text-anchor', 'left')
+
+ /* add x-axis label */
+ svg
+ .append('text')
+ .attr('class', 'axis-label')
+ .attr('transform', `translate(${margin.left}, ${margin.top})`)
+ .text('Per cent change')
+ .style('text-anchor', 'left')
+ })
+}
+
+/* fn: update the plots when the data (area), projection variant or model horizon changes */
+function updatePlots(grpDat, selectedProjVar, selectedHorizon, selectedPod) {
+ /* get the new data */
+ let projDat = grpDat
+ .get(selectedProjVar)
+ .get(selectedHorizon)
+ .get(selectedPod)
+
+ /* data to plot */
+ let hsaGrps = Array.from(
+ d3.group(projDat, (d) => d.hsagrp),
+ ([key, value]) => ({ key, value })
+ )
+
+ /* iterate over data array and create plot elements (x-axis, bars, and circles) */
+ hsaGrps.forEach(function (d) {
+ /* set xScale domain based on range of end_p values in each hsagrp */
+ let maxEndp = d3.max(d.value.map((d) => Number(d.end_p)))
+ xScale.domain([0, maxEndp]).nice(5)
+
+ /* define x-axis for each hsagrp */
+ let xAxis = d3.axisBottom().scale(xScale)
+
+ /* attempt to find optimal number of bins for each hsagrp */
+ let binDat = d3
+ .bin()
+ .thresholds(30)
+ .value((d) => Number(d.end_p))(d.value)
+
+ /* select the correct svg for each hsagrp */
+ let svg = d3.select('svg.' + d.value[0].hsagrp)
+
+ /* transition bars */
+ svg
+ .selectAll('rect')
+ .data(binDat)
+ .join('rect')
+ .transition()
+ .duration(transDur)
+ .attr('transform', function (d) {
+ return 'translate(' + xScale(d.x1) + ',' + margin.top + ')'
+ })
+ .attr('x', 0)
+ .attr('width', function (d) {
+ return xScale(d.x1) - xScale(d.x0)
+ })
+ .attr('height', height / 1.66)
+ .style('fill', function (d) {
+ return colorPal(d.length)
+ })
+
+ svg.selectAll('#end-p-bar').exit().remove()
+
+ /* transition circles */
+ let cx = d.value.map((d) => Number(d.end_p_nohsa))[0]
+ let cy = height / 1.66 / 2
+ let circleDat = [{ x: cx, y: cy }]
+
+ svg
+ .selectAll('circle')
+ .data(circleDat)
+ .join('circle')
+ .transition()
+ .duration(transDur)
+ .attr('transform', 'translate(' + 0 + ',' + margin.top + ')')
+ .attr('cx', function (d) {
+ return xScale(d.x)
+ })
+ .attr('cy', function (d) {
+ return d.y
+ })
+ .attr('r', 4)
+ .style('fill', '#fd484e')
+
+ /* hack to make sure circles appear on top of bars */
+ svg.selectAll('.circle').raise()
+
+ /* transition x-axis */
+ svg.selectAll('#x-axis').transition().duration(transDur).call(xAxis)
+ })
+}
+
+/* fn: switch the plots when pod changes */
+function switchPod(data, selectedProjVar, selectedHorizon, selectedPod) {
+ /* group the data */
+ let grpDat = d3.group(
+ data,
+ (d) => d.proj_id,
+ (d) => d.end_year,
+ (d) => d.pod
+ )
+ /* get the new data */
+ let projDat = grpDat
+ .get(selectedProjVar)
+ .get(selectedHorizon)
+ .get(selectedPod)
+ /* remove plots */
+ d3.select('#panels').selectAll('svg').remove()
+ /* reset hsa toggle */
+ d3.select('#toggleHsa').property('checked', false)
+ plotHsaGrps(projDat)
+ /* hack */
+ d3.selectAll('.circle').style('opacity', 0)
+}
+
+/* fn: update the plots when the area dropdown changes */
+function switchArea(
+ selectedArea,
+ selectedProjVar,
+ selectedHorizon,
+ selectedPod
+) {
+ /* get name of csv file from area dropdown */
+ let url = blobDir + filePrefix + selectedArea + fileExt
+ d3.csv(url).then(function (data) {
+ /* group the new data */
+ let grpDat = d3.group(
+ data,
+ (d) => d.proj_id,
+ (d) => d.end_year,
+ (d) => d.pod
+ )
+ updatePlots(grpDat, selectedProjVar, selectedHorizon, selectedPod)
+ })
+}
+
+/* fn: update the plots when the health status toggle changes */
+function toggleHsa(opacity) {
+ d3.selectAll('.circle').style('opacity', opacity)
+}
+
+/* export variables */
+export {
+ plotTitleSpacer,
+ margin,
+ width,
+ height,
+ transDur,
+ blobDir,
+ filePrefix,
+ fileExt
+}
+/* export d3 functions */
+export { d3 }
+export { xScale, colorPal, tooltip }
+/* export udf functions */
+export { mouseOver, mouseOut }
+export { plotHsaGrps }
+export { updatePlots }
+export { switchPod }
+export { switchArea }
+export { toggleHsa }