function Chart() {
const margin = { left: 40, top: 20, bottom: 20, right: 20 };
let x = d3.scaleLinear(),
y = d3.scaleLinear(),
xAttr = "x",
yAttr = "y",
id = null,
width = 600,
height = 500,
onBrush = filtered => {
console.log("sel", filtered);
};
const brush = d3.brush();
function updateBrush(sel, data) {
function brushed(event) {
const selection = event.selection;
if (selection) {
const [[x0, y0], [x1, y1]] = selection;
const isSelected = d =>
x(d[xAttr]) >= x0 &&
x(d[xAttr]) <= x1 &&
y(d[yAttr]) >= y0 &&
y(d[yAttr]) <= y1;
const filteredData = data.filter(isSelected);
onBrush(filteredData);
sel
.selectAll(".item")
.style("fill", "steelblue")
.filter(isSelected)
.style("fill", "red");
}
}
brush
.extent([
[0, 0],
[
width - margin.left - margin.right,
height - margin.top - margin.bottom
]
])
.on("start brush end", brushed);
sel.select(".brush").call(brush);
}
// A good rule of thumb is that your appends should go here
const onEnter = enter => {
const svgEnter = enter.append("svg");
const g = svgEnter.append("g").attr("class", "gDrawing");
g.append("g")
.attr("class", "x-axis")
.append("text")
.attr("class", "axisLabel");
g.append("g")
.attr("class", "y-axis")
.append("text")
.attr("class", "axisLabel");
g.append("g").attr("class", "marks");
g.append("g").attr("class", "brush");
return svgEnter;
};
const onUpdate = update => update;
const onExit = exit => exit.remove();
function chart(sel) {
sel.each(data => {
const iwidth = width - margin.left - margin.right,
iheight = height - margin.top - margin.bottom;
const svg = sel
.selectAll("svg")
.data([data])
.join(onEnter, onUpdate, onExit)
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);
const g = svg
.select("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
x.domain(d3.extent(data, d => d[xAttr])).range([0, iwidth]);
y.domain(d3.extent(data, d => d[yAttr])).range([iheight, 0]);
updateBrush(sel, data);
// *** Transition ***
const t = g.transition().duration(750);
// Observable only, stop the animation if it isn't done and the cell is disposed
invalidation.then(() => svg.interrupt(t));
// ***** Axis ******
g.select(".x-axis")
.attr("transform", `translate(0, ${iheight})`)
.transition(t)
.call(d3.axisBottom(x).ticks(3));
g.select(".x-axis")
.select(".axisLabel")
.style("fill", "black")
.attr("transform", `translate(${iwidth}, -10)`)
.style("text-anchor", "end")
.text(xAttr);
g.select(".y-axis")
.transition(t)
.call(d3.axisLeft(y).ticks(3));
g.select(".y-axis")
.select(".axisLabel")
.style("fill", "black")
.style("text-anchor", "middle")
.attr("transform", `translate(0, -10)`)
.text(yAttr);
// ****** Marks ******
const moveItems = item =>
item.attr("cx", d => x(d[xAttr])).attr("cy", d => y(d[yAttr]));
g.select(".marks")
.selectAll(".item")
.data(data, (d, i) => (id !== null ? d[id] : i))
.join(
enter =>
enter
.append("circle")
.attr("class", "item")
.call(moveItems),
update => update.call(update => update.transition(t).call(moveItems))
)
.attr("r", 2)
.style("fill", "steelblue");
});
}
chart.x = function(_) {
if (!arguments.length) return xAttr;
xAttr = _;
return chart;
};
chart.y = function(_) {
if (!arguments.length) return yAttr;
yAttr = _;
return chart;
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
chart.id = function(_) {
if (!arguments.length) return id;
id = _;
return chart;
};
chart.onBrush = function(_) {
if (!arguments.length) return onBrush;
onBrush = _;
return chart;
};
return chart;
}