I'm trying to get mouse events to cooperate between different elements in a scatterplot. D3's brush component adds some listeners to the called element (e.g. svg.call(brush)). I also want to display points bound on the SVG, much like a scatterplot, and for those points to support mouseover events (for tooltips and other interactions).
A previous solution suggests drawing points before calling the brush, which supports mouseover on points while allowing the brush to be drawn and the extent modified. However, if the dragging motion for the brush starts upon a point (which I anticipate in very dense graphs), the brush component misbehaves when an extent is already active (translating the brush resizes the extent instead). You can try it out on this example, where the above suggested solution has been implemented.
I've narrowed the issue to how the event is handled in d3's brushstart() function, internal to the d3.svg.brush component. Here's what relevant variables look like when the brush is correctly working.
this eventTarget dragging resizing
-------------- ------------------------------------- ---------- ----------
Translating extent brush parent rect.extent true 0
Resizing extent brush parent rect (invisible rects for resizing) false e.g. "e"
Redrawing brush parent rect.background false 0
This is what it looks like currently, with the solution above:
this eventTarget dragging resizing
-------------------- -------------- ------------- ---------- ----------------
Translating extent brush parent circle false circle.datum()
Resizing extent brush parent circle false circle.datum()
Redrawing brush parent circle false circle.datum()
The real question is: how can I fudge the source of d3.event.target to match the first table? If I can do that, I can get the behavior I want. Thanks for any help!
If you've missed it, here's a bl.ock of this conundrum in action: http://bl.ocks.org/yelper/d38ddf461a0175ebd927946d15140947
Here's a quick hack which corrects the behavior:
.on('mousedown', function(d){
var e = brush.extent(),
m = d3.mouse(svg.node()), // pointer position with respect to g
p = [x.invert(m[0]), y.invert(m[1])]; // position in user space
if ( brush.empty() || // if there is no brush
(e[0][0] > d[0] || d[0] > e[1][0]
|| e[0][1] > d[1] || d[1] > e[1][1] ) // or our current circle is outside the bounds of the brush
) {
brush.extent([p,p]); // set brush to current position
} else {
d3.select(this).classed('extent', true); // else we are moving the brush, so fool d3 (I got this from looking at source code, it's how d3 determines a drag)
}
});
Working code below, updated block here.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.hidden {
opacity: 0.3;
}
.extent {
fill: #000;
fill-opacity: .125;
stroke: #fff;
}
</style>
<body>
<script src="//d3js.org/d3.v3.js"></script>
<script>
var margin = {top: 20, right: 50, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width])
.domain([0, 10]);
var y = d3.scale.linear()
.range([height, 0])
.domain([0, 10]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var curPt = d3.select('body')
.append('p')
.html("Current point: ")
.append('span')
.attr('id', 'curPt');
var svg = d3.select('body').insert('svg', 'p')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,'+height+')')
.call(xAxis);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
var brush = d3.svg.brush()
.x(x)
.y(y)
.on("brush", function() {
var e = brush.extent(),
c = svg.selectAll('circle');
c.classed('extent', false);
c.classed('hidden', function(d) {
return e[0][0] > d[0] || d[0] > e[1][0]
|| e[0][1] > d[1] || d[1] > e[1][1];
}
);
})
.on("brushend", function() {
if (brush.empty()) svg.selectAll('circle').classed('hidden', false);
});
svg.call(brush);
var data = d3.range(50).map(function() { return [Math.random() * 10, Math.random() * 10]; });
svg.append('g')
.attr('class', 'points')
.selectAll('circle')
.data(data).enter()
.append('circle')
.attr('cx', function(d) { return x(d[0]); })
.attr('cy', function(d) { return y(d[1]); })
.attr('r', 10)
.style('fill', 'steelblue')
.on('mouseover', function(d) {
curPt.html("[" + d[0] + ", " + d[1] + "]");
})
.on('mouseout', function(d) {
curPt.html("");
})
.on('mousedown', function(d){
var e = brush.extent(),
m = d3.mouse(svg.node()),
p = [x.invert(m[0]), y.invert(m[1])];
if ( brush.empty() ||
(e[0][0] > d[0] || d[0] > e[1][0]
|| e[0][1] > d[1] || d[1] > e[1][1] )
) {
brush.extent([p,p]);
} else {
d3.select(this).classed('extent', true);
}
});
</script>
Here is the working example:
https://jsfiddle.net/paradite/rpqusqdc/2/
Basically I used my previously coded range selection tool using drag event instead of brush: http://bl.ocks.org/paradite/71869a0f30592ade5246
It does not interfere with your circles. So you just need to get the current rect dimensions and update your circles accordingly:
// select your points here
var e = selectionRect.getCurrentAttributes();
svg.selectAll('circle').classed('hidden', function(d) {
return e.x1 > x(d[0]) || x(d[0]) > e.x2 || e.y1 > y(d[1]) || y(d[1]) > e.y2;
});
Of course you can remove parts of its logic as much of it is not necessary for your case.
Related
I have trouble updating the dots and axis of the scatter plot, and corresponding bar chart in the d3 visualization when I select a different attribute for the x-axis or y-axis. As shown in the image.
I reused the code from this example, made some changes to add a drop-down menu for both axis. When changing the axis from the drop-down it adds correct data points but won't remove the previous ones. I need help updating and removing the previous elements if the x-axis or y-axis are changed from the drop-down menu.
<!--
An example of linked views using model.js.
Curran Kelleher August 2014
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<!-- A functional reactive model library. github.com/curran/model -->
<script src="https://curran.github.io/model/cdn/model-v0.2.4.js"></script>
<style>
/* CSS for the visualization.
* Curran Kelleher 4/17/2014 */
/* Size the visualization container. */
#container {
position: fixed;
top: 30px;
bottom: 30px;
left: 30px;
right: 30px;
}
/* Style the visualization.
* This CSS is copied verbatim from the
* D3 scatter plot example found at
* http://bl.ocks.org/mbostock/3887118 */
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
fill: black;
}
/* The following CSS is for brushing,
* adapted from http://bl.ocks.org/mbostock/4343214 */
.dot.selected {
fill: red;
}
.brush .extent {
stroke: gray;
fill-opacity: .125;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<div id="container"></div>
<div id = "axisSelectors">
<b>x-axis:</b>
<select id="xSelector" onchange="onCategoryChanged()">
<option value="sepalWidth">sepalWidth</option>
<option value="petalWidth">petalWidth</option>
</select>
<b>y-axis:</b>
<select id="ySelector" onchange="onCategoryChanged()">
<option value="sepalLength">sepalLength</option>
<option value="petalLength">petalLength</option>
</select>
</div>
<script>
function onCategoryChanged() {
var select = d3.select('#xSelector').node();
// Get current value of select element
var x_category = select.options[select.selectedIndex].value;
// Update chart with the selected category of letters
var select = d3.select('#ySelector').node();
var y_category = select.options[select.selectedIndex].value;
main(x_category,y_category);
}
function BarChart (container) {
var defaults = {
margin: {
top: 20,
right: 20,
bottom: 30,
left: 40
},
yAxisNumTicks: 10,
yAxisTickFormat: ""
},
model = Model(defaults),
xAxis = d3.svg.axis().orient("bottom"),
yAxis = d3.svg.axis().orient("left")
svg = d3.select(container).append('svg')
// Use absolute positioning on the SVG element
// so that CSS can be used to position the div later
// according to the model `box.x` and `box.y` properties.
.style('position', 'absolute'),
g = svg.append("g"),
xAxisG = g.append("g").attr("class", "x axis"),
yAxisG = g.append("g").attr("class", "y axis"),
yAxisText = yAxisG.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end");
// Encapsulate D3 Conventional Margins.
// See also http://bl.ocks.org/mbostock/3019563
model.when(["box", "margin"], function (box, margin) {
model.width = box.width - margin.left - margin.right,
model.height = box.height - margin.top - margin.bottom;
});
model.when("margin", function (margin) {
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
});
// Adjust Y axis tick mark parameters.
// See https://github.com/mbostock/d3/wiki/Quantitative-Scales#linear_tickFormat
model.when(['yAxisNumTicks', 'yAxisTickFormat'], function (count, format) {
yAxis.ticks(count, format);
});
// Respond to changes in size and offset.
model.when("box", function (box) {
// Resize the svg element that contains the visualization.
svg.attr("width", box.width).attr("height", box.height);
// Set the CSS `left` and `top` properties
// to move the SVG element to `(box.x, box.y)`
// relative to the container div to apply the offset.
svg
.style('left', box.x + 'px')
.style('top', box.y + 'px');
});
model.when("height", function (height) {
xAxisG.attr("transform", "translate(0," + height + ")");
});
model.when(["data", "xAttribute", "width"], function (data, xAttribute, width) {
model.xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(data.map(function(d) { return d[xAttribute]; }));
});
model.when(["data", "yAttribute", "height"], function (data, yAttribute, height) {
model.yScale = d3.scale.linear()
.range([height, 0])
.domain([0, d3.max(data, function(d) { return d[yAttribute]; })]);
});
model.when(["xScale"], function (xScale) {
xAxis.scale(xScale)
xAxisG.call(xAxis);
});
model.when(["yScale"], function (yScale) {
yAxis.scale(yScale)
yAxisG.call(yAxis);
});
model.when("yAxisLabel", yAxisText.text, yAxisText);
model.when(["data", "xAttribute", "yAttribute", "xScale", "yScale", "height"],
function (data, xAttribute, yAttribute, xScale, yScale, height) {
var bars = g.selectAll(".bar").data(data);
bars.enter().append("rect").attr("class", "bar");
bars
.attr("x", function(d) { return xScale(d[xAttribute]); })
.attr("width", xScale.rangeBand())
.attr("y", function(d) { return yScale(d[yAttribute]); })
.attr("height", function(d) { return height - yScale(d[yAttribute]); });
bars.exit().remove();
});
return model;
}
// An adaptation of the [D3 scatter plot example](http://bl.ocks.org/mbostock/3887118)
// that uses `model.js`. This version, unlike the original example,
// is model driven and reactive. When a part of the model updates,
// only the parts of the visualization that depend on those parts
// of the model are updated. There are no redundant calls to visualization
// update code when multiple properties are changed simultaneously.
//
// Draws from this brushing example for interaction:
// http://bl.ocks.org/mbostock/4343214
//
// See also docs on quadtree:
// https://github.com/mbostock/d3/wiki/Quadtree-Geom
//
// Define the AMD module using the `define()` function
// provided by Require.js.
//define(['d3', 'model'], function (d3, Model) {
function ScatterPlot (div){
var x = d3.scale.linear(),
y = d3.scale.linear(),
xAxis = d3.svg.axis().scale(x).orient('bottom'),
yAxis = d3.svg.axis().scale(y).orient('left'),
// Use absolute positioning so that CSS can be used
// to position the div according to the model.
svg = d3.select(div).append('svg').style('position', 'absolute'),
g = svg.append('g'),
xAxisG = g.append('g').attr('class', 'x axis'),
yAxisG = g.append('g').attr('class', 'y axis'),
xAxisLabel = xAxisG.append('text')
.attr('class', 'label')
.attr('y', -6)
.style('text-anchor', 'end'),
yAxisLabel = yAxisG.append('text')
.attr('class', 'label')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end'),
// Add the dots group before the brush group,
// so that mouse events go to the brush
// rather than to the dots, even when the mouse is
// on top of a dot.
dotsG = g.append('g'),
brushG = g.append('g')
.attr('class', 'brush'),
brush = d3.svg.brush()
.on('brush', brushed),
dots,
quadtree,
model = Model();
model.when('xLabel', xAxisLabel.text, xAxisLabel);
model.when('yLabel', yAxisLabel.text, yAxisLabel);
model.when('margin', function (margin) {
g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
});
model.when('box', function (box) {
svg.attr('width', box.width)
.attr('height', box.height);
// Set the CSS `left` and `top` properties
// to move the SVG element to `(box.x, box.y)`
// relative to the container div.
svg
.style('left', box.x + 'px')
.style('top', box.y + 'px')
});
model.when(['box', 'margin'], function (box, margin) {
model.width = box.width - margin.left - margin.right;
model.height = box.height - margin.top - margin.bottom;
});
model.when('width', function (width) {
xAxisLabel.attr('x', width);
});
model.when('height', function (height) {
xAxisG.attr('transform', 'translate(0,' + height + ')');
});
model.when(['width', 'height'], function (width, height) {
brush.x(d3.scale.identity().domain([0, width]));
brush.y(d3.scale.identity().domain([0, height]));
brushG
.call(brush)
.call(brush.event);
});
model.when(['width', 'height', 'data', 'xField', 'yField'], function (width, height, data, xField, yField) {
// Updated the scales
x.domain(d3.extent(data, function(d) { return d[xField]; })).nice();
y.domain(d3.extent(data, function(d) { return d[yField]; })).nice();
x.range([0, width]);
y.range([height, 0]);
// update the quadtree
quadtree = d3.geom.quadtree()
.x(function(d) { return x(d[xField]); })
.y(function(d) { return y(d[yField]); })
(data);
// update the axes
xAxisG.call(xAxis);
yAxisG.call(yAxis);
// Plot the data as dots
dots = dotsG.selectAll('.dot').data(data);
dots.enter().append('circle')
.attr('class', 'dot')
.attr('r', 3.5);
dots
.attr('cx', function(d) { return x(d[xField]); })
.attr('cy', function(d) { return y(d[yField]); });
dots.exit().remove();
});
return model;
function brushed() {
var e = brush.extent(), selectedData;
if(dots) {
dots.each(function(d) { d.selected = false; });
selectedData = search(e[0][0], e[0][1], e[1][0], e[1][1]);
dots.classed('selected', function(d) { return d.selected; });
}
model.selectedData = brush.empty() ? model.data : selectedData;
}
// Find the nodes within the specified rectangle.
function search(x0, y0, x3, y3) {
var selectedData = [];
quadtree.visit(function(node, x1, y1, x2, y2) {
var d = node.point, x, y;
if (d) {
x = node.x;
y = node.y;
d.visited = true;
if(x >= x0 && x < x3 && y >= y0 && y < y3){
d.selected = true;
selectedData.push(d);
}
}
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
});
return selectedData;
}
}
// The main program that assembles the linked views.
//
// Curran Kelleher 4/17/2014
//require(['d3', 'scatterPlot', 'barChart'], function (d3, ScatterPlot, BarChart) {
function main(xvar,yvar){
// Grab the container div from the DOM.
var div = document.getElementById('container'),
// Add both visualizations to the same div.
// Each will create its own SVG element.
scatterPlot = ScatterPlot(div),
barChart = BarChart(div);
// Configure the scatter plot to use the iris data.
scatterPlot.set({
xField: xvar,
yField: yvar,
xLabel: xvar,
yLabel: yvar,
margin: { 'top': 20, 'right': 20, 'bottom': 30, 'left': 40 }
});
// Configure the bar chart to use the aggregated iris data.
barChart.set({
xAttribute: 'species',
yAttribute: 'count',
yAxisLabel: 'number of irises',
margin: { 'top': 20, 'right': 20, 'bottom': 30, 'left': 40 }
});
// Compute the aggregated iris data in response to brushing
// in the scatter plot, and pass it into the bar chart.
scatterPlot.when('selectedData', function (scatterData) {
var speciesCounts = {};
// Aggregate scatter plot data by counting
// the number of irises for each species.
scatterData.forEach(function (d) {
if(!speciesCounts[d.species]){
speciesCounts[d.species] = 0;
}
speciesCounts[d.species]++;
});
// Flatten the object containing species counts into an array.
// Update the bar chart with the aggregated data.
barChart.data = Object.keys(speciesCounts).map(function (species) {
return {
species: species,
count: speciesCounts[species]
};
});
});
// Load the iris data.
d3.tsv('data.tsv', function (d) {
d.sepalLength = +d.sepalLength;
d.sepalWidth = +d.sepalWidth;
d.petalLength = +d.petalLength;
d.petalWidth = +d.petalWidth;
return d;
}, function(error, data) {
// Set sizes once to initialize.
setSizes();
// Set sizes when the user resizes the browser.
window.addEventListener('resize', setSizes);
// Set the data.
scatterPlot.data = data;
});
// Sets the `box` property on each visualization
// to arrange them within the container div.
function setSizes(){
// Put the scatter plot on the left.
scatterPlot.box = {
x: 0,
y: 0,
width: div.clientWidth / 2,
height: div.clientHeight
};
// Put the bar chart on the right.
barChart.box = {
x: div.clientWidth / 2,
y: 0,
width: div.clientWidth / 2,
height: div.clientHeight
};
}
}
main('sepalWidth','sepalLength');
</script>
</body>
</html>
I have a function where that when a button is pressed (Several buttons the represent several animal types), that animal types SVG is updated with its corresponding data. I'm trying to replicate this zoom function but am having issues implementing it with my code. There are several SVGs that are used globally like this (one for each animal type):
let x = d3.scaleLinear()
.domain([0, 1000])
.range([ 0, width ]);
var xAxis = d3.axisBottom(x);
svgReptile.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
const yAxis = d3.scaleLinear()
.domain([0, 220])
.range([ height, 0])
svgReptile.append("g")
.call(d3.axisLeft(yAxis))
The function below is called when one of the animal buttons is pressed.
function update(animal, whatSVG, xAxis, yAxis, color) {
const points = whatSVG
.selectAll("circle")
.data(data);
points.enter()
.append("circle")
.attr("cx", function(d) {
return xAxis(d.state);
})
.attr("cy", function(d) {
return yAxis(d.percentage);
})
.merge(points)
.attr("r", 3)
.attr("cx", function(d) {
return xAxis(d.decade)
})
.attr("cy", function(d) {
return yAxis(d.count)
})
.style("fill", function (d) { return colour(d.animal) } );
points.exit()
.attr('r', 0)
.remove();
}
Question:
How can I implement a zoom feature that expands the x-axis when zoomed (or anything similar) like the one linked above?
I think you're looking for a 'brush zoom' from the last line of your question.
The following source code if from an example in a d3 graph gallery
The cross hair allows you to select an area to expand. If you follow the link there is a graph above it that is entitled "Zoom with axis" but it doesn't zoom in the way you've described, it just moves the axis, but doesn't enlarge the graph contents with it. Perhaps both will be useful!
Hope this helps
// set the dimensions and margins of the graph
var margin = {top: 10, right: 20, bottom: 20, left: 20},
width = 500 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var Svg = d3.select("#brushZoom")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/iris.csv", function(data) {
// Add X axis
var x = d3.scaleLinear()
.domain([4, 8])
.range([ 0, width ]);
var xAxis = Svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, 9])
.range([ height, 0]);
Svg.append("g")
.call(d3.axisLeft(y));
// Add a clipPath: everything out of this area won't be drawn.
var clip = Svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
// Color scale: give me a specie name, I return a color
var color = d3.scaleOrdinal()
.domain(["setosa", "versicolor", "virginica" ])
.range([ "#440154ff", "#21908dff", "#fde725ff"])
// Add brushing
var brush = d3.brushX() // Add the brush feature using the d3.brush function
.extent( [ [0,0], [width,height] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
.on("end", updateChart) // Each time the brush selection changes, trigger the 'updateChart' function
// Create the scatter variable: where both the circles and the brush take place
var scatter = Svg.append('g')
.attr("clip-path", "url(#clip)")
// Add circles
scatter
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.Sepal_Length); } )
.attr("cy", function (d) { return y(d.Petal_Length); } )
.attr("r", 8)
.style("fill", function (d) { return color(d.Species) } )
.style("opacity", 0.5)
// Add the brushing
scatter
.append("g")
.attr("class", "brush")
.call(brush);
// A function that set idleTimeOut to null
var idleTimeout
function idled() { idleTimeout = null; }
// A function that update the chart for given boundaries
function updateChart() {
extent = d3.event.selection
// If no selection, back to initial coordinate. Otherwise, update X axis domain
if(!extent){
if (!idleTimeout) return idleTimeout = setTimeout(idled, 350); // This allows to wait a little bit
x.domain([ 4,8])
}else{
x.domain([ x.invert(extent[0]), x.invert(extent[1]) ])
scatter.select(".brush").call(brush.move, null) // This remove the grey brush area as soon as the selection has been done
}
// Update axis and circle position
xAxis.transition().duration(1000).call(d3.axisBottom(x))
scatter
.selectAll("circle")
.transition().duration(1000)
.attr("cx", function (d) { return x(d.Sepal_Length); } )
.attr("cy", function (d) { return y(d.Petal_Length); } )
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="brushZoom"></div>
There are grid lines from points.
Is there another solution with better performance, because if I add many svg elements(etc. rects, circles, paths) and increase the dimension of the grid I will see the freeze effect when I use zoom, move element...
The size of the grid is changed.
Also, how can I create endless grid lines, instead limited (gridCountX, gridCountY)?
Thanks
var svg = d3.select("body").append("svg");
var svgG = svg.append("g");
var gridLines = svgG.append("g").classed("grid-lines-container", true).data(["gridLines"]);
var gridCountX = _.range(100);
var gridCountY = _.range(100);
var size = 10;
gridLines.selectAll("g").data(gridCountY)
.enter()
.append("g")
.each(function(d) {
d3.select(this).selectAll("circle").data(gridCountX).enter()
.append("circle")
.attr("cx", function(_d) {return _d*size;})
.attr("cy", function(_d) {return d*size;})
.attr("r", 0.5)
.attr("style", function() {
return "stroke: black;";
});
});
var zoomSvg = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", function(){
svgG.attr("transform", d3.event.transform);
});
svg.call(zoomSvg);
svg {
width: 100%;
height: 100%;
border: 1px solid #a1a1a1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
As you note, this approach is not really scalable and has a larger impact on performance. I have found the approach of utilizing d3 axes for grids to have minimal performance impact while also being relatively straightforward to incorporate with zoom such that you can have infinite zoom with the grid lines updating in a sensible manner due to the "magic" of automatic generation of sensible tick locations in d3.
To implement something similar in d3 v4, you can do something along these lines:
var svg = d3.select("svg"),
margin = {top: 20, right: 140, bottom: 50, left: 70},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"),
innerSvg = g.append("svg").attr("width", width).attr("height", height);
// Calculate domain for x and y from data and store in x0, y0 (not shown here)
x.domain(x0);
y.domain(y0);
xGridAxis = d3.axisBottom(x).ticks(10);
yGridAxis = d3.axisLeft(y).ticks(10 * height / width);
// Create grouping and additional set of axes for displaying grid
innerSvg.append("g")
.attr("class", "grid x-grid")
.attr("transform", "translate (0," + height + ")")
.call(xGridAxis
.tickSize(-height, 0, 0)
.tickFormat("")
)
.selectAll(".tick");
innerSvg.append("g")
.attr("class", "grid y-grid")
.attr("transform", "translate (" + width + ", 0)")
.call(yGridAxis
.tickSize(width)
.tickFormat("")
);
// Add element to capture mouse events for drag and pan of plots
var zoom = d3.zoom()
.on("zoom", zoomed);
var scrollZoom = innerSvg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "all") // Defaults to panning with mouse
.call(zoom);
// Mouse panning and scroll-zoom implementation using d3.zoom
// Modification of : http://bl.ocks.org/lorenzopub/013c0c41f9ffab4d27f860127f79c5f5
function zoomed() {
lastEventTransform = d3.event.transform;
// Rescale the grid using the new transform associated with zoom/pan action
svg.select(".x-grid").call(xGridAxis.scale(lastEventTransform.rescaleX(x)));
svg.select(".y-grid").call(yGridAxis.scale(lastEventTransform.rescaleY(y)));
// Calculate transformed x and y locations which are used to redraw all plot elements
var xt = lastEventTransform.rescaleX(x),
yt = lastEventTransform.rescaleY(y);
// Code below just shows how you might do it. Will need to tweak based on your plot
var line = d3.line()
.x(function(d) { return xt(d.x); })
.y(function(d) { return yt(d.y); });
innerSvg.selectAll(".line")
.attr("d", function(d) { return line(d.values); });
innerSvg.selectAll(".dot")
.attr("cx", function(d) {return xt(d.x); })
.attr("cy", function(d) {return yt(d.y); });
}
Here is a worked out example in d3 v4 that inspired my version above:
http://bl.ocks.org/lorenzopub/013c0c41f9ffab4d27f860127f79c5f5
I'm using D3 to generate a bar chart (I adapted the code from this example). The labels I'm using on the x-axis are a couple of words long each, and since this makes all of the labels overlap I need to break these labels across lines. (It'll be fine if I can replace all of the spaces in each label with newlines.)
I originally tried this by replacing the spaces with literal newlines (
) and setting xml:space="preserve" on the labels' <text> elements. Unfortunately, it turns out that SVG doesn't respect this property. Next I tried to wrap each word in a <tspan> that I could later style. I passed each label through this function:
function (text) {
return '<tspan>' + text.replace(/ /g, '</tspan><tspan>') + '</tspan>';
}
but this just puts literal <tspan>s into the output. How can I wrap my text labels in tspans (or do something else) so that my labels don't overlap?
I ended up using the following code to break each x-axis label across lines:
var insertLinebreaks = function (d) {
var el = d3.select(this);
var words = d.split(' ');
el.text('');
for (var i = 0; i < words.length; i++) {
var tspan = el.append('tspan').text(words[i]);
if (i > 0)
tspan.attr('x', 0).attr('dy', '15');
}
};
svg.selectAll('g.x.axis g text').each(insertLinebreaks);
Note that this assumes that the labels have already been created. (If you follow the canonical histogram example then the labels will have been set up in just the way you need.) There also isn't any real line-breaking logic present; the function converts every space into a newline. This fits my purposes fine but you may need to edit the split() line to be smarter about how it partitions the parts of the string into lines.
SVG text element does not support text-wrapping, so there are two options:
split the text into multiple SVG text elements
use an overlay HTML div on top of the SVG
See Mike Bostock's comment on this here.
Something I've found to be useful is using a 'foreignObject' tag instead of text or tspan elements. This allows for the simple embedding of HTML, allowing for words to break naturally. The caveat being the overall dimensions of the object meeting specific needs:
var myLabel = svg.append('foreignObject')
.attr({
height: 50,
width: 100, // dimensions determined based on need
transform: 'translate(0,0)' // put it where you want it...
})
.html('<div class"style-me"><p>My label or other text</p></div>');
Whatever elements you place inside of this object can later be obtained using d3.select/selectAll to update text values dynamically as well.
Having looked around I found that Mike Bostock has provided a solution enabling you to wrap text round.
http://bl.ocks.org/mbostock/7555321
To implement it on my code (I'm using collapsed tree diagram). I simply copied the "wrap" method.
Then appended the following
// Standard code for a node
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.text(function(d) { return d.text; })
// New added line to call the function to wrap after a given width
.call(wrap, 40);
I don't see any reason this should not work for a force-directed, bar or any other pattern
Amendment :
I've modified the wrap function to the following for anyone who reads this and is using collapisible graph. The change in the "x" attribute sets the allignment correctly, incrementing linenumber was performed on a separate line as issues were noted in the original code and "y" has been hard set to zero otherwise issues would occur in which the line spacing increased with each line.
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
lineHeight = 1.1, // ems
tspan = text.text(null).append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
var textWidth = tspan.node().getComputedTextLength();
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
++lineNumber;
tspan = text.append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", 0).attr("dy", lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
There's also this answer on wrapping long labels.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.title {
font: bold 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 80, right: 180, bottom: 80, left: 180},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1, .3);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(8, "%");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.tsv("data.tsv", type, function(error, data) {
x.domain(data.map(function(d) { return d.name; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
svg.append("text")
.attr("class", "title")
.attr("x", x(data[0].name))
.attr("y", -26)
.text("Why Are We Leaving Facebook?");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll(".tick text")
.call(wrap, x.rangeBand());
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.name); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); });
});
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
function type(d) {
d.value = +d.value;
return d;
}
</script>
and the data file "data.tsv":
name value
Family in feud with Zuckerbergs .17
Committed 671 birthdays to memory .19
Ex is doing too well .10
High school friends all dead now .15
Discovered how to “like” things mentally .27
Not enough politics .12
use <tspan>
and in nv.d3
nv.models.axis = function() {
...
.select('text')
.attr('dy', '0em')
.attr('y', -axis.tickPadding())
.attr('text-anchor', 'middle')
.text(function(d,i) {
var v = fmt(d);
return ('' + v).match('NaN') ? '' : v;
});
change all occurrences of .text( to .html(
I'm using D3 to generate a bar chart (I adapted the code from this example). The labels I'm using on the x-axis are a couple of words long each, and since this makes all of the labels overlap I need to break these labels across lines. (It'll be fine if I can replace all of the spaces in each label with newlines.)
I originally tried this by replacing the spaces with literal newlines (
) and setting xml:space="preserve" on the labels' <text> elements. Unfortunately, it turns out that SVG doesn't respect this property. Next I tried to wrap each word in a <tspan> that I could later style. I passed each label through this function:
function (text) {
return '<tspan>' + text.replace(/ /g, '</tspan><tspan>') + '</tspan>';
}
but this just puts literal <tspan>s into the output. How can I wrap my text labels in tspans (or do something else) so that my labels don't overlap?
I ended up using the following code to break each x-axis label across lines:
var insertLinebreaks = function (d) {
var el = d3.select(this);
var words = d.split(' ');
el.text('');
for (var i = 0; i < words.length; i++) {
var tspan = el.append('tspan').text(words[i]);
if (i > 0)
tspan.attr('x', 0).attr('dy', '15');
}
};
svg.selectAll('g.x.axis g text').each(insertLinebreaks);
Note that this assumes that the labels have already been created. (If you follow the canonical histogram example then the labels will have been set up in just the way you need.) There also isn't any real line-breaking logic present; the function converts every space into a newline. This fits my purposes fine but you may need to edit the split() line to be smarter about how it partitions the parts of the string into lines.
SVG text element does not support text-wrapping, so there are two options:
split the text into multiple SVG text elements
use an overlay HTML div on top of the SVG
See Mike Bostock's comment on this here.
Something I've found to be useful is using a 'foreignObject' tag instead of text or tspan elements. This allows for the simple embedding of HTML, allowing for words to break naturally. The caveat being the overall dimensions of the object meeting specific needs:
var myLabel = svg.append('foreignObject')
.attr({
height: 50,
width: 100, // dimensions determined based on need
transform: 'translate(0,0)' // put it where you want it...
})
.html('<div class"style-me"><p>My label or other text</p></div>');
Whatever elements you place inside of this object can later be obtained using d3.select/selectAll to update text values dynamically as well.
Having looked around I found that Mike Bostock has provided a solution enabling you to wrap text round.
http://bl.ocks.org/mbostock/7555321
To implement it on my code (I'm using collapsed tree diagram). I simply copied the "wrap" method.
Then appended the following
// Standard code for a node
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.text(function(d) { return d.text; })
// New added line to call the function to wrap after a given width
.call(wrap, 40);
I don't see any reason this should not work for a force-directed, bar or any other pattern
Amendment :
I've modified the wrap function to the following for anyone who reads this and is using collapisible graph. The change in the "x" attribute sets the allignment correctly, incrementing linenumber was performed on a separate line as issues were noted in the original code and "y" has been hard set to zero otherwise issues would occur in which the line spacing increased with each line.
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
lineHeight = 1.1, // ems
tspan = text.text(null).append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
var textWidth = tspan.node().getComputedTextLength();
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
++lineNumber;
tspan = text.append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", 0).attr("dy", lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
There's also this answer on wrapping long labels.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.title {
font: bold 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 80, right: 180, bottom: 80, left: 180},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1, .3);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(8, "%");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.tsv("data.tsv", type, function(error, data) {
x.domain(data.map(function(d) { return d.name; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
svg.append("text")
.attr("class", "title")
.attr("x", x(data[0].name))
.attr("y", -26)
.text("Why Are We Leaving Facebook?");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll(".tick text")
.call(wrap, x.rangeBand());
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.name); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); });
});
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
function type(d) {
d.value = +d.value;
return d;
}
</script>
and the data file "data.tsv":
name value
Family in feud with Zuckerbergs .17
Committed 671 birthdays to memory .19
Ex is doing too well .10
High school friends all dead now .15
Discovered how to “like” things mentally .27
Not enough politics .12
use <tspan>
and in nv.d3
nv.models.axis = function() {
...
.select('text')
.attr('dy', '0em')
.attr('y', -axis.tickPadding())
.attr('text-anchor', 'middle')
.text(function(d,i) {
var v = fmt(d);
return ('' + v).match('NaN') ? '' : v;
});
change all occurrences of .text( to .html(