I'm looking for some times now to allow zooming into one or the other (or both) directions X/Y on a d3.js chart. Here is my simple chart :
var margin = parseInt(attrs.margin) || {top: 20, right: 20, bottom: 20, left: 20},
padding = parseInt(attrs.padding) || 30;
var svg = d3.select(ele[0]).append('svg')
.style('width', '100%');
var width = 960 - margin.left - margin.right - padding,
height = 500 - margin.top - margin.bottom;
// X scale
var x = d3.scale.linear().domain([d3.min(data, function(d){return d.x;}), d3.max(data, function(d){return d.x})]).range([0, width]);
// Y scale
var y = d3.scale.linear().domain([0, d3.max(data, function(d){return d.y;})]).range([height, 0]);
svg.call(d3.behavior.zoom().x(x).y(x).on("zoom", zoomed));
// create a line function that can convert data[] into x and y points
var line = d3.svg.line()
.interpolate("basis")
// assign the X function to plot our line as we wish
.x(function(d,i) { return x(d.x); })
.y(function(d) { return y(d.y); });
// Add an SVG element with the desired dimensions and margin.
svg.attr("width", width + margin.left + margin.right + padding)
.attr("height", height + margin.top + margin.bottom);
var g = svg.append("g").attr("transform", "translate(60," + margin.top + ")");
// create Axis
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
// Add the x-axis.
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(xAxis);
// Add the y-axis to the left
g.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + 0 +",0)")
.call(yAxis);
// objects for the zooming clipping
var clip = g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
var chartBody = g.append("g")
.attr("clip-path", "url(#clip)");
chartBody.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
function zoomed() {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
svg.select(".line")
.attr("class", "line")
.attr("d", line);
}
Is there a way to use a key modifier when zooming (for example when shift+mouse wheel, only zoom X) ?
Related
I am new to D3.js. I am stuck of the following concepts:
I couldn't find examples where this is done in D3.js V4 and I am not sure how to navigate it.
To limit the zoom from going beyond zero I would like to use the minimum of the zoom as ZERO. I am not sure how to do this in scatter plot.
To avoid the zoomed points touching the y and z axis. I would like the points to fade or disappear when it touches the axis areas.
Here is my code
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 750 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var xMax = d3.max(graphdata, function(d) { return d["x"]; }),
yMax = d3.max(graphdata, function(d) { return d["y"]; });
var xScale = d3.scaleLinear()
.range([0, width])
.domain([0, xMax]).nice();
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, yMax]).nice();
var xAxis = d3.axisBottom()
.scale(xScale);
var yTicks = 5
var yAxis = d3.axisLeft()
.scale(yScale);
var svg = d3.select("#plotspace").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("id", "plot")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// create a clipping region
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var gX = svg.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'x axis')
.call(xAxis);
var gY= svg.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'y axis')
.call(yAxis
);
var bubble = svg.selectAll('.bubble')
.data(graphdata)
.enter().append('path')
.attr('class', 'bubble')
.attr("d", d3.symbol().type(d3.symbolCircle).size(30))
.attr("transform", function(d) { return "translate(" + xScale(d["x"]) + "," + yScale(d["y"]) + ")"; })
.attr('r', 3.5 )
.attr('fill-opacity',0.7)
.style('fill','blue');
bubble.append('title')
.attr('x', 3.5 )
.text(keys[0]);
// Pan and zoom
var zoom = d3.zoom()
.scaleExtent([.5, 20])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(zoom);
function zoomed() {
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
gX.call(xAxis.scale(new_xScale));
gY.call(yAxis.scale(new_yScale));
bubble.data(graphdata)
.attr("transform", function(d) { return "translate(" + new_xScale(d["x"]) + "," + new_yScale(d["y"]) + ")"; })
}
Your first issue, negative numbers, is a result of allowing a zoom out from the initial zoom state. If the scales already hold all the data (since you dynamically create the scales), you should never have to zoom out from this zoom level. Zooming out from the initial zoom creates a plot area greater than the translate extent, this is causing negative values to appear in the scale. Try:
zoom.scaleExtent([1,4]);
That fixes the negative numbers, but you can still have overflow within those translate extents because you aren't using a clip path correctly.
You currently use one g called svg to plot points and draw axes, but you don't want to apply a clip area to this g, as the axes are outside of where you wish to draw the points. Instead, you could create a new g for the points only, and apply the plot area to that g with g.attr('clip-path','url(#id)');. Below I call that g plotArea and demonstrate these two changes:
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 750 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var graphdata = d3.range(200).map(function(d) {
return {x: d3.randomLogNormal()(), y: d3.randomLogNormal()()}
})
var xMax = d3.max(graphdata, function(d) { return d["x"]; }),
yMax = d3.max(graphdata, function(d) { return d["y"]; });
var xScale = d3.scaleLinear()
.range([0, width])
.domain([0, xMax]).nice();
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, yMax]);
var xAxis = d3.axisBottom()
.scale(xScale);
var yTicks = 5
var yAxis = d3.axisLeft()
.scale(yScale);
var svg = d3.select("#plotspace").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("id", "plot")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// create a clipping region
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var plotArea = svg.append("g") // we don't want to clip the axes.
.attr("clip-path","url(#clip)");
var gX = svg.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'x axis')
.call(xAxis);
var gY= svg.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'y axis')
.call(yAxis
);
var bubble = plotArea.selectAll('.bubble') // add to clipped area.
.data(graphdata)
.enter().append('path')
.attr('class', 'bubble')
.attr("d", d3.symbol().type(d3.symbolCircle).size(30))
.attr("transform", function(d) { return "translate(" + xScale(d["x"]) + "," + yScale(d["y"]) + ")"; })
.attr('r', 3.5 )
.attr('fill-opacity',0.7)
.style('fill','blue')
// Pan and zoom
var zoom = d3.zoom()
.scaleExtent([1, 20])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
function zoomed() {
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
gX.call(xAxis.scale(new_xScale));
gY.call(yAxis.scale(new_yScale));
bubble.data(graphdata)
.attr("transform", function(d) { return "translate(" + new_xScale(d["x"]) + "," + new_yScale(d["y"]) + ")"; })
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="plotspace"></div>
I am using D3.js to create a simple line graph.
<div>
<h6>Price Over Time</h6>
<div id="priceOverTimeChart"></div>
</div>
// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 800 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%d-%b-%y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(10);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(10);
// Define the line
var valueline = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// Adds the svg canvas
var svg = d3.select("#priceOverTimeChart")
.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 + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
The data for the line graph uses the following data format:
26-Apr-12,0.048
25-Apr-12,0.048
24-Apr-12,0.048
I would like to add an optional string to each record so it looks like:
26-Apr-12,0.048, "product 1 launch",
25-Apr-12,0.048, "product 2",
24-Apr-12,0.048, "product 3"
26-Apr-12,0.048, null
25-Apr-12,0.048, null
24-Apr-12,0.048, null
The graph would then look something like this with the labels on it:
Graph with optional labels
How can I accomplish this? Thanks in advance!
Appending texts to the corresponding x, y position will do the trick.
Please refer this working JS Fiddle
svg.append("g").selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", function(d) { return x(d.date) - paddingForText })
.attr("y", function(d) { return y(d.close) + paddingForText })
.attr("fill", "red")
.text(function(d) { return d.notes });
I'm messing around with d3 but a basic line seems to be inverted. Seems like the origin is at the top of the page (like the default orientation on a page). However, i assumed that the d3 points were relative to the svg graph.
How can I set the origin to be bottom left of graph? (Without transforming the data)
// make dataset
var dataset = [[1,1]];
for (var x = 0; x< 10000; x +=1) {
var y = x*x;
dataset.push([x, y])
}
// set graph dims
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// Adds the svg canvas
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");;
// add scale
var x = d3.scale.linear().domain([0, 200]).range([0, width]);
var y = d3.scale.linear().domain([0, 1000]).range([height, 0]);
// add x axis
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(10);
// add y axis
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(10);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height - margin.top - margin.bottom) + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
//add dots
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return d[0];
})
.attr("cy", function(d) {
return d[1];
})
.attr("r", 1);
Regarding the orientation: it has nothing to do with D3. D3 manipulates DOM elements, and normally (but not always) we use D3 to manipulate SVG elements. And the SVG specs say that the origin (0,0) is at the top left corner.
Regarding your problem: you correctly set the y scale to go from the bottom to the top, but you simply forgot to use it! Use the scale:
.attr("cx", function(d) {
return x(d[0]);
})
.attr("cy", function(d) {
return y(d[1]);
})
Here is the demo:
// make dataset
var dataset = [[1,1]];
for (var x = 0; x< 100; x +=1) {
var y = x*x;
dataset.push([x, y])
}
// set graph dims
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// Adds the svg canvas
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");;
// add scale
var x = d3.scale.linear().domain([0, 200]).range([0, width]);
var y = d3.scale.linear().domain([0, 1000]).range([height - margin.bottom - margin.top, 0]);
// add x axis
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(10);
// add y axis
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(10);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height - margin.top - margin.bottom) + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
//add dots
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return x(d[0]);
})
.attr("cy", function(d) {
return y(d[1]);
})
.attr("r", 1);
line, path {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I'm trying to modify Zoomable area from http://bl.ocks.org/mbostock/4015254 with adding data points from csv file in it, but couldn't get things right. Only lines can be zoomed and moved but not points.
My guess is I need to put circle's .attr("transform", function(d){ return "translate("+x(d[key0])+","+y(d[key1])+")";}); within function draw, but then it cannot recognize key0 and key1 (I have multiple columns in the csv file).
The following is the code I modified. Any idea how to fix this?
<script>
var margin = {top: 20, right: 60, bottom: 30, left: 20},
width = 960 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;`
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse,
formatDate = d3.time.format("%Y");
var x = d3.time.scale()
.range([0, width]);
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("right")
.tickSize(-width)
.tickPadding(6);
var xGrid = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height,0)
.ticks(5)
.tickPadding(1)
.tickFormat("");
var line = d3.svg.line();
var zoom = d3.behavior.zoom()
.scaleExtent([1,128])
.on("zoom", draw);
var zoom2 = d3.behavior.zoom()
.scaleExtent([1,128])
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 + ")")
.call(zoom);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + width + ",0)");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height/2 + ")");
svg.append("g")
.attr("class", "x line")
.attr("transform", "translate(0," + height/2 + ")")
.append("svg:line")
.attr("x1",x(0))
.attr("y1",-y(0))
.attr("x2",x(width))
.attr("y2",-y(0));
svg.append("g")
.attr("class", "grid")
.attr("transform", "translate(0,"+ height + ")");
svg.append("path")
.attr("class", "line");
svg.append("rect")
.attr("class", "pane")
.attr("width", width)
.attr("height", height);
circle = svg.selectAll("circle");
d3.csv("encounter.csv", function(error, data) {
key0 = Object.keys(data[0])[1];
key1 = Object.keys(data[0])[3];
data.forEach(function(d,i) {
d[key0] = parseDate(d[key0]);
d[key1] = +d[key1];
});
x.domain([new Date(2014, 0, 1), new Date(2015, 0, 1)]);
y.domain(d3.extent(data, function(d){return d[key1];})).nice();
zoom.x(x);
svg.select("path.line").data([data]);
line.x(function(d) { return x(d[key0]); })
.y(function(d) { return y(d[key1]); });
circle.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5)
.attr("transform", function(d){ return "translate("+x(d[key0])+","+y(d[key1])+")";});
draw();
});
function draw() {
svg.select("g.x.axis").call(xAxis);
svg.select("g.y.axis").call(yAxis);
svg.select("g.grid").call(xGrid);
svg.select("path.line").attr("d", line);
}
I am trying to essentially rotate this horizontal bar chart into a vertical bar chart, but can't figure out how to do so. I can create a normal column chart, but once I try to put in the negative values and compute the y and height, all hell breaks loose. Here's my fiddle. (At least I was able to create the y-axis (I think).)
What am I doing wrong here?
var data = [{"letter":"A",'frequency':10},{"letter":"B","frequency":-5},{"letter":"C","frequency":7}];
var margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 750 - margin.left - margin.right, height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal().rangeRoundBands([0, width], .1);
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");
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 + ")");
var x0 = Math.max(-d3.min(data), d3.max(data));
x.domain(data.map(function(d) { return d.letter; }));
y.domain([d3.min(data, function(d) { return d.frequency; }), d3.max(data, function(d) { return d.frequency; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.selectAll(".bar")
.data(data).enter().append("rect")
.attr("class", "bar")
.attr("x", function(d, i) { return x(d.letter); })
.attr("y", function(d, i) { return x(Math.min(0, d.frequency));})
.attr("width", x.rangeBand())
.attr("height", function(d) { return Math.abs(x(d.frequency) - x(0)); });
Looks like there are two problems here:
The typos: .attr("y", function(d, i) { return x(...);}) should now be .attr("y", function(d, i) { return y(...);}). Same is true for the scales in your height attribute.
The change from a 0 base on the X axis to a 0 base on the Y axis. With a zero-based bar on the X axis, the x attribute of the bar is x(0). With a 0 based bar on the Y axis, the y attribute of the bar is not y(0), but y(value) (because the "base" of the bar is no longer the leading edge of the rectangle) - so in this code you need to use Math.max(0, value) (which will give y(value) for positive values) instead of Math.min(0, value):
svg.selectAll(".bar")
// ...snip...
.attr("y", function(d, i) { return y(Math.max(0, d.frequency));})
.attr("height", function(d) { return Math.abs(y(d.frequency) - y(0)); });
See updated fiddle: http://jsfiddle.net/pYZn8/5/