Skip to content

Instantly share code, notes, and snippets.

@mthh
Last active April 6, 2018 01:54
Show Gist options
  • Save mthh/e05dedf9543411e33ed10359d5095be3 to your computer and use it in GitHub Desktop.
Save mthh/e05dedf9543411e33ed10359d5095be3 to your computer and use it in GitHub Desktop.
Grouped bar chart with negative values
license: gpl-3.0
border: no
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
@import url('https://fonts.googleapis.com/css?family=Patrick+Hand|Signika|Dosis');
</style>
</head>
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script>
const color = d3.schemeCategory10;
const margin = { top: 50, right: 20, bottom: 60, left: 60 };
const width = 480 - margin.left - margin.right;
const height = 480 - margin.top - margin.bottom;
const zoom = d3.zoom()
.scaleExtent([1, 5])
.translateExtent([[0, 0], [width, height]])
.on("zoom", () => { zoomed(d3.event.transform); });
const svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const plot = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const clip = plot.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
const x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1);
const x1 = d3.scaleBand()
.padding(0.05);
const y = d3.scaleLinear()
.range([height, 0])
.nice();
const z = d3.scaleOrdinal()
// .range(["#7b6888", "#6b486b", "#a05d56"]);
.range(['#80fe9c', '#feb580', '#80cdfe']);
const keys = ['dist1', 'dist2', 'dist3'];
const my_region = 'FRE';
const variable1 = 'DENS_2016';
const variable2 = 'TX_EMP_2014';
const variable3 = 'EUR_HAB_EU_2014';
const pretty_name1 = 'Densité (2016)';
const pretty_name2 = 'Taux d\'emploi (2014)';
const pretty_name3 = 'PIB par habitant en parité (2014)'
let nbDisplayFt = 5;
let data;
let nbFt;
const ref_values = {};
function computeDist(obj_data, ref_region) {
const _getPR = (v, serie) => {
let count = 0;
for (let i = 0; i < serie.length; i++) {
if (serie[i] <= v) {
count += 1;
}
}
return 100 * count / serie.length;
};
const len_values = obj_data.length;
const serie1 = obj_data.map(ft => ft[variable1]);
const serie2 = obj_data.map(ft => ft[variable2]);
const serie3 = obj_data.map(ft => ft[variable3]);
let ref_val_var1;
let ref_val_var2;
let ref_val_var3;
obj_data.forEach(ft => {
if (ft.id !== ref_region) return;
else {
ref_values[variable1] = ref_val_var1 = _getPR(ft[variable1], serie1);
ref_values[variable2] = ref_val_var2 = _getPR(ft[variable2], serie2);
ref_values[variable3] = ref_val_var3 = _getPR(ft[variable3], serie3);
}
});
for (let i = 0; i < len_values; i++) {
const ft = obj_data[i];
ft['dist1'] = _getPR(ft[variable1], serie1) - ref_val_var1;
ft['dist2'] = _getPR(ft[variable2], serie2) - ref_val_var2;
ft['dist3'] = _getPR(ft[variable3], serie3) - ref_val_var3;
ft['mean_dist'] = Math.sqrt(Math.pow(ft.dist1, 2)+ Math.pow(ft.dist2, 2) + Math.pow(ft.dist3, 2));
}
};
function getClosestFt(obj_data, nb){
const abs = Math.abs;
let a = obj_data.map(ft => ({ id: ft.id, mean: ft.mean_dist, dist1: ft.dist1, dist2: ft.dist2, dist3: ft.dist3 }));
a.sort((a, b) => abs(a.mean) - abs(b.mean));
return a.slice(1, nb + 1);
}
d3.json("nuts1_data.geojson", function(error, geojson_data) {
if (error) throw error;
ref_data = geojson_data.features.map(ft => ({
id: ft.properties.NUTS1_2016,
TX_EMP_2014: (+ft.properties.EMP_2014 / +ft.properties['Y20.64_2014']) * 100000,
DENS_2016: +ft.properties.DENS_2016,
EUR_HAB_EU_2014: +ft.properties.EUR_HAB_EU_2014,
})).filter(ft => ft[variable1] && ft[variable2] && ft[variable3]);
computeDist(ref_data, my_region);
data = getClosestFt(ref_data, nbDisplayFt);
nbFt = data.length;
x0.domain(data.map(d => d.id));
x1.domain(keys).rangeRound([0, x0.bandwidth()]);
y.domain([
d3.min(data, function(d) { return d3.min(keys, function(key) { return d[key]; }); }),
d3.max(data, function(d) { return d3.max(keys, function(key) { return d[key]; }); })]).nice();
drawBar();
plot.append("g")
.attr("class", "axis axis-x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0));
plot.selectAll('.axis-x > .tick > line')
.attr('transform', `translate(38,0)`)
.attr('y1', '6')
.attr('y2', '-370')
.style('stroke', 'gray')
.style('stroke-opacity', '0.4');
plot.append("g")
.attr("class", "axis axis-y")
.call(d3.axisLeft(y).ticks(null, "s"))
svg.append("text")
.attr("x", margin.left / 2)
.attr("y", margin.top + (height / 2))
.attr("transform", `rotate(-90, ${margin.left / 2}, ${margin.top + (height / 2)})`)
.style("text-anchor", "middle")
.styles({ 'font-family': '\'Signika\', sans-serif', 'font-size': '12px', fill: 'black' })
.text("Écart à ma région (%)");
plot.insert('line')
.attrs({
x1: 0, x2: width, y1: y(0), y2: y(0), 'stroke-width': '1px', stroke: '#000', id: 'zero_line'
})
prepareTooltip();
makeGrid();
makeFilterSection();
});
function drawBar() {
plot.insert("g", '#zero_line')
.attr('id', 'bar')
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", d => "translate(" + x0(d.id) + ",0)")
.selectAll("rect")
.data(d => keys.map(function(key) { return { key: key, value: d[key], id: d.id }; }))
.enter().append("rect")
.attr("x", d => x1(d.key))
.attr("y", function(d) { return y(Math.max(0, d.value)); })
.attr("width", x1.bandwidth())
.attr('height', d => Math.abs(y(d.value) - y(0)))
.attr("fill", d => z(d.key));
plot.selectAll('rect')
.on("mouseover", () => { svg.select('.tooltip').style('display', null); })
.on("mouseout", () => { svg.select('.tooltip').style('display', 'none'); })
.on("mousemove", function(d) {
const indicateur = d.key.indexOf('1') > -1 ? variable1 : d.key.indexOf('2') > -1 ? variable2 : variable3;
const val_other = ref_data.filter(ft => ft.id === d.id)[0][indicateur];
const tooltip = svg.select('.tooltip');
const tooltip_title = tooltip
.select("tspan#tooltip_title")
.attr('x', 10)
.text(`${my_region} - ${d.id}`);
tooltip.select('tspan#tooltip_l1')
.text(`Indicateur : ${indicateur}`);
tooltip.select('tspan#tooltip_l2')
.text(`${d.id} : ${Math.round(val_other * 10) / 10}`);
tooltip.select('tspan#tooltip_l3')
.text(`${my_region} : ${Math.round(ref_values[indicateur] * 10) / 10}`);
tooltip.select('tspan#tooltip_l4')
.text(`Écart relatif : ${Math.round(d.value * 10) / 10}%`);
const new_rect_size = tooltip.select('text').node().getBoundingClientRect().width + 20;
tooltip_title.attr('x', new_rect_size / 2);
tooltip.select('rect')
.attr('width', new_rect_size);
let x_val = +this.parentElement.getAttribute('transform').split('(')[1].split(',')[0] + this.x.baseVal.value - new_rect_size / 2;
tooltip
.attr('transform', `translate(${[x_val - 5, d3.mouse(this)[1] - 85]})`);
});
}
function makeGrid() {
plot.insert("g", '#bar')
.attr("class", "grid grid-y")
.call(d3.axisLeft(y)
.tickSize(-width)
.tickFormat(''));
plot.selectAll('.grid')
.selectAll('line')
.attr('stroke', 'gray');
}
function prepareTooltip() {
const tooltip = plot.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("rect")
.attr("width", 260)
.attr("height", 80)
.attr("fill", "beige")
.style("opacity", 0.65);
let text_zone = tooltip.append("text")
.attr("x", 10)
.attr("dy", "0")
.style('font-family', 'sans-serif')
.attr("font-size", "11px")
.style('text-anchor', 'start')
.style('fill', 'black');
text_zone.append("tspan")
.attr('id', 'tooltip_title')
.attr("x", 10)
.attr("dy", "15")
.attr("font-size", "12px")
.attr('font-weight', '800');
text_zone.append("tspan")
.attr('id', 'tooltip_l1')
.attr("x", 10)
.attr("dy", "14");
text_zone.append("tspan")
.attr('id', 'tooltip_l2')
.attr("x", 10)
.attr("dy", "14");
text_zone.append("tspan")
.attr('id', 'tooltip_l3')
.attr("x", 10)
.attr("dy", "14");
text_zone.append("tspan")
.attr('id', 'tooltip_l4')
.attr("x", 10)
.attr("dy", "14");
}
function makeFilterSection () {
const filter_section = d3.select('body').append('div')
.styles({ display: 'inline', position: 'absolute', margin: '20px', top: '180px' });
filter_section.append('span')
.html('Afficher les ');
filter_section.append('input')
.attr('type', 'number')
.style('width', '35px')
.property('value', 5)
.on('change', function () {
const nb_feature = +this.value;
data = getClosestFt(ref_data, nb_feature);
nbFt = data.length;
const t = plot.selectAll('#bar')
.transition()
.duration(100);
plot.selectAll('#bar').remove();
x0.domain(data.map(d => d.id));
x1.domain(keys).rangeRound([0, x0.bandwidth()]);
y.domain([
d3.min(data, function(d) { return d3.min(keys, function(key) { return d[key]; }); }),
d3.max(data, function(d) { return d3.max(keys, function(key) { return d[key]; }); })]).nice();
drawBar();
let axis_x = plot.select('.axis-x')
.transition(t)
.call(d3.axisBottom(x0));
plot.select('.axis-y')
.transition(t)
.call(d3.axisLeft(y).ticks(null, "s"));
plot.select('#zero_line')
.attrs({ y1: y(0), y2: y(0) });
plot.select(".grid-y")
.transition(t)
.call(d3.axisLeft(y)
.tickSize(-width)
.tickFormat(''))
.selectAll('line')
.attr('stroke', 'lightgray');
axis_x.selectAll("text")
.attrs(d => {
if (nb_feature > 18) {
return {
dx: '-0.8em',
dy: '0.15em',
transform: 'rotate(-65)',
'font-size': nb_feature > 45 ? '7.5px' : '9px' };
} else {
return { dx: '0', dy: '0.71em', transform: null };
}
})
.style('text-anchor', d => nb_feature > 18 ? 'end' : 'middle');
axis_x.selectAll('.tick > line')
.attr('transform', `translate(${Math.ceil(380 / (nbFt * 2))},0)`)
.attr('y1', '6')
.attr('y2', '-370')
.style('stroke', 'gray')
.style('stroke-opacity', '0.4');
});
filter_section.append('span')
.html('régions les plus proches');
}
</script>
</body>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment