Published
Edited
Apr 10, 2021
5 stars
Insert cell
md`# D3 coordinated highlight`
Insert cell
d3code = require("d3@5")
Insert cell
d3lasso = require('d3-lasso')
Insert cell
d3 = {
const d3 = Object.assign(d3code,{lasso:d3lasso.lasso});
window.d3 = d3;
return d3;
}
Insert cell
styles = html`
<div></div>
<style>
.lasso path {
stroke: rgb(80,80,80);
stroke-width:2px;
}

.lasso .drawn {
fill-opacity:.05 ;
}

.lasso .loop_close {
fill:none;
stroke-dasharray: 4, 4;
}

.lasso .origin {
fill:#3399FF;
fill-opacity:.5;
}
</style>
`
Insert cell
chart = {
const color = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.create("svg").attr("width", 1000).attr("height", 400);
const data = d3.range(100).map(i => ({x:Math.random() * 380, y:Math.random() * 380, id: i, label: Math.round(Math.random() * 10)}));

svg.append('rect').attr('width', 400).attr('height', 400).attr('fill', 'none').attr('stroke', 'black').attr('stroke-width', 1);
const scatter_g = svg.append('g').classed('scatterplot', true).attr('transform', 'translate(10,10)');
scatter_g.append('rect').attr('width', 380).attr('height', 380).attr('fill','white');
const scatter = scatter_g.selectAll('circle').data(data).join('circle')
.attr('fill', d => color(d.label))
.attr('cx', d=>d.x)
.attr('cy', d=>d.y)
.attr('r', 5);

const control_g = svg.append('g').classed('scatterplot', true).attr('transform', 'translate(450,10)');
const labels = d3.range(10);

control_g.append('text').text('show/hide').attr('x',0).attr('y', 10).style('user-select', 'none');
const show_g = control_g.selectAll('g.show').data(labels).join('g').each(function(d) {
const g = d3.select(this);
let data = { label: d, show: true }
g.append('circle')
.classed('show', true)
.datum(data)
.attr('fill', d=>color(d.label))
.attr('stroke', d=>color(d.label))
.attr('stroke-width', 1)
.attr('cx', 20)
.attr('cy', d=>38 * (d.label + 1))
.attr('r', 5)
.on('click', function(d) {
d.show = !d.show;
d3.select(this).attr('fill', d=>d.show ? color(d.label) : 'white')
scatter.filter(dd=>dd.label === d.label).attr('display', !d.show ? 'none' : 'block');
});
g.append('text')
.text(d)
.attr('x', 40)
.attr('y', 38 * (d + 1))
.attr('dy', '0.3rem')
.style('user-select', 'none');
})

control_g.append('text').text('highlight').attr('x',100).attr('y', 10).style('user-select', 'none');
const highlight_g = control_g.selectAll('g.highlight').data(labels).join('g').each(function(d) {
const g = d3.select(this);
let data = { label: d, highlight: false }
g.append('circle')
.classed('highlight', true)
.datum(data)
.attr('fill', d=>color(d.label))
.attr('stroke', d=>color(d.label))
.attr('stroke-width', 1)
.attr('cx', 120)
.attr('cy', d=>38 * (d.label + 1))
.attr('r', 5)
.on('mouseover', function(d) {
d3.select(this).attr('r', 8);
scatter.filter(dd=>dd.label === d.label).raise().attr('r', 8);
})
.on('mouseout', function(d) {
d3.select(this).attr('r', d.highlight ? 8 : 5);
scatter.filter(dd=>dd.label === d.label).attr('r', d.highlight ? 8 : 5);
})
.on('click', function(d) {
d.highlight = !d.highlight;
});
g.append('text')
.text(d)
.attr('x', 140)
.attr('y', 38 * (d + 1))
.attr('dy', '0.3rem')
.style('user-select', 'none');
});

control_g.append('text').text('radio-highlight').attr('x',200).attr('y', 10).style('user-select', 'none');
const radio_controller = {
label: -1,
highlight: function(label) {
if (label === undefined) label = this.label;
scatter.attr('r', d=>d.label === label ? 8 : 5);
d3.selectAll('circle.radio-highlight').attr('r', d=>d.label === label ? 8 : 5);
},
};
const radio_highlight_g = control_g.selectAll('g.highlight').data(labels).join('g').each(function(d) {
const g = d3.select(this);
let data = { label: d }
g.append('circle')
.classed('radio-highlight', true)
.datum(data)
.attr('fill', d=>color(d.label))
.attr('stroke', d=>color(d.label))
.attr('stroke-width', 1)
.attr('cx', 220)
.attr('cy', d=>38 * (d.label + 1))
.attr('r', 5)
.on('mouseover', function(d) {
d3.select(this).attr('r', 8);
radio_controller.highlight(d.label);
})
.on('mouseout', function(d) {
d3.select(this).attr('r', d.highlight ? 8 : 5);
scatter.filter(dd=>dd.label === d.label).attr('r', d.highlight ? 8 : 5);
radio_controller.highlight();
})
.on('click', function(d) {
radio_controller.label = (radio_controller.label === d.label) ? -1 : d.label;
radio_controller.highlight();
});
g.append('text')
.text(d)
.attr('x', 240)
.attr('y', 38 * (d + 1))
.attr('dy', '0.3rem')
.style('user-select', 'none');
});

control_g.append('text').text('number').attr('x',350).attr('y', 10).style('user-select', 'none');
const bar_g = control_g.selectAll('g.highlight').data(labels).join('g').each(function(d) {
const g = d3.select(this);
let _data = { label: d, num: data.filter(dd=>dd.label===d).length, highlight: false }
g.append('rect')
.classed('bar', true)
.datum(_data)
.attr('fill', d=>color(d.label))
.attr('fill-opacity', 0.3)
.attr('stroke', d=>color(d.label))
.attr('stroke-width', 1)
.attr('x', 370)
.attr('y', d=>38 * (d.label + .75))
.attr('width', d=>5*d.num)
.attr('height', 38 * 0.5)
.on('mouseover', function(d) {
d3.select(this).attr('stroke-width', 3);
scatter.filter(dd=>dd.label === d.label).raise().attr('r', 8);
})
.on('mouseout', function(d) {
d3.select(this).attr('stroke-width', d.highlight ? 3 : 1);
scatter.filter(dd=>dd.label === d.label).attr('r', d.highlight ? 8 : 5);
})
.on('click', function(d) {
d.highlight = !d.highlight;
});
g.append('rect')
.classed('bar-dark', true)
.datum(_data)
.attr('fill', d=>color(d.label))
.attr('fill-opacity', 0.8)
.attr('stroke', d=>color(d.label))
.attr('stroke-width', 1)
.attr('x', 370)
.attr('y', d=>38 * (d.label + .75))
.attr('width', 0)
.attr('height', 38 * 0.5);
g.append('text')
.text(d)
.attr('x', 350)
.attr('y', 38 * (d + 1))
.attr('dy', '0.3rem')
.style('user-select', 'none');
});

const bar_highlight = function() {
const possible_item = scatter_g.selectAll('.possible');
const selected_item = scatter_g.selectAll('.selected');
d3.selectAll('rect.bar-dark')
.attr('width', d=>5*(possible_item.filter(dd => dd.label == d.label).size() + selected_item.filter(dd => dd.label == d.label).size()))
}


const lasso_start = function() {
console.log('start')
lasso.items()
.attr("r",5)
.classed("not_possible",true)
.classed("selected",false);
bar_highlight();
};

const lasso_draw = function() {
console.log('draw')
lasso.possibleItems()
.classed("not_possible",false)
.classed("possible",true)
.attr("r",8);
lasso.notPossibleItems()
.classed("not_possible",true)
.classed("possible",false)
.attr("r",5);
bar_highlight();
};

const lasso_end = function() {
console.log('end')
lasso.items()
.classed("not_possible",false)
.classed("possible",false);
lasso.selectedItems()
.classed("selected",true)
.attr("r",8);
lasso.notSelectedItems()
.attr("r",5);
bar_highlight();
};
const lasso = d3lasso.lasso()
.closePathDistance(305)
.closePathSelect(true)
.targetArea(scatter_g)
.items(scatter)
.on("start",lasso_start)
.on("draw",lasso_draw)
.on("end",lasso_end);
scatter_g.call(lasso);
return svg.node();
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more