Skip to content

Instantly share code, notes, and snippets.

@mthh
Last active August 24, 2017 21:13
Show Gist options
  • Save mthh/0e8fce303def3d2bf6603ad017369032 to your computer and use it in GitHub Desktop.
Save mthh/0e8fce303def3d2bf6603ad017369032 to your computer and use it in GitHub Desktop.
Scatterplot with grid, tooltip and filter
license: gpl-3.0
border: no
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
</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 x = d3.scaleLinear()
.range([0, width])
.nice();
const y = d3.scaleLinear()
.range([height, 0]);
const xAxis = d3.axisBottom(x).ticks(12),
yAxis = d3.axisLeft(y).ticks(12 * height / width);
const xAxis2 = d3.axisBottom(x).ticks(12),
yAxis2 = d3.axisLeft(y).ticks(12 * height / width);
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 my_region = 'FRE';
const variable1 = 'DENS_2016';
const variable2 = 'TX_EMP_2014';
const rank_variable1 = `pr_${variable1}`;
const rank_variable2 = `pr_${variable2}`;
const pretty_name1 = 'Densité';
const pretty_name2 = 'Taux d\'emploi';
let nbFt;
let mean_variable1;
let mean_variable2;
d3.json("nuts1_data.geojson", function(error, geojson_data) {
if (error) throw error;
const getQDENS = (v) => {
if (v.indexOf('q1') > -1) return 1;
else if (v.indexOf('q2') > -1) return 2;
else if (v.indexOf('q3') > -1) return 3;
else return 4;
};
ref_data = geojson_data.features.map(ft => ({
id: ft.properties.NUTS1_2016,
EMP_2014: +ft.properties.EMP_2014,
Y20_60_2014: +ft.properties['Y20.64_2014'] / 1000,
TX_EMP_2014: (+ft.properties.EMP_2014 / +ft.properties['Y20.64_2014']) * 100000,
DENS_2016: +ft.properties.DENS_2016,
TypoDENS_2016: getQDENS(ft.properties.TypoDENS_2016)
})).filter(ft => ft[variable1] && ft[variable2]);
computePercentileRank(ref_data, variable1, rank_variable1);
computePercentileRank(ref_data, variable2, rank_variable2);
data = [].concat(ref_data);
nbFt = data.length;
x.domain(d3.extent(data, d => d[rank_variable1])).nice();
y.domain(d3.extent(data, d => d[rank_variable2])).nice();
mean_variable1 = _getPR(d3.mean(data.map(d => d[variable1])), data.map(d => d[variable1]));
mean_variable2 = _getPR(d3.mean(data.map(d => d[variable2])), data.map(d => d[variable2]));
const scatter = plot.append("g")
.attr("id", "scatterplot")
.attr("clip-path", "url(#clip)")
.append('g');
const underlying_rect = scatter
.append("rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0)
.style('fill', 'transparent')
.call(zoom);
underlying_rect.on('dblclick.zoom', () => {
zoomed(d3.zoomTransform({x: 0, y: 0, k: 1}))
});
const groupe_line_mean = plot.append('g')
.attr("clip-path", "url(#clip)")
.attr('class', 'mean');
groupe_line_mean.append('line')
.attr("clip-path", "url(#clip)")
.attrs({
x1: x(mean_variable1), x2: x(mean_variable1), y1: 0, y2: width,
'stroke-dasharray': '10, 5', 'stroke-width': '2px',
})
.style('stroke', 'red');
groupe_line_mean.append('line')
.attr("clip-path", "url(#clip)")
.attrs({
x1: 0, x2: width, y1: y(mean_variable2), y2: y(mean_variable2),
'stroke-dasharray': '10, 5', 'stroke-width': '2px',
})
.style('stroke', 'red');
scatter.selectAll(".dot")
.data(data)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 5)
.attr("cx", d => x(d[rank_variable1]))
.attr("cy", d => y(d[rank_variable2]))
.attr("opacity", 0.6)
.style("fill", d => color[d.TypoDENS_2016])
.on("mouseover", () => { svg.select('.tooltip').style('display', null); })
.on("mouseout", () => { svg.select('.tooltip').style('display', 'none'); })
.on("mousemove", function(d) {
const tooltip = svg.select('.tooltip');
const tooltip_title = tooltip
.select("tspan#tooltip_title")
.text(`${d.id}`);
tooltip.select('tspan#tooltip_l1')
.text(`${pretty_name1} (rang) : ${Math.round(d[rank_variable1] * 10) / 10}/100`);
tooltip.select('tspan#tooltip_l2')
.text(`${pretty_name1} (valeur) : ${Math.round(d[variable1] * 10) / 10}`);
tooltip.select('tspan#tooltip_l3')
.text(`${pretty_name2} (rang) : ${Math.round(d[rank_variable2] * 10) / 10}/100`);
tooltip.select('tspan#tooltip_l4')
.text(`${pretty_name1} (valeur) : ${Math.round(d[variable2] * 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);
tooltip
.attr('transform', `translate(${[d3.mouse(this)[0] - 5, d3.mouse(this)[1] - 35]})`);
});
makeGrid();
plot.append("g")
.attr("class", "x axis")
.attr('id', "axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
plot.append("g")
.attr("class", "y axis")
.attr('id', "axis--y")
.call(yAxis);
svg.append("text")
.attr('id', 'title-axis-x')
.attr("x", margin.left + width / 2)
.attr("y", margin.top + height + margin.bottom / 2 + 5)
.style("text-anchor", "middle")
.styles({ 'font-family': 'sans-serif', 'font-size': '12px' })
.text(variable1);
let isXinverted = false;
let isYinverted = false;
svg.append('image')
.attrs({
x: margin.left + width / 2 - 20 - svg.select('#title-axis-x').node().getBoundingClientRect().width / 2,
y: margin.top + height + margin.bottom / 2 - 7.5,
width: 15,
height: 15,
'xlink:href': 'reverse_blue.png',
id: 'img_reverse_x'
})
.on('click', function () {
for (let i = 0; i < nbFt; i++) {
data[i][rank_variable1] = 100 - data[i][rank_variable1];
}
scatter.selectAll('circle')
.transition()
.attr('cx', d => x(d[rank_variable1]));
});
svg.append("text")
.attr('id', 'title-axis-y')
.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': 'sans-serif', 'font-size': '12px' })
.text(variable2);
svg.append('image')
.attrs({
x: margin.left / 2 - 15,
y: margin.top + (height / 2) + svg.select('#title-axis-y').node().getBoundingClientRect().height / 2 + 7.5,
width: 15,
height: 15,
'xlink:href': 'reverse_blue.png',
id: 'img_reverse_y'
})
.on('click', function () {
for (let i = 0; i < nbFt; i++) {
data[i][rank_variable2] = 100 - data[i][rank_variable2];
}
scatter.selectAll('circle')
.transition()
.attr('cy', d => y(d[rank_variable2]));
});
svg.append('text')
.attrs({ x: 350, y: 25 })
.styles({ 'font-family': 'sans-serif', 'font-size': '12px' })
.text(`Complétude : ${Math.round(data.length / geojson_data.features.length * 1000) / 10}%`);
prepareTooltip();
makeFilterSection();
});
function makeGrid() {
plot.insert("g", '#scatterplot')
.attr("class", "grid grid-x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis2
.tickSize(-height)
.tickFormat(''));
plot.insert("g", '#scatterplot')
.attr("class", "grid grid-y")
.call(yAxis2
.tickSize(-width)
.tickFormat(''));
plot.selectAll('.grid')
.selectAll('line')
.attr('stroke', 'lightgray');
}
function prepareTooltip() {
const tooltip = svg.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('p')
.html('Filtre de comparaison :');
const filter_select = filter_section.append('select');
filter_select.append('option').property('value', 0).text('Pas de filtre');
filter_select.append('option').property('value', 1).text('Typo - Catégorie 1');
filter_select.append('option').property('value', 2).text('Typo - Catégorie 2');
filter_select.append('option').property('value', 3).text('Typo - Catégorie 3');
filter_select.append('option').property('value', 4).text('Typo - Catégorie 4');
filter_select.on('change', function () {
const val = +this.value;
plot.select('#scatterplot').selectAll('circle')
.transition()
.style('display', 'initial');
if (val > 0) {
plot.select('#scatterplot').selectAll('circle')
.transition()
.style('display', d => d.TypoDENS_2016 === val ? 'inital' : 'none');
}
});
}
function zoomed(transform) {
if (transform.k === 1) {
transform.x = 0;
transform.y = 0;
}
const new_xScale = transform.rescaleX(x);
const new_yScale = transform.rescaleY(y);
const t = plot.select('#scatterplot').selectAll('circle').transition().duration(350);
t.attr('transform', transform);
plot.select("#axis--x")
.transition(t)
.call(xAxis.scale(new_xScale));
plot.select(".grid-x")
.transition(t)
.call(xAxis2.scale(new_xScale)
.tickSize(-height)
.tickFormat(''))
.selectAll('line')
.attr('stroke', 'lightgray');
plot.select("#axis--y")
.transition(t)
.call(yAxis.scale(new_yScale));
plot.select(".grid-y")
.transition(t)
.call(yAxis2.scale(new_yScale)
.tickSize(-width)
.tickFormat(''))
.selectAll('line')
.attr('stroke', 'lightgray');
plot.select('.mean')
.selectAll('line')
.transition(t)
.attr('transform', transform);
}
function computePercentileRank(obj, field_name, result_field_name) {
const values = obj.map(d => d[field_name]);
const len_values = values.length;
const getPR = (v) => {
let count = 0;
for (let i = 0; i < len_values; i++) {
if (values[i] <= v) {
count += 1;
}
}
return 100 * count / len_values;
};
for (let ix = 0; ix < len_values; ix++) {
obj[ix][result_field_name] = getPR(values[ix]);
}
}
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;
};
</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