d3js v4: independent multiple axes scaling and panning - javascript

I'm trying to implement a chart framework which is able to create line and area charts with multiple axes for each orientation, i.e 2-y-axis left, 1-yaxis right and 1-x-axis bottom.
That I got to work. My next task would be to implement zoom behaviour to the chart. I indented to have a global zoom behaviour, which is trigged if the user uses his mouse within the plot area. The displayed series would get rescaled and it would be possible to pan the plot. This one I got to work too.
In addition I wanted an independent zoom/scaling for each axis. I got the scaling, but I still have problems with the global zooming and panning. If I scale one axis, the associated series in the plot area gets rescaled but the panning does not work. And after the independent scaling of an axis, if I use the global rescale, the scaling gets reset and then gets scaled by the global zoom behaviour.
On the d3.js page I found an simple example for independent and global scaling and panning, but written with d3v3 .
I changed the example in such a way, so that it displays my problem jsfiddle demo. Use you mouse on the axes and in the plot area.
<!DOCTYPE html>
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Independent Axis Zooms on x, y, or xy</title>
<script src="//d3js.org/d3.v4.min.js"></script>
<style>
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
var data = [];
for (var i = 0; i < 100; i++) {
data.push([Math.random(), Math.random()]);
}
var svg = d3.select('#chart')
.append("svg")
.attr("width", window.innerWidth).attr("height", window.innerHeight);
function example(svg, data) {
var svg;
var margin = {
top: 60,
bottom: 80,
left: 60,
right: 0
};
var width = 500;
var height = 400;
var xaxis = d3.axisBottom();
var yaxis = d3.axisLeft();
var xscale = d3.scaleLinear();
var yscale = d3.scaleLinear();
var xcopyScale, ycopyScale;
var xyzoom, xzoom, yzoom;
updateZooms();
function update() {
var gs = svg.select("g.scatter");
var circle = gs.selectAll("circle")
.data(data);
circle.enter().append("svg:circle")
.attr("class", "points")
.style("fill", "steelblue")
.attr("cx", function (d) {
return X(d);
})
.attr("cy", function (d) {
return Y(d);
})
.attr("r", 4);
circle.attr("cx", function (d) {
return X(d);
})
.attr("cy", function (d) {
return Y(d);
});
circle.exit().remove();
}
function updateZooms() {
xyzoom = d3.zoom()
.on("zoom", function () {
xaxis.scale(d3.event.transform.rescaleX(xscale));
yaxis.scale(d3.event.transform.rescaleY(yscale));
draw();
});
xzoom = d3.zoom()
.on("zoom", function () {
xaxis.scale(d3.event.transform.rescaleX(xscale));
draw();
});
yzoom = d3.zoom()
.on("zoom", function () {
yaxis.scale(d3.event.transform.rescaleY(yscale));
draw();
});
}
function draw() {
svg.select('g.x.axis').call(xaxis);
svg.select('g.y.axis').call(yaxis);
update();
// After every draw, we reinitialize zoom. After every zoom, we reexecute draw, which will reinitialize zoom.
// This is how we can apply multiple independent zoom behaviors to the scales.
// (Note that the zoom behaviors will always end up with zoom at around 1.0, and translate at around [0,0])
svg.select('rect.zoom.xy.box').call(xyzoom);
svg.select('rect.zoom.x.box').call(xzoom);
svg.select('rect.zoom.y.box').call(yzoom);
}
// X value to scale
function X(d) {
return xaxis.scale() !== undefined && xaxis.scale() !== null
? xaxis.scale()(d[0])
: xscale(d[0]);
}
// Y value to scale
function Y(d) {
return yaxis.scale() !== undefined && yaxis.scale() !== null
? yaxis.scale()(d[1])
: yscale(d[1]);
}
var g = svg.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
g.append("svg:rect")
.attr("class", "border")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.style("stroke", "black")
.style("fill", "none");
g.append("g").attr("class", "x axis")
.attr("transform", "translate(" + 0 + "," + (height - margin.top - margin.bottom) + ")");
g.append("g").attr("class", "y axis");
g.append("g")
.attr("class", "scatter")
.attr("clip-path", "url(#clip)");
g
.append("svg:rect")
.attr("class", "zoom xy box")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(xyzoom);
g
.append("svg:rect")
.attr("class", "zoom x box")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.attr("transform", "translate(" + 0 + "," + (height - margin.top - margin.bottom) + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(xzoom);
g
.append("svg:rect")
.attr("class", "zoom y box")
.attr("width", margin.left)
.attr("height", height - margin.top - margin.bottom)
.attr("transform", "translate(" + -margin.left + "," + 0 + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(yzoom);
// Update the x-axis
xscale.domain(d3.extent(data, function (d) {
return d[0];
})).range([0, width - margin.left - margin.right]);
xaxis.scale(xscale)
.tickPadding(10);
svg.select('g.x.axis').call(xaxis);
// Update the y-scale.
yscale.domain(d3.extent(data, function (d) {
return d[1];
})).range([height - margin.top - margin.bottom, 0]);
yaxis.scale(yscale)
.tickPadding(10);
svg.select('g.y.axis').call(yaxis);
draw();
}
var exampleChart = example(svg, data);
</script>
</body>
</html>
To put it briefly: How can I solve my problem by using d3v4 to create a chart with multiple axes that has global and independent scaling and panning behaviour ?

as of now the current release of d3v4 doesn't natively support multiple independent zoom behaviours.
A possible solution would be to reset the internal transform state stored inside the selection on which you called the appropriate zoom behaviour.
There is an already open issue on the argument and i encourage you to go and read it and offer your input as well.
Best of luck!

There seems to be no correct solution (see https://github.com/d3/d3-zoom/issues/48).
But if you clear the scale after each zoom it seems to work
(see https://jsfiddle.net/DaWa/dLmp8zk8/2/).

Related

area chart not able render D3.js

I am learning d3.js and this is me trying to learn area chart but i am getting this error of :
<!DOCTYPE html>
<meta charset="utf-8" />
<style>
/* set the CSS */
.line {
fill: none;
stroke: green;
stroke-width: 2px;
}
.area {
fill: green;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// parse the date / time
var parseTime = d3.timeParse("%B %e, %Y");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the area
var area = d3
.area()
.x(function (d) {
return x(d.Date);
})
.y0(height)
.y1(function (d) {
return y(d["New cases"]);
});
// define the line
var valueline = d3
.line()
.x(function (d) {
return x(d.date);
})
.y(function (d) {
return y(d.newcases);
});
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
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 + ")");
// get the data
d3.csv(
"https://raw.githubusercontent.com/pravinpoudel/file-host/main/COVID-Utah.csv"
).then(function (data) {
// format the data
data.forEach(function (d) {
d.Date = parseTime(d.Date);
d["New cases"] = +d["New cases"];
});
x.domain(
d3.extent(data, function (d) {
return d.Date;
})
);
y.domain([
0,
d3.max(data, function (d) {
return d["New cases"];
}),
]);
// add the area
svg.append("path").data([data]).attr("class", "area").attr("d", area);
// add the valueline path.
svg
.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// add the X Axis
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the Y Axis
svg.append("g").call(d3.axisLeft(y));
});
</script>
</body>
can anyone please help me on this !!
I think I've found an answer, so let me try and pitch in.
The biggest issue is how you append the <path> elements for the area and line.
svg.append("path").data([data]).attr("class", "area").attr("d", area);
svg.append("path").data([data]).attr("class", "line").attr("d", valueline);
Once you append the <path> element you instead need to pass the data in the area and valueline functions.
svg.append("path").attr("class", "area").attr("d", area(data));
svg.append("path").attr("class", "line").attr("d", valueline(data));
Unfortunately this is not enough, and the console points you to the error.
Error: <path> attribute d: Expected number, "…5.1232876712329,NaNL882.68493150…"
Some coordinates are assigned a value of NaN, and looking at the data this has to do with the New cases field. The value is not always a number, often NaN or N/A. As a workaround you can try the conversion and provide a fallback.
d["New cases"] = +d["New cases"]
+d["New cases"] = +d["New cases"] || 0;
It's a bit of a judgment call, however. You need to decide how to interpret data points which do not have an available value.
With these two changes you finally are able to display the area, but not the line. Looking at the definition of valueline it seems you point to two different properties.
var valueline = d3
.line()
.x(function (d) {
return x(d.date);
})
.y(function (d) {
return y(d.newcases);
});
Be sure to reference the properties of the data, as you have done with the area function.
-return x(d.date);
+return x(d.Date);
-return y(d.newcases);
+return y(d['New cases']);
As the line and area have the same color you might find it difficult to distinguish them visually, but they are both plotted.
A minor note on the name of the svg variable.
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 + ")");
As you mention in the comment you append a group element to translate the origin. This means the variable refers to the group element, and not the svg.
Hope it helps.

d3.event.pageX & d3.mouse(this)[0]

I tried to figure out the difference between 'd3.event.pageX' & 'd3.mouse(this)[0]'.
I guessed both are same but,
when I console.log both,
the value was different by '8' in my code.
var height=600;
var width=600;
var graphgap=60;
d3.csv('./details.csv').then(function(data){
var svg =d3.select('section').append('svg')
.attr('width',600).attr('height',600)
.on('mousemove',mousemove)
drawrect(data);
})
function drawrect(data){
let bars=d3.select('svg').selectAll('rect').data(data);
bars.enter().append('rect').classed('bargraph',true)
.attr('x',function(d,i){return (i+1)*graphgap})
.attr('y',function(d){return height-(d.Age)*5})
.attr('width',55)
.attr('height',function(d){return (d.Age)*(5)})
}
function mousemove(){
let mouselocation =[];
d3.select('svg').append('text')
.text(d3.event.pageX)
.attr('x',d3.event.pageX)
.attr('y',d3.event.pageY)
console.log(d3.event.pageX)
console.log(d3.mouse(this)[0])
}
So, I think these two are two different things.
Can anyone let me know why it makes a difference?
The reason why I tried to figure this out is because I was re-writing the code below.
<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.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/data_IC.csv",function(data) {
// Add X axis --> it is a date format
var x = d3.scaleLinear()
.domain([1,100])
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, 13])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
// This allows to find the closest X index of the mouse:
var bisect = d3.bisector(function(d) { return d.x; }).left;
// Create the circle that travels along the curve of chart
var focus = svg
.append('g')
.append('circle')
.style("fill", "none")
.attr("stroke", "black")
.attr('r', 8.5)
.style("opacity", 0)
// Create the text that travels along the curve of chart
var focusText = svg
.append('g')
.append('text')
.style("opacity", 0)
.attr("text-anchor", "left")
.attr("alignment-baseline", "middle")
// Create a rect on top of the svg area: this rectangle recovers mouse position
svg
.append('rect')
.style("fill", "none")
.style("pointer-events", "all")
.attr('width', width)
.attr('height', height)
.on('mouseover', mouseover)
.on('mousemove', mousemove)
.on('mouseout', mouseout);
// Add the line
svg
.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
)
// What happens when the mouse move -> show the annotations at the right positions.
function mouseover() {
focus.style("opacity", 1)
focusText.style("opacity",1)
}
function mousemove() {
// recover coordinate we need
var x0 = x.invert(d3.mouse(this)[0]);
var i = bisect(data, x0, 1);
selectedData = data[i]
focus
.attr("cx", x(selectedData.x))
.attr("cy", y(selectedData.y))
focusText
.html("x:" + selectedData.x + " - " + "y:" + selectedData.y)
.attr("x", x(selectedData.x)+15)
.attr("y", y(selectedData.y))
}
function mouseout() {
focus.style("opacity", 0)
focusText.style("opacity", 0)
}
})
</script>
In documentation is written:
While you can use the native event.pageX and event.pageY, it is often
more convenient to transform the event position to the local
coordinate system of the container that received the event using
d3.mouse, d3.touch or d3.touches.
d3.event
d3.mouse - uses local coordinate (without margin (60px))
d3.event.pageX - uses global coordinate (with margin (60px))
But local cordinate start on 68px. I guess 8 pixels is used to describe the y-axis.

D3 Transform Rescale X jumps to right

I'm creating a timeline line chart showing 5 or 6 different lines and want to be able to zoom in and scroll (once zoomed). I used some examples that use area charts but for some reason my line chart jumps to the right when I zoom the first time and I lose some of the data off to the right (I can no longer scroll to see it or see it when zoomed fully out). Also the lines appear over the y axis when I zoom or scroll.
I've copied it to JSFiddle (see here) with a dataset from 1 of the lines in my chart. Why is the line jumping to the right as soon as you use the zoom function? How can I stop the line from appearing over the y-axis?
Here is the JS of my version if you'd prefer to read it here:
function drawTimeline() {
var margin = {top: 10, right: 0, bottom: 50, left: 60}
var width = d3.select('#timeline').node().getBoundingClientRect().width/3*2;
var height = 300;
var svg = d3.select("#timeline").append("svg")
.attr("width", width)
.attr("height", height+margin.top+margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("width", width - margin.left - margin.right)
.call(d3.zoom()
// .extent()
.scaleExtent([1, 10])
.translateExtent([[0, -Infinity], [width - margin.left - margin.right, Infinity]])
.on("zoom", zoom)
);
var view = svg.append("rect")
.attr("class", "view")
.attr("width", width - margin.left - margin.right)
.attr("height", height)
.attr("fill", "white");
var x = d3.scaleTime()
.domain([
d3.min(poll_data[0].avgpolls, function(p) { return p.date; }),
d3.max(poll_data[0].avgpolls, function(p) { return p.date; })
])
.range([0, width - margin.left - margin.right]);
var y = d3.scaleLinear()
.domain([50, 0])
.range([0, height]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var gX = svg.append("g")
.attr("class", "axis xaxis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
var gY = svg.append("g")
.attr("class", "axis yaxis")
.call(yAxis);
//All lines are drawn in the same way (x and y points)
var line = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.poll); });
//selectAll allows us to create and manipulate multiple groups at once
var party = svg.selectAll(".party")
.data(poll_data)
.enter()
.append("g")
.attr("class", "party");
//Add path to every country group at once
var pollPaths = party.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.avgpolls); })
.attr("fill", "none")
.attr("stroke-width", 2)
.style("stroke", function(d) { return returnPartyColour(d.party); });
function zoom() {
console.log("zooming: " + d3.event.transform);
var new_x = d3.event.transform.rescaleX(x);
gX.call(d3.axisBottom(new_x));
//view.attr("transform", d3.event.transform);
//Redraw lines
//pollPaths.select(".line").attr("d", function(d) { return line(d.avgpolls); });
var newline = d3.line()
.x(function(d) { return new_x(d.date); })
.y(function(d) { return y(d.poll); });
pollPaths.attr("d", function(d) { return line(d.avgpolls); });
}
}
The data is formatted like this in my version but not the JSFiddle:
poll_data = [
{
'party' : 'Party 1',
'avgpolls' : [
{'date' : new Date(year, month, day), 'poll' : 0, },
],
]
Thanks
Two things required to fix these issues.
The reason the line was jumping on zoom was because the zoom extent was not set. This was set and the value of translateExtent updated:
d3.zoom()
.scaleExtent([1, 10])
.translateExtent([[0, 0], [width - margin.left - margin.right, Infinity]])
.extent([[0, 0], [width - margin.left - margin.right, height]])
.on("zoom", zoom)
To prevent the paths from overflowing a clipping path is required. After creating the svg, before other elements are added, I added a clip-path as follows:
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
//Same dimensions as the area for the lines to appear
.attr("width", width - margin.left - margin.right)
.attr("height", height);
Then this had to be added to each path.
var pollPaths = party.append("path")
...
.attr("clip-path", "url(#clip)");
The original JSFiddle has been updated to reflect these changes.

D3.js v4 horizontal panning only (no zoom)

I'm new to D3.js and am trying to build a very-wide line chart that pans horizontally to show more data from a .csv file. I haven't been able to find many good resources for the latest version of D3.
What I'm trying to achieve:
chart fills the screen with overflow hidden
drag/mousewheel reveals more data from the right
axis labels stay put (but x-axis changes to reflect new data)
no zooming
The code I have right now renders the chart, and it can be clicked and dragged, but the whole thing just moves off the screen... Here's the code:
var margin = {top: 50, right: 50, bottom: 50, left: 50},
width = window.innerWidth - margin.left - margin.right,
height = window.innerHeight - margin.top - margin.bottom;
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var line = d3.line()
.x(function(d) { return x(d.mile); })
.y(function(d) { return y(d.elevation); })
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(d3.zoom().on("zoom", function () {
svg.attr("transform", d3.event.transform)
}))
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
function draw(data) {
data.forEach(function(d) {
d.mile = +d.mile;
d.elevation = +d.elevation;
});
x.domain(d3.extent(data, function(d) { return d.mile }) )
y.domain([0, d3.max(data, function(d) {
return Math.max(d.elevation) }) ]);
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", line)
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
}
d3.json("data.json", function(error, data) {
if (error) throw error;
draw(data)
});
And here's a sample of what the data looks like:
mile,elevation
1505.9,1800
1506.4,1360
1507.0,1340
1507.9,1750
1509.7,2365
Thanks in advance for any resources anyone can offer me to help solve this!

Library for creating scatter plot charts with quadrants

I have to create scatter plot with quadrants. I have looked at libraries like d3.js , high charts, nvd3 but I found only normal scatter charts.
Can someone suggest which js library will help me achieve this?
Thanks
D3.js allows this feature if you simply add negative values for the coordinates in the scatterplot. Just off the top of my hat, you could give the points their regular coordinates, just set the domains of your d3.scale function as to allow negative values. Just an example would be
var x = d3.scale.linear().range([0, width]).domain([d3.min(data), d3.max(data)]);
This sets the range of your scatterplot to the width you have selected, but allows all values to be accepted into the plot, regardless of them being positive or negative. As is very well explained here, scales fit to the size of the range, spreading the contents of the domain over the range. It is, however, not a requirement that said domain is completely positive.
If you check for the biggest absolute number in your data, you can format the domain likewise, therefore having the axes in the center of your plot, instead of them being misaligned.
Next, just add your axes like normal, only shift them to the middle of your canvas using x and y attributes.
Here is the upated Plunker link for creating scatter plot charts with quadrants through d3.js:-
http://plnkr.co/edit/yEfkN0tn7DPAypAvyWjD?p=preview
Code:
<script>
var svg = d3.select("#scatter"),
margin = {top: 20, right: 20, bottom: 30, left: 50},
width = +svg.attr("width"),
height = +svg.attr("height"),
domainwidth = width - margin.left - margin.right,
domainheight = height - margin.top - margin.bottom;
var x = d3.scaleLinear()
.domain(padExtent([1,5]))
.range(padExtent([0, domainwidth]));
var y = d3.scaleLinear()
.domain(padExtent([1,5]))
.range(padExtent([domainheight, 0]));
var g = svg.append("g")
.attr("transform", "translate(" + margin.top + "," + margin.top + ")");
g.append("rect")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.attr("fill", "#F6F6F6");
d3.json("data.json", function(error, data) {
if (error) throw error;
data.forEach(function(d) {
d.consequence = +d.consequence;
d.value = +d.value;
});
g.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 7)
.attr("cx", function(d) { return x(d.consequence); })
.attr("cy", function(d) { return y(d.value); })
.style("fill", function(d) {
if (d.value >= 3 && d.consequence <= 3) {return "#60B19C"} // Top Left
else if (d.value >= 3 && d.consequence >= 3) {return "#8EC9DC"} // Top Right
else if (d.value <= 3 && d.consequence >= 3) {return "#D06B47"} // Bottom Left
else { return "#A72D73" } //Bottom Right
});
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + y.range()[0] / 2 + ")")
.call(d3.axisBottom(x).ticks(5));
g.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + x.range()[1] / 2 + ", 0)")
.call(d3.axisLeft(y).ticks(5));
});
function padExtent(e, p) {
if (p === undefined) p = 1;
return ([e[0] - p, e[1] + p]);
}
</script>

Categories