Last active
April 6, 2018 01:54
-
-
Save mthh/e05dedf9543411e33ed10359d5095be3 to your computer and use it in GitHub Desktop.
Grouped bar chart with negative values
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 | |
border: no |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment