Skip to content

Instantly share code, notes, and snippets.

@mthh
Last active November 21, 2017 22:42
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 mthh/8f97dda227d21163773b0a714a573856 to your computer and use it in GitHub Desktop.
Save mthh/8f97dda227d21163773b0a714a573856 to your computer and use it in GitHub Desktop.
"Cooperative" brush and tooltip II
license: gpl-3.0
border: no

"Cooperative" brush and tooltip II

The brush overlay is added after (ie. on the top of) the features on the chart, then mouse events are transmitted from the brush area to the bars on the chart.

Example with the opposite method (brush under the features receiving mouse events): "Cooperative" brush and tooltip I

<!DOCTYPE html>
<meta charset="utf-8">
<style>
@import url('https://fonts.googleapis.com/css?family=Patrick+Hand|Signika|Dosis');
.area {
fill: steelblue;
clip-path: url(#clip);
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
.axis--y > .domain, .axis--x > .domain{
stroke: #BEBEBE;
}
.axis--y > g.tick > line, .axis--x > g.tick > line{
stroke: #BEBEBE;
}
#var_name {
margin-left: 60px;
margin-bottom: 5px;
font-family: 'Dosis', sans-serif;
font-size: 22px;
font-weight: 800;
}
</style>
<!-- <p id="var_name"></p> -->
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script>
const svg = d3.select("svg"),
margin = { top: 20, right: 20, bottom: 110, left: 40 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
const x = d3.scaleBand().range([0, width]).padding(0.1),
y = d3.scaleLinear().range([height, 0]);
const xAxis = d3.axisBottom(x),
yAxis = d3.axisLeft(y);
let brush, zoom, ref_data, data, nbFt, mean_value;
let focus;
let displayed;
let current_range;
let my_region = 'FRE';
let variable_name = 'Ma variable';
let t;
// d3.select('#var_name')
// .text(variable_name);
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,
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,
ratio: (+ft.properties.EMP_2014 / +ft.properties['Y20.64_2014']) * 100000,
})).filter(ft => ft.ratio);
data = [].concat(ref_data);
data.sort((a, b) => a.ratio - b.ratio);
nbFt = data.length;
current_range = [0, nbFt];
mean_value = d3.mean(data.map(d => d.ratio));
brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on('brush', brushed)
.on("end", endbrush);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(data.map(ft => ft.id));
y.domain([d3.min(data, d => d.ratio), d3.max(data, d => d.ratio)]);
focus.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.select('.axis--x')
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
let groupe_line_mean = focus.append('g').attr('class', 'mean');
groupe_line_mean.append('text')
.attrs({ x: 60, y: y(mean_value) + 20 })
.styles({
'font-family': '\'Signika\', sans-serif',
'fill': 'red',
'fill-opacity': '0.8',
display: 'none'
})
.text('Valeur moyenne');
focus.append("g")
.attr("class", "brush")
.call(brush);
svg.append('image')
.attrs({
x: width + margin.left + 5,
y: 385,
width: 15,
height: 15,
'xlink:href': 'reverse_blue.png',
id: 'img_reverse'
})
.on('click', function () {
if (data[0].ratio < data[data.length - 1].ratio) {
data.sort((a, b) => b.ratio - a.ratio);
} else {
data.sort((a, b) => a.ratio - b.ratio);
}
x.domain(data.slice(current_range[0], current_range[1]).map(ft => ft.id));
update();
updateAxis();
});
const tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("rect")
.attr("width", 50)
.attr("height", 40)
.attr("fill", "white")
.style("opacity", 0.5);
tooltip.append("text")
.attr('class', 'id_feature')
.attr("x", 25)
.attr("dy", "1.2em")
.style("text-anchor", "middle")
.attr("font-size", "14px");
tooltip.append("text")
.attr('class', 'value_feature')
.attr("x", 25)
.attr("dy", "2.4em")
.style("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold");
update();
});
function update() {
displayed = 0;
let bar = focus.selectAll(".bar")
.data(data);
bar
.attr("x", d => x(d.id))
.attr("width", x.bandwidth())
.attr("y", d => y(d.ratio))
.attr("height", d => height - y(d.ratio))
.style('fill', d => d.id !== my_region ? 'steelblue' : 'yellow')
.style("display", (d) => {
let to_display = x(d.id) != null;
if (to_display) {
displayed += 1;
return 'initial';
}
return 'none';
});
bar.enter()
.insert("rect", '.mean')
.attr("class", "bar")
.attr("x", d => x(d.id))
.attr("width", x.bandwidth())
.attr("y", d => y(d.ratio))
.attr("height", d => height - y(d.ratio))
.style('fill', d => d.id !== my_region ? 'steelblue' : 'yellow');
bar.exit().remove();
focus.selectAll(".bar")
.on("mouseover", () => {
clearTimeout(t);
svg.select('.tooltip').style('display', null);
})
.on("mouseout", () => {
clearTimeout(t);
t = setTimeout(() => { svg.select('.tooltip').style('display', 'none'); }, 250);
})
.on("mousemove", function(d) {
clearTimeout(t);
const tooltip = svg.select('.tooltip').style('display', null);
tooltip
.select("text.id_feature")
.text(`${d.id}`);
tooltip.select('text.value_feature')
.text(`${Math.round(d.ratio)}`);
tooltip
.attr('transform', `translate(${[d3.mouse(this)[0] - 5, d3.mouse(this)[1] - 25]})`);
});
svg.select('.brush')
.on('mousemove mousedown', function () {
dispatchClickToBar(d3.event.pageX, d3.event.pageY, d3.event.clientX, d3.event.clientY, 'mousemove');
})
.on('mouseout', function () {
clearTimeout(t);
t = setTimeout(() => { svg.select('.tooltip').style('display', 'none'); }, 250);
})
}
function updateAxis() {
let axis_x = focus.select(".axis--x").call(xAxis);
axis_x.selectAll("text")
.attrs(d => {
if (displayed > 20) {
return { dx: '-0.8em', dy: '0.15em', transform: 'rotate(-65)' };
} else {
return { dx: '0', dy: '0.71em', transform: null };
}
})
.style('text-anchor', d => displayed > 20 ? 'end' : 'middle');
}
function dispatchClickToBar(pageX, pageY, clientX, clientY, type) {
const elems = document.elementsFromPoint(pageX, pageY);
const elem = elems.find(e => e.className.baseVal === 'bar');
if (elem) {
const new_click_event = new MouseEvent(type, {
pageX: pageX,
pageY: pageY,
clientX: clientX,
clientY: clientY,
bubbles: true,
cancelable: true,
view: window
});
elem.dispatchEvent(new_click_event);
} else {
clearTimeout(t);
t = setTimeout(() => { svg.select('.tooltip').style('display', 'none'); }, 5);
}
}
function brushed() {
clearTimeout(t);
svg.select('.tooltip').style('display', 'none');
var s = d3.event.selection || [0,0];
let _current_range = [Math.round(s[0] / (width/nbFt)), Math.round(s[1] / (width/nbFt))];
focus.selectAll(".bar")
.style('fill', (_, i) => {
if (i >= _current_range[0] && i < _current_range[1]) {
return 'orange';
}
return 'steelblue';
});
}
function endbrush() {
// dispatchClickToBar(
// d3.event.sourceEvent.pageX,
// d3.event.sourceEvent.pageY,
// d3.event.sourceEvent.clientX,
// d3.event.sourceEvent.clientY,
// 'mousemove');
var s = d3.event.selection || [0, 0];
let _current_range = [Math.round(s[0] / (width/nbFt)), Math.round(s[1] / (width/nbFt))];
focus.selectAll(".bar")
.style('fill', (_, i) => {
if (i >= _current_range[0] && i < _current_range[1]) {
return 'red';
}
return 'steelblue';
});
}
</script>
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