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>
Fiddle Example
I couldn't get ordinal scales to work with zooming and panning to achieve horizontally scroll-able effect so I have settled for scale.linear() to use numeric values for axis ticks. I was wondering if it is possible to superimpose the text label in g.tick with an image element. I have come up with this code after .call(xAxis)
svg.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis)
.selectAll(".tick text").each(function(k,i){
console.log(data[k]["img"]);
var r = document.createElement('image')
r.setAttribute('x',0)
r.setAttribute('y',9)
r.setAttribute('xlink:href',data[k]["img"])
r.setAttribute('width',50)
r.setAttribute('height',50)
this.parentNode.insertBefore(r,this);
})
I saw that the elements are prepended in g.tick but the images are nowhere to be seen. Is it because of the x and y values? Also, how can I bind the image links from the JSON data to the image element? I have tried data[k]["img"] but it gave me Cannot read property 'img' of undefined.
var img = "https://www.gravatar.com/avatar/904fd39461599f72d580fadf3a73115b?s=128&d=identicon&r=PG&f=1";
var data = [{"item":1,"diameter":"15.00","img":img},{"item":2,"diameter":"10.00","img":img},
{"item":3,"diameter":"25.00","img":img},{"item":4,"diameter":"7.00","img":img},
{"item":5,"diameter":"35.00","img":img},{"item":6,"diameter":"15.00","img":img},
{"item":7,"diameter":"12.00","img":img},{"item":8,"diameter":"10.00","img":img},
{"item":9,"diameter":"17.00","img":img},{"item":10,"diameter":"13.00","img":img},
{"item":11,"diameter":"5.00","img":img},{"item":12,"diameter":"10.00","img":img},
];
function line_chart(field,el){
margin = {
top: 20,
right: 20,
bottom: 20,
left: 45
},
tickno = 10,
width = 960 - margin.left - margin.right,
height = 450 - margin.top - margin.bottom,
x = d3.scale.linear().domain(d3.extent(data, function (d) {
return d.item;
})).range([0, width]),
ymax = d3.max(data,function(d){
return (parseInt(d[field])+1);
}),
ymin = d3.min(data,function(d){
return d[field];
}),
xmax = d3.max(data,function(d){
return d.name;
}),
y = d3.scale.linear()
.domain([ymin,ymax])
.range([height, 0]);
var line = d3.svg.line()
.x(function (d) {
return x(d.item);
})
.y(function (d) {
return y(d[field]);
});
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 1])
.y(y)
.on("zoom", zoomed);
svg = d3.select(el)
.append("svg:svg")
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append("svg:g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
svg.append("svg:rect")
.attr("width", width)
.attr("height", height)
.attr("class", "plot");
var make_x_axis = function () {
return d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(tickno);
};
var make_y_axis = function () {
return d3.svg.axis()
.scale(y)
.orient("left")
.ticks(tickno);
};
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(tickno);
svg.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis)
.selectAll(".tick text").each(function(data){
var r = document.createElement('image')
r.setAttribute('x',0)
r.setAttribute('y',9)
r.setAttribute('xlink:href','https://www.gravatar.com/avatar/904fd39461599f72d580fadf3a73115b?s=32&d=identicon&r=PG&f=1')
r.setAttribute('width',50)
r.setAttribute('height',50)
this.parentNode.insertBefore(r,this);
})
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(tickno);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "x grid")
.attr("transform", "translate(0," + height + ")")
.call(make_x_axis()
.tickSize(-height, 0, 0)
.tickFormat(""));
svg.append("g")
.attr("class", "y grid")
.call(make_y_axis()
.tickSize(-width, 0, 0)
.tickFormat(""));
var clip = svg.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
var chartBody = svg.append("g")
.attr("clip-path", "url(#clip)");
chartBody.append("svg:path")
.datum(data)
.attr("class", "line")
.attr("d", line);
svg.append('g').attr('class','dots').selectAll(".dot").data(data).enter().append("circle").attr("r",3.5)
.attr("cx",function(d){return x(d.item);})
.attr("cy",function(d){return y(d[field]);})
function zoomed() {
console.log(d3.event.translate);
console.log(d3.event.scale);
svg.select(".dots")
.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
svg.selectAll(".dots circle").attr("r", function(){
return (3.5 * d3.event.scale);
});
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
svg.select(".x.grid")
.call(make_x_axis()
.tickSize(-height, 0, 0)
.tickFormat(""));
svg.select(".y.grid")
.call(make_y_axis()
.tickSize(-width, 0, 0)
.tickFormat(""));
svg.select(".line")
.attr("class", "line")
.attr("d", line);
svg.selectAll(".dots circle").attr("r", function(){
return (3.5 * d3.event.scale);
});
}
}
line_chart('diameter','#area')
Your image doesn't display because your width is too small.
Also, it's a bit cleaner if you append the images the d3 way:
.selectAll(".tick").each(function(d,i){
d3.select(this)
.append('image')
.attr('xlink:href', data[i].img)
.attr('x',0)
.attr('width',128)
.attr('height',128);
})
Updated fiddle.
Below code draws a circle and an x axis. There are a couple of problems. The y axis is not displayed but it is being added :
svgContainer.append('g')
.attr("class", "axis")
.call(yAxis);
The x axis is not being aligned to bottom but I have set the orientation to bottom :
orient("bottom").ticks(5);
here is complete code :
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script>
var svgContainer = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 1000);
var circle = svgContainer.append("circle")
.attr("cx", 150)
.attr("cy", 150)
.attr("r", 100)
.attr("fill", "none")
.attr("stroke" , "red")
.attr("stroke-width" , 2);
var x = d3.scale.linear().range([0, 1000]);
var y = d3.scale.linear().range([0, 1000]);
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
svgContainer.append('g')
.attr("class", "axis")
.call(xAxis);
svgContainer.append('g')
.attr("class", "axis")
.call(yAxis);
</script>
</body>
</html>
fiddle : http://jsfiddle.net/zzz8svuq/1/
Why are these issues occurring ?
Setting the orientation only affects the placement of the labels with respect to the axis line, D3 won't automatically position the axis on the canvas for you. The way this is usually done is to set a margin for the axes and offset the containers accordingly:
svgContainer.append('g')
.attr("class", "axis")
.attr("transform", "translate(" + margin + "," + (1000 - margin) + ")")
.call(xAxis);
svgContainer.append('g')
.attr("class", "axis")
.attr("transform", "translate(" + margin + ",0)")
.call(yAxis);
Complete demo here.
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) ?
I'm trying to display dates in the x axis and at the same time zoom it when you scroll.
So, I have this code:
<script type="text/javascript">
var data = [
[{'x':20111001,'y':1},{'x':20111002,'y':6},{'x':20111003,'y':11},{'x':20111004,'y':1},{'x':20111005,'y':2},{'x':20111006,'y':12},{'x':20111007,'y':2},{'x':20111008,'y':3},{'x':20111009,'y':13},{'x':20111010,'y':3}],
[{'x':20111001,'y':2},{'x':20111002,'y':2},{'x':20111003,'y':12},{'x':20111004,'y':2},{'x':20111005,'y':3},{'x':20111006,'y':1},{'x':20111007,'y':2},{'x':20111008,'y':7},{'x':20111009,'y':2},{'x':20111010,'y':7}],
[{'x':20111001,'y':3},{'x':20111002,'y':10},{'x':20111003,'y':13},{'x':20111004,'y':3},{'x':20111005,'y':12},{'x':20111006,'y':14},{'x':20111007,'y':6},{'x':20111008,'y':1},{'x':20111009,'y':7},{'x':20111010,'y':9}],
[{'x':20111001,'y':4},{'x':20111002,'y':4},{'x':20111003,'y':14},{'x':20111004,'y':14},{'x':20111005,'y':10},{'x':20111006,'y':15},{'x':20111007,'y':3},{'x':20111008,'y':0},{'x':20111009,'y':3},{'x':20111010,'y':12}]
];
var colors = [
'steelblue',
'green',
'red',
'purple'
]
var b =[];
var parseDate = d3.time.format("%Y%m%d").parse;
data.forEach(function (d) {
f = d;
f.forEach(function(f){
b.push(parseDate(String(f.x)));
})
})
var margin = {top: 20, right: 30, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.time.scale()
.domain([d3.extent(b)])
.range([0, width]);
var y = d3.scale.linear()
.domain([-1, 16])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(-height)
.tickPadding(10)
.tickSubdivide(true)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.tickPadding(10)
.tickSize(-width)
.tickSubdivide(true)
.orient("left");
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("body").append("svg")
.call(zoom)
.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);
svg.append("g")
.attr("class", "y axis")
.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", (-margin.left) + 10)
.attr("x", -height/2)
.style("text-anchor", "end")
.text("Ventas (Miles €)");
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
svg.selectAll('.line')
.data(data)
.enter()
.append("path")
.attr("class", "line")
.transition()
.attr("clip-path", "url(#clip)")
.attr('stroke', function(d,i){
return colors[i%colors.length];
})
.attr("d", line);
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var points = svg.selectAll('.dots')
.data(data)
.enter()
.append("g")
.attr("class", "dots")
.attr("clip-path", "url(#clip)");
points.selectAll('.dot')
.data(function(d, index){
var a = [];
d.forEach(function(point,i){
a.push({'index': index, 'point': point});
});
return a;
})
.enter()
.append('circle')
.attr('class','dot')
.attr("r", 2.5)
.attr('fill', function(d,i){
return colors[d.index%colors.length];
})
.attr("transform", function(d) {
return "translate(" + x(d.point.x) + "," + y(d.point.y) + ")"; }
).on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div .html(d.point.x + "K<br/>" + d.point.y)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
})
function zoomed() {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
svg.selectAll('path.line').attr('d', line);
points.selectAll('circle').attr("transform", function(d) {
return "translate(" + x(d.point.x) + "," + y(d.point.y) + ")"; }
);
}
</script>
I can make it with numbers but can't implement it with dates. I've checked other examples and how they make it but can't find the way to code it in my chart.
I'd like to know how to display dates on x axis.
Is your question how to make the axis dates instead of just numbers? Or is it how to make the axis pannable? If it's the first, use code like this:
var x=d3.time.scale()
.domain([minDate,maxDate])
The minDate and maxDate have to be javascript Date objects.