Skip to content

Instantly share code, notes, and snippets.

@widged
Forked from wpoely86/README.md
Last active July 20, 2018 00:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save widged/a58fdeb7c2357c16f4071dcc3889ea84 to your computer and use it in GitHub Desktop.
Save widged/a58fdeb7c2357c16f4071dcc3889ea84 to your computer and use it in GitHub Desktop.
Diverging Stacked Bar Chart
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Diverging Stacked Bar Chart with D3.js</title>
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis.zero line {
stroke: #808080;
stroke-width: 1;
}
.legendbox {
height: 16px;
line-height: 16px;
}
.legendbox .legend {
position: relative;
display: inline-block;
padding-right: 16px;
}
.legendbox .legend .rect {
display: inline-block;
width: 32px;
height: 16px;
margin: 0; padding: 0;
}
.legendbox .legend .label {
display: inline-block;
transform: translateY(-20%);
font: "10px sans-serif";
margin-left: 8px;
position: relative;
}
</style>
<script src="https://d3js.org/d3.v5.min.js"></script>
<body>
<div id="figure" style="margin-bottom: 50px;"></div>
<script>
const svgChart = (props, data) => {
const { width, height, margin, id, selector } = props;
var svg = d3
.select(selector)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", id)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
return {
svg,
width: width - margin.left - margin.right,
height: height - margin.top - margin.bottom
};
};
const decimalRounder = n => {
const rounder = Math.pow(10, n);
return d => {
return Math.round(d * rounder) / rounder;
};
};
const keyConfig = [
{ csv: "1", g: "-", color: "#c7001e", label: "Strongly disagree" },
{ csv: "2", g: "-", color: "#f6a580", label: "Disagree" },
{ csv: "3", g: "n", color: "#cccccc", label: "Neither agree nor disagree" },
{ csv: "4", g: "+", color: "#92c6db", label: "Agree" },
{ csv: "5", g: "+", color: "#086fad", label: "Strongly agree" }
];
const ratioOfLikert = (keys, getN, data) => {
const ratioRounder = decimalRounder(3);
const cumulRounder = decimalRounder(5);
return data.map(d => {
return keys.reduce(
(acc, k, i, arr) => {
const { obs, ratio, N } = acc;
const raw = parseInt(d[k], 10);
obs[i] = raw;
ratio[i] = ratioRounder(raw / N);
return acc;
},
{ obs: [], ratio: [], N: getN(d), label: d.Question }
);
});
};
const addDataToGroup = rawsAndRatios => {
return group => {
const { key, series } = group;
const items = rawsAndRatios.map((d, i) => {
let x0 = 0,
x1;
return series.map(g => {
const { idx } = g;
const obs = d.obs[idx],
ratio = d.ratio[idx];
x1 = x0 + ratio;
const r = { obs, ratio, x0, x1 };
x0 = x1;
return r;
});
});
const maxes = items.map((d, i) => {
return d[d.length - 1].x1;
});
return { group: key, series, data: items, maxes };
};
};
const plotHorizontalHtmlLegend = props => {
const { parentNode, data, className } = props;
const g = d3
.select(parentNode)
.append("div")
.attr("class", className);
var item = g
.selectAll(".legend")
.data(data)
.enter()
.append("div")
.attr("class", "legend");
item
.append("div")
.attr("class", "rect")
.style("background-color", d => {
return d.color;
});
item
.append("div")
.attr("class", "label")
.text(d => {
return d.label;
});
// d3.selectAll(".legendbox").attr("transform", "translate(" + movesize + ",0)");
};
const plotYAxis = props => {
const { svg, className, yScale } = props;
svg
.append("g")
.attr("class", className)
.call(d3.axisLeft(yScale));
};
const plotZeroLine = props => {
const { svg, className, x, ys } = props;
const [y1, y2] = ys;
svg
.append("g")
.attr("class", className)
.append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", y1)
.attr("y2", y2);
};
const serieExtent = data => {
const xMin = d3.min(data, d => {
return d[0].x0;
});
const xMax = d3.max(data, d => {
return d[d.length - 1].x1;
});
return [xMin, xMax];
};
const plotGroup = (data, svg, config) => {
const {
getQuestionTransform,
getQuestionAxis,
getBarX,
getBarWidth,
getBarHeight,
getBarText,
getBarColor
} = config;
svg
.append("g")
.attr("class", "x axis")
.call(
getQuestionAxis().tickFormat(d => {
return d3.format(".0%")(Math.abs(d));
})
);
const g = svg
.selectAll(".question")
.data(data)
.enter()
.append("g")
.attr("class", "question")
.attr("transform", getQuestionTransform);
var bars = g
.selectAll("rect")
.data((d, i) => {
return d;
})
.enter()
.append("g")
.attr("class", "subbar");
bars
.append("rect")
.attr("height", getBarHeight)
.attr("x", getBarX)
.attr("width", getBarWidth)
.style("fill", getBarColor);
bars
.append("text")
.attr("x", getBarX)
.attr("y", () => {
return getBarHeight() / 2;
})
.attr("dy", "0.5em")
.attr("dx", "0.5em")
.style("text-anchor", "begin")
.text(getBarText);
};
const plotRest = data => {
rows
.insert("rect", ":first-child")
.attr("height", bandwidth)
.attr("x", "1")
.attr("width", width)
.attr("fill-opacity", "0.5")
.style("fill", "#F5F5F5")
.attr("class", function(d, index) {
return index % 2 == 0 ? "even" : "uneven";
});
};
const computeLayout = props => {
const {
alignRight,
xScale,
yScale,
maxes,
data,
series,
questionLabels
} = props;
const getY = i => {
return yScale(questionLabels[i]);
};
const getBarHeight = () => {
return yScale.bandwidth();
};
const colorScale = d3.scaleOrdinal().range(
series.map(d => {
return d.color;
})
);
const getQuestionX = (d, i) => {
return;
};
const getQuestionTransform = (d, i) => {
const x = alignRight ? xScale(-maxes[i]) : 0;
const y = getY(i);
return `translate(${x},${y} )`;
};
const getQuestionAxis = (d, i) => {
const [d0, d1] = xScale.domain();
const [r0, r1] = xScale.range();
const domain = alignRight ? [-d1, -d0] : [d0, d1];
const range = alignRight ? [-r1, -r0] : [r0, r1];
const scale = d3
.scaleLinear()
.domain(domain)
.rangeRound(range)
.nice();
return d3.axisTop(scale).tickValues(
d3.range(0, d3.max(maxes) + 0.05, 0.1).map(d => {
return alignRight ? -d : d;
})
);
};
const getBarX = (d, i) => {
return xScale(d.x0);
};
const getBarWidth = d => {
return Math.abs(xScale(d.x1) - xScale(d.x0));
};
const getBarText = d => {
return d.n !== 0 && getBarWidth(d) > 0.3 ? d.obs : "";
};
const getBarColor = (d, i) => {
// console.log(d, i, series[i].label);
return colorScale(series[i].label);
};
return {
getQuestionTransform,
getQuestionAxis,
getBarX,
getBarWidth,
getBarHeight,
getBarText,
getBarColor
};
};
d3.csv("raw_data.csv").then(function(data) {
const rawsAndRatios = ratioOfLikert(
keyConfig.map(d => {
return d.csv;
}),
d => {
return d.N;
},
data
);
const groups = keyConfig.reduce(
(acc, d, i) => {
const { ks, groups } = acc;
const { g, csv, color, label } = d;
let idx = ks.indexOf(g);
if (idx === -1) {
idx = ks.length;
ks.push(g);
}
if (!groups[idx]) {
groups[idx] = { key: g, series: [] };
}
groups[idx].series.push({ idx: i, csv, color, label });
return { ks, groups };
},
{ ks: [], groups: [] }
).groups;
const groupsWithData = groups.map(addDataToGroup(rawsAndRatios));
plotHorizontalHtmlLegend({
parentNode: document.querySelector("#figure"),
data: keyConfig.map(d => {
return { label: d.label, color: d.color };
}),
center: 50,
className: "legendbox"
});
const { svg, width, height } = svgChart({
margin: { top: 50, right: 20, bottom: 10, left: 65 },
width: 800,
height: 500,
selector: "#figure",
id: "d3-plot"
});
const questionLabels = rawsAndRatios.map(function(d) {
return d.label;
});
const yScale = d3
.scaleBand()
.rangeRound([0, height])
.padding(0.3)
.domain(questionLabels);
const xEnd = d3.sum(groupsWithData, d => {
return d3.max(d.maxes);
});
const leftPadding = 32;
const neutralPadding = 96;
const xScale = d3
.scaleLinear()
.domain([0, xEnd])
.rangeRound([0, width - neutralPadding - leftPadding])
.nice();
plotYAxis({ svg, className: "y axis", yScale });
const svgGroup = svg
.append("g")
.attr("class", "plot")
.attr("transform", "translate(" + leftPadding + "," + 0 + ")");
const config = {
xScale,
yScale,
questionLabels
};
let groupData, xOffset, xMax;
// -- group
groupData = groupsWithData[0];
xOffset = xScale(d3.max(groupsWithData[0].maxes));
plotGroup(
groupData.data,
svgGroup
.append("g")
.attr("class", "negative")
.attr("transform", "translate(" + xOffset + "," + 0 + ")"),
computeLayout(
Object.assign({}, config, groupData, {
alignRight: true
})
)
);
// -- group
groupData = groupsWithData[2];
xOffset = xScale(d3.max(groupsWithData[0].maxes));
plotGroup(
groupData.data,
svgGroup
.append("g")
.attr("class", "positive")
.attr("transform", "translate(" + xOffset + "," + 0 + ")"),
computeLayout(Object.assign({}, config, groupData))
);
plotZeroLine({
svg: svgGroup,
className: "zero axis",
x: xOffset,
ys: [0, height]
});
// -- group
groupData = groupsWithData[1];
xOffset =
xScale(d3.max(groupsWithData[0].maxes)) +
neutralPadding +
xScale(d3.max(groupsWithData[2].maxes));
plotGroup(
groupData.data,
svgGroup
.append("g")
.attr("class", "neutral")
.attr("transform", "translate(" + xOffset + "," + 0 + ")"),
computeLayout(Object.assign({}, config, groupData))
);
});
</script>
</body>
</html>
Question 1 2 3 4 5 N
Question 1 24 294 594 1927 376 3215
Question 2 2 2 0 7 0 11
Question 3 2 0 2 4 2 10
Question 4 0 2 1 7 6 16
Question 5 0 1 3 16 4 24
Question 6 1 1 2 9 3 16
Question 7 0 0 1 4 0 5
Question 8 0 0 0 0 2 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment