Allianz visualization Challenge
04/10/2017
Giacomo Debidda
Cool... but wrong!
book_genres.tsv
Satire | Romance | Drama | ... | |
Satire | 3798 | 505 | 19 | ... |
Romance | 505 | 65243 | 1572 | ... |
Drama | 19 | 1572 | 3557 | ... |
... | ... | ... | ... | ... |
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Data Visualization Challenge</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CSS bundle injected here by html-webpack-plugin -->
<link rel="stylesheet"
href="home.370bb5cb3969b9610455.bundle.css?3ca15c1ae9444dabc9a4">
</head>
<body>
<div id="static-bar-chart"></div>
<div id="dynamic-bar-chart"></div>
<div id="comparison-chart"></div>
<!-- JS bundles injected here by html-webpack-plugin -->
<script type="text/javascript"
src="commons.e0ffa94b1f6f4891ea2c.bundle.js?3ca15c1ae9444dabc9a4">
</script>
<script type="text/javascript"
src="home.370bb5cb3969b9610455.bundle.js?3ca15c1ae9444dabc9a4">
</script>
</body>
</html>
Scaffolding
const computeLayout = (outerWidth, outerHeight, margin) => {
const width = outerWidth - margin.left - margin.right;
const height = outerHeight - margin.top - margin.bottom;
return {
width,
height,
margin,
};
};
const createComponent = (nodeId, outerWidth = 1200, outerHeight = 600,
margin = { top: 10, right: 10, bottom: 10, left: 10 }) => {
const selection = d3.select(nodeId);
const { width, height } = computeLayout(outerWidth, outerHeight, margin);
const container = selection
.append('div')
.attr('class', 'container')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const svg = container.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const chart = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
const coords = {
width,
height,
};
// not shown: header, footer, tooltip, defs
const viz = {
chart,
coords,
header,
footer,
tooltip,
};
return viz;
};
module.exports = {
createComponent,
};
Prepare the DOM
let selectedGenre = 'Science fiction'; // it will change dynamically
const zScale = d3.scaleOrdinal(d3.schemeCategory20);
const outerWidth = 1300;
const outerHeight = 600;
const margin = { top: 0, right: 30, bottom: 20, left: 250 };
const staticBarChartViz = createComponent('#static-bar-chart',
outerWidth, outerHeight, margin);
const dynamicBarChartViz = createComponent('#dynamic-bar-chart',
outerWidth, outerHeight, margin);
const comparisonChartViz = createComponent('#comparison-chart',
outerWidth, outerHeight, margin);
const drawStaticBarChart = (viz, dataset, genre) => {
// first of all, get all the d3 selections we'll need
const header = viz.header;
const footer = viz.footer;
const chart = viz.chart;
const coords = viz.coords;
const tooltip = viz.tooltip;
// code that draws the static bar chart
};
Data
import * as d3 from 'd3';
const createComponent = require('./layout_manager').createComponent;
require('../sass/main.sass');
{
// code for data visualizations here...
const rowFunction = (d) => {
/* there is no relationship between the number of books of a given genre,
and the number of books of other genres (e.g. a single person could buy 1
Satire book and: 10 Science Fiction books + 2 Drama books, etc...) */
const genre = d[''];
const obj = {
genre,
};
const entries = Object.entries(d).filter(e => e[0] !== '');
entries.forEach((e) => {
const otherGenre = e[0];
const numBooks = +e[1];
obj[otherGenre] = numBooks;
});
return obj;
};
d3.tsv('../data/book_genres.tsv', rowFunction, (error, dataset) => {
if (error) throw error;
drawStaticBarChart(staticBarChartViz, dataset);
drawDynamicBarChart(dynamicBarChartViz, dataset, selectedGenre);
drawComparisonChart(comparisonChartViz, dataset, selectedGenre);
});
}
Comparison Chart
SVG Pattern
<defs>
<pattern id="diagonal-stripe-3" patternUnits="userSpaceOnUse"
width="10" height="10">
<image xmlns:xlink="base64 image here..."
x="0" y="0" width="10" height="10">
</image>
</pattern>
<pattern id="hash4_4" patternUnits="userSpaceOnUse"
width="8" height="8" patternTransform="rotate(60)">
<rect width="4" height="8" transform="translate(0,0)">
</rect>
</pattern>
</defs>
<rect class="ratioLeft"
x="506.2221212788406"
y="484"
width="3.777878721159425"
height="19"
style="fill: url("#hash4_4");
opacity: 0.75;">
</rect>
SVG Pattern
<pattern id="diagonal-stripe-3" patternUnits="userSpaceOnUse" width="10" height="10">
<image xmlns:xlink="base64 image here..." x="0" y="0" width="10" height="10">
</image>
</pattern>
<defs>
const defs = svg.append('defs');
const patternImage = defs
.append('pattern')
.attr('id', 'diagonal-stripe-3')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 10)
.attr('height', 10)
.append('image')
.attr('xlink:href', 'base64 image here...')
.attr('x', 0)
.attr('y', 0)
.attr('width', 10)
.attr('height', 10);
const pattern = defs
.append('pattern')
.attr('id', 'hash4_4')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 8)
.attr('height', 8)
.attr('patternTransform', 'rotate(60)')
.append('rect')
.attr('width', 4)
.attr('height', 8)
.attr('transform', 'translate(0,0)');
Good Stuff
Thanks!
Bonus: all JS code
import * as d3 from 'd3';
const createComponent = require('./layout_manager').createComponent;
require('../sass/main.sass');
{
let selectedGenre = 'Science fiction'; // it will change dynamically
const zScale = d3.scaleOrdinal(d3.schemeCategory20);
const outerWidth = 1300;
const outerHeight = 600;
const margin = { top: 0, right: 30, bottom: 20, left: 250 };
/* A visualization is an Object which contains a header, a footer, the chart, its coordinates
and a tooltip. We don't need to have the data ready to insert the visualization in the DOM */
// TODO: think about appending the axes in createComponent, instead of creating them here.
const staticBarChartViz = createComponent('#static-bar-chart', outerWidth, outerHeight, margin);
const staticBarChartXAxis = staticBarChartViz.chart
.append('g')
.attr('class', 'axis axis--x')
.attr('transform', `translate(0, ${staticBarChartViz.coords.height})`);
const staticBarChartYAxis = staticBarChartViz.chart
.append('g')
.attr('class', 'axis axis--y');
const dynamicBarChartViz = createComponent(
'#dynamic-bar-chart', outerWidth, outerHeight, margin);
const dynamicBarChartXAxis = dynamicBarChartViz.chart
.append('g')
.attr('class', 'axis axis--x')
.attr('transform', `translate(0, ${dynamicBarChartViz.coords.height})`);
const dynamicBarChartYAxis = dynamicBarChartViz.chart
.append('g')
.attr('class', 'axis axis--y');
const comparisonChartViz = createComponent(
'#comparison-chart', outerWidth, outerHeight, margin);
const comparisonChartXAxisLeft = comparisonChartViz.chart
.append('g')
.attr('class', 'axis axis--x')
.attr('transform', `translate(0, ${comparisonChartViz.coords.height})`);
const comparisonChartXAxisRight = comparisonChartViz.chart
.append('g')
.attr('class', 'axis axis--x')
.attr('transform',
`translate(${comparisonChartViz.coords.width / 2}, ${comparisonChartViz.coords.height})`);
const comparisonChartYAxis = comparisonChartViz.chart
.append('g')
.attr('class', 'axis axis--y');
const drawDynamicBarChart = (viz, dataset, genre) => {
const header = viz.header;
const footer = viz.footer;
const chart = viz.chart;
const coords = viz.coords;
const tooltip = viz.tooltip;
const obj = dataset.filter(d => d.genre === genre)[0];
const entries = Object.entries(obj).filter(d => d[0] !== 'genre' && d[0] !== genre);
const data = entries.map(d => ({ genre: d[0], customers: d[1] }));
data.sort((a, b) => d3.ascending(a.customers, b.customers));
const genres = data.map(d => d.genre);
const genre1 = genres[genres.length - 1];
const genre2 = genres[genres.length - 2];
const genre3 = genres[genres.length - 3];
const mouseover = (d) => {
const htmlTooltip = `<span>${d.genre}</span><br><span>${d.customers}</span>`;
tooltip.html(htmlTooltip)
.style('left', `${d3.event.layerX}px`)
.style('top', `${(d3.event.layerY - 10)}px`);
};
header
.style('background-color', zScale(genre))
.select('h1')
.text(`${genre}`);
const spanFigure = '<span><strong>Figure 1: </strong></span>';
const spanGenre = `<span style="color: ${zScale(genre)};"><strong>${genre}</strong></span>`;
const span1 = `<span style="color: ${zScale(genre1)};"><strong>${genre1}</strong></span>`;
const span2 = `<span style="color: ${zScale(genre2)};"><strong>${genre2}</strong></span>`;
const span3 = `<span style="color: ${zScale(genre3)};"><strong>${genre3}</strong></span>`;
const htmlFooter = `${spanFigure}${obj[obj.genre]} customers bought at least 1 ${spanGenre} book.<br>
These customers also like ${span1}, ${span2} and ${span3}.`;
footer.select('p').html(htmlFooter);
const xScale = d3.scaleLinear()
.domain([0, d3.max(data.map(d => d.customers))])
.range([0, coords.width]);
const yScale = d3.scaleBand()
.domain(genres)
.range([coords.height, 0])
.round(true)
.paddingInner(0.2); // space between bars (it's a ratio)
const xAxis = d3.axisBottom()
.scale(xScale);
const yAxis = d3.axisLeft()
.scale(yScale);
dynamicBarChartXAxis
.call(xAxis);
dynamicBarChartYAxis
.call(yAxis);
const rects = chart.selectAll('rect')
.data(data);
// enter + update section (merge is new in D3 v4)
rects
.enter()
.append('rect')
.merge(rects)
.attr('x', 0)
.attr('y', d => yScale(d.genre))
.attr('width', d => xScale(d.customers))
.attr('height', yScale.bandwidth())
.style('fill', d => zScale(d.genre))
.on('mouseover', mouseover)
.on('mouseout', () => tooltip.transition().duration(500).style('opacity', 0));
};
const drawComparisonChart = (viz, dataset, genre) => {
const header = viz.header;
const footer = viz.footer;
const chart = viz.chart;
const coords = viz.coords;
const tooltip = viz.tooltip;
header
.style('background-color', zScale(genre))
.select('h1')
.text(`${genre} across all genres`);
const spanFigure = '<span><strong>Figure 2: </strong></span>';
const spanGenre = `<span style="color: ${zScale(genre)};"><strong>${genre}</strong></span>`;
const htmlFooter = `<p>${spanFigure}Comparative analysis of ${spanGenre} across all genres.<br>
On the left, percentage of customers that, given that they bough (at least) 1 ${spanGenre}
book, bought (at least) 1 book of the genre indicated on the Y axis.<br>
On the right, percentage of customers that, given that they bough (at least) 1 book of the
genre indicated on the Y axis, bought (at least) 1 ${spanGenre} book.</p>`;
footer.select('p').html(htmlFooter);
const objGenre = dataset.filter(d => d.genre === genre)[0];
const objectsOtherGenres = dataset.filter(d => d.genre !== genre);
const genres = objectsOtherGenres.map(d => d.genre);
/*
'genre' is the genre we are interested in analyzing (e.g. Art) across all genres.
'otherGenre' is the current genre we are comparing with 'genre'.
E.g. Art is 'genre', and 'otherGenre' is 'Science'.
We are interested in 2 ratios (or percentages):
1) ratioRight: % of customers who bought at least 1 'genre' book (e.g. Art), GIVEN that
they bought at least one 'otherGenre' book (e.g. Science);
2) ratioLeft: % of customers who bought at least 1 'otherGenre' book (e.g. Science), GIVEN
that they bought at least one 'genre' book (e.g. Art).
*/
const data = genres.map((otherGenre) => {
const objOtherGenre = dataset.filter(d => d.genre === otherGenre)[0];
const ratioRight = objOtherGenre[genre] / objOtherGenre[otherGenre];
const ratioLeft = objGenre[otherGenre] / objGenre[genre];
return {
genre: otherGenre,
ratioRight,
ratioLeft,
};
});
// sort alphabetically (in place)
genres.sort((a, b) => d3.descending(a, b));
const mouseoverRight = (d) => {
tooltip.transition().duration(200).style('opacity', 0.9);
const html = `${d3.format('.0%')(d.ratioRight)} customers who bought a ${d.genre} book,
bought a ${genre} book too`;
tooltip.html(html)
.style('left', `${d3.event.layerX}px`)
.style('top', `${(d3.event.layerY - 10)}px`);
};
const mouseoverLeft = (d) => {
tooltip.transition().duration(200).style('opacity', 0.9);
const html = `${d3.format('.0%')(d.ratioLeft)} customers who bought a ${genre} book, bought
a ${d.genre} book too`;
tooltip.html(html)
.style('left', `${d3.event.layerX}px`)
.style('top', `${(d3.event.layerY - 10)}px`);
};
const maxPercentage = d3.max(
[d3.max(data.map(d => d.ratioLeft)), d3.max(data.map(d => d.ratioRight))]);
const xScaleLeft = d3.scaleLinear()
.domain([0, maxPercentage])
.range([coords.width / 2, 0]);
const xScaleRight = d3.scaleLinear()
.domain([maxPercentage, 0])
.range([coords.width / 2, 0]);
const yScale = d3.scaleBand()
.domain(genres)
.range([coords.height, 0])
.round(true)
.paddingInner(0.2); // space between bars (it's a ratio)
const xAxisLeft = d3.axisBottom()
.scale(xScaleLeft)
.ticks(6)
.tickFormat(d3.format('.0%'));
const xAxisRight = d3.axisBottom()
.scale(xScaleRight)
.ticks(6)
.tickFormat(d3.format('.0%'));
const yAxis = d3.axisLeft()
.scale(yScale);
comparisonChartXAxisLeft
.call(xAxisLeft);
comparisonChartXAxisRight
.call(xAxisRight);
comparisonChartYAxis
.call(yAxis);
// TODO: currently I inject #hash4_4 in every svg (so there are duplicate ids in the DOM)
// const pattern = d3.select('#comparison-chart').select('#hash4_4')
// update the fill color of the pattern dynamically
d3.select('#hash4_4').style('fill', zScale(genre));
const rectsLeft = chart.selectAll('.ratioLeft')
.data(data);
// no need for a key function in the data binding (not sure why)
// .data(data, d => d.genre);
// .data(data, function (d) { return d.genre; });
rectsLeft
.enter()
.append('rect')
.attr('class', 'ratioLeft')
.merge(rectsLeft)
.order(rectsLeft)
.attr('x', d => xScaleLeft(d.ratioLeft))
.attr('y', d => yScale(d.genre))
.attr('width', d => (coords.width / 2) - xScaleLeft(d.ratioLeft))
.attr('height', yScale.bandwidth())
// .style('stroke', d => zScale(d.genre))
// .style('stroke', d => zScale(d.genre))
// .style('fill', 'url(#diagonal-stripe-3)')
.style('fill', 'url(#hash4_4)')
.style('opacity', 0.75)
.on('mouseover', mouseoverLeft)
.on('mouseout', () => tooltip.transition().duration(500).style('opacity', 0));
// the exit section is required
rectsLeft.exit().remove();
const rectsRight = chart.selectAll('.ratioRight')
.data(data);
rectsRight
.enter()
.append('rect')
.attr('class', 'ratioRight')
.merge(rectsRight)
.attr('x', coords.width / 2)
.attr('y', d => yScale(d.genre))
.attr('width', d => xScaleRight(d.ratioRight))
.attr('height', yScale.bandwidth())
.style('fill', zScale(genre))
.on('mouseover', mouseoverRight)
.on('mouseout', () => tooltip.transition().duration(500).style('opacity', 0));
// the exit section is required
rectsRight.exit().remove();
};
const drawStaticBarChart = (viz, dataset) => {
const header = viz.header;
const footer = viz.footer;
const chart = viz.chart;
const coords = viz.coords;
const tooltip = viz.tooltip;
const data = dataset.map((d) => {
const obj = { genre: d.genre, customers: d[d.genre] };
return obj;
});
// sort in ascending order (in place). The bar at the bottom (lowest value) is drawn first
// data.sort((a, b) => a.customers - b.customers);
data.sort((a, b) => d3.ascending(a.customers, b.customers));
const genres = data.map(d => d.genre);
const genre1 = genres[genres.length - 1];
const genre2 = genres[genres.length - 2];
const genre3 = genres[genres.length - 3];
const mouseover = (d) => {
const htmlTooltip = `<span>${d.genre}</span><br><span>${d.customers}</span>`;
tooltip.html(htmlTooltip)
.style('left', `${d3.event.layerX}px`)
.style('top', `${(d3.event.layerY - 10)}px`);
selectedGenre = d.genre;
drawDynamicBarChart(dynamicBarChartViz, dataset, selectedGenre);
drawComparisonChart(comparisonChartViz, dataset, selectedGenre);
};
header.select('h1')
.text('Dataviz Challenge');
const spanFigure = '<span><strong>Figure 1: </strong></span>';
const span1 = `<span style="color: ${zScale(genre1)};"><strong>${genre1}</strong></span>`;
const span2 = `<span style="color: ${zScale(genre2)};"><strong>${genre2}</strong></span>`;
const span3 = `<span style="color: ${zScale(genre3)};"><strong>${genre3}</strong></span>`;
const htmlFooter = `${spanFigure}Customers who bought at least 1 book of these genres.<br>
The three most popular genres are: ${span1}, ${span2} and ${span3}.<br>
<em>Note: hover on the bars to update the visualizations below.</em>`;
footer.select('p').html(htmlFooter);
const xScale = d3.scaleLinear()
.domain([0, d3.max(data.map(d => d.customers))])
.range([0, coords.width]);
const yScale = d3.scaleBand()
.domain(genres)
.range([coords.height, 0])
.round(true)
.paddingInner(0.2);
const xAxis = d3.axisBottom()
.scale(xScale);
const yAxis = d3.axisLeft()
.scale(yScale);
staticBarChartXAxis
.call(xAxis);
staticBarChartYAxis
.call(yAxis);
const rects = chart.selectAll('rect')
.data(data);
// enter + update section (merge is new in D3 v4)
rects
.enter()
.append('rect')
.merge(rects)
.attr('x', 0)
.attr('y', d => yScale(d.genre))
.attr('width', d => xScale(d.customers))
.attr('height', yScale.bandwidth())
.style('fill', d => zScale(d.genre))
.on('mouseover', mouseover)
.on('mouseout', () => tooltip.transition().duration(500).style('opacity', 0));
};
const rowFunction = (d) => {
/* there is no relationship between the number of books of a given genre,
and the number of books of other genres (e.g. a single person could buy 1
Satire book and: 10 Science Fiction books + 2 Drama books, etc...) */
const genre = d[''];
const obj = {
genre,
};
const entries = Object.entries(d).filter(e => e[0] !== '');
entries.forEach((e) => {
const otherGenre = e[0];
const numBooks = +e[1];
obj[otherGenre] = numBooks;
});
return obj;
};
d3.tsv('../data/book_genres.tsv', rowFunction, (error, dataset) => {
if (error) throw error;
drawStaticBarChart(staticBarChartViz, dataset);
drawDynamicBarChart(dynamicBarChartViz, dataset, selectedGenre);
drawComparisonChart(comparisonChartViz, dataset, selectedGenre);
});
}
Dataviz Challenge
By Giacomo Debidda
Dataviz Challenge
- 1,226