D3.js Synchronized Zoom Across Multiple Graphs - javascript

I want to make a line chart to zoom/pan in sync with multiple web pages.
These client has same Javascript and HTML source.
User zooms or pan on client A, message which is day time of domain of data is sent to the other and sender(blue line on above fig), and graph of received clients will be change simultaneously . Of course, other clients can do the same.
It is similar like a chat application.
Zoom function is:
function zoomed() {
let msg = [];
let t = d3.event.transform; //1)
msg[0] = t.rescaleX(x2).domain()[0].toString(); //2)
msg[1] = t.rescaleX(x2).domain()[1].toString(); //2)
sendMessage(msg); //3)
}
d3.event.transform catches mouse event.
convert to date time and strings.
send new scale domain to server.
Server sends received data to all clients:
function passiveZoom(rcv){
let leftend;
let rightend;
leftend = new Date(rcv[0]);
rightend = new Date(rcv[1]);
x.domain([leftend, rightend]);
svg.select(".line").attr("d", valueline);
svg.select(".axis").call(xAxis);
}
Received message from server which contain new day time.
set new domain,
update the line charts.
With this it is possible to zoom|pan all the line charts.
However, it does not work as required.
If I zoom|pan in client A, client B and client C will be changed. That is ok.
Next, I zoom|pan on client C(orange line on above figure), All graphs change to initial scale and position. Why!?
I assume that the mouse coordinates are not sent to the clients, but how should I handle it when I send the position coordinates of the mouse?
The Zoom|Pan process is forked from mbostock's block: Brush & Zoom. The sender also changes the range of the X2 domain with t.rescalex (x2).domain().
Since X2 is not used in the drawing, I changed X to x2, but I can only zoom in. I do not understand the meaning of X2.
Would you please let me know how to synchronize all of clients?
And what is x2?
This code is for clients forked from Simple line graph with v4.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* set the CSS */
body {
font: 12px Arial;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="socket.io.js"></script>
<script>
//--- Network----
let rcvT;
let socket = io.connect('http://localhost:3000');
//Recive event from server
socket.on("connect", function() {});
socket.on("disconnect", function(client) {});
socket.on("S_to_C_message", function(data) {
rcvT = data.value;
passiveZoom(rcvT);
});
socket.on("S_to_C_broadcast", function(data) {
console.log("Rcv broadcast " + data.value);
rcvT = data.value;
passiveZoom(rcvT);
});
function sendMessage(msg) {
socket.emit("C_to_S_message", { value: msg }); //send to server
}
function sendBroadcast(msg) {
socket.emit("C_to_S_broadcast", { value: msg }); // send to server
}
// --------------------
// Set the dimensions of the canvas / graph
var margin = { top: 30, right: 20, bottom: 30, left: 50 },
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.timeParse("%d-%b-%y");
// Set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleTime().range([height, 0]);
var x2 = d3.scaleTime().range([0, width]);
xAxis = d3.axisBottom(x)
.tickFormat(d3.timeFormat('%d-%b-%y'))
.ticks(5);
// var yAxis = d3.svg.axis().scale(y)
// .orient("left").ticks(5);
yAxis = d3.axisLeft(y);
// Define the line
var valueline = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// Adds the svg canvas
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("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; }));
x2.domain(x.domain());
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// 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);
});
//follow is zoom method------------------
zoom = d3.zoom()
.scaleExtent([1, 45])
.translateExtent([
[0, 0],
[width, height]
])
.extent([
[0, 0],
[width, height]
])
.on("zoom", zoomed);
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
function zoomed() {
let msg = [];
let t = d3.event.transform;
msg[0] = t.rescaleX(x2).domain()[0].toString();
msg[1] = t.rescaleX(x2).domain()[1].toString();
sendMessage(msg);
}
function passiveZoom(rcv){
let start;
let end;
start = new Date(rcv[0]);
end = new Date(rcv[1]);
x.domain([start, end]);
svg.select(".line").attr("d", valueline);
svg.select(".axis").call(xAxis);
}
</script>
</body>
If you try this code, you should exec in a few bowser windows, and run this node.js script.
var http = require("http");
var socketio = require("socket.io");
var fs = require("fs");
console.log("reflector start");
var server = http.createServer(function(req, res) {
res.writeHead(200, {"Content-Type":"text/html"});
var output = fs.readFileSync("./index.html", "utf-8");
res.end(output);
}).listen(process.env.VMC_APP_PORT || 3000);
var io = socketio.listen(server);
io.sockets.on("connection", function (socket) {
// send message to all
socket.on("C_to_S_message", function (data) {
io.sockets.emit("S_to_C_message", {value:data.value});
console.log("MSG "+data.value);
});
// boradcast send to all without sender
socket.on("C_to_S_broadcast", function (data) {
socket.broadcast.emit("S_to_C_broadcast", {value:data.value});
});
// disconnection
socket.on("disconnect", function () {
console.log("disconnect");
});
});

Assuming I understand the problem,
The (first) problem is that you are not updating (the) zoom itself.
Where d3.zoom is used, it often just keeps track of current zoom state rather than applying a transform on a container directly. In the brush and zoom example, the zoom is applied by re-scaling the data - not by applying an SVG transform to the container. Using that example, we can see that when we brush, we also call:
svg.select(".zoom").call(zoom.transform, someZoomTransform);
This:
updates the zoom state/identity as tracked by the zoom variable
emits a zoom event, which invokes the zoomed function (which in the brush and zoom example is ignored if a brush triggers it)
If we remove this line, changes in scale state made by brushing don't update the zoom. Brush to a very small domain, then zoom in and see here.
This is the case in your code, when you update the chart with the zoomed function and d3.event.transform you aren't updating the zoom state. You are updating the scales - but zoom is not updated.
Below I'll demonstrate using one zoom to update another. Note: if each zoomed function calls the others, we'll enter an infinite loop. With brush and zoom we could see if the trigger was a brush to see if the zoomed function was needed, below I use d3.event.sourceEvent.target to see if the other zoomed functions need to propagate the zoom:
var svg = d3.select("svg");
var size = 100;
var zoom1 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed1);
var zoom2 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed2);
var rect1 = svg.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", 10)
.attr("y", 10)
.call(zoom1);
var rect2 = svg.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", 300)
.attr("y", 10)
.call(zoom2);
function zoomed1() {
var t = d3.event.transform;
var k = Math.sqrt(t.k);
rect1.attr("width",size/k).attr("height",size*k);
if(d3.event.sourceEvent.target == this) {
rect2.call(zoom2.transform,t);
}
}
function zoomed2() {
var t = d3.event.transform;
var k = Math.sqrt(t.k);
rect2.attr("width",size/k).attr("height",size*k);
if(d3.event.sourceEvent.target == this) {
rect1.call(zoom2.transform,t);
}
}
rect {
cursor: pointer;
stroke: #ccc;
stroke-width: 10;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Zoom on one rectangle to update the other.
<svg width="600" height="300"></svg>
You might wonder why I hard coded size, why don't I just modify the current size, rather than the original. The answer is that the zoom transform scale is the scale relative to the original state - not the last state. For example, if scale doubles each zoom, and we zoom in 2 times, the scale goes from: k=1 → k=2 → k=4. If we multiply the current size of a shape by the new scale, we get size=1 → size=2 → size=8, this is not correct (and upon zooming out to k=2, we'll double the amount we zoom in, rather than zooming out). The transform is cumulative already, we don't want to apply it to a value that has a transform applied on it.
Applying the transform on a transformed value, rather than the original value, can lead to increasing zoom even when zooming out - this is probably why you have had trouble zooming out
So, this brings me to the second problem, x2. x2 is the reference, the original value. Yes, as Gerardo notes it is also the scale for the brush in your example, but more importantly, he states that this scale doesn't change. Because of this, x2 is well suited to be used as a reference scale, to which we can use to transform x given a zoom state:
x.domain(t.rescaleX(x2).domain());
What happens here? transform.rescaleX(x2) doesn't modify x2, it "returns a copy of the continuous scale x whose domain is transformed [given a zoom transformation].(docs)". We take the copy's domain and assign it to the x scale (range of course remains the same), and by doing so, apply the transform to the x scale. This is essentially the same as my snippet above with the square/rectangles, where I keep a reference value for the initial size of the shapes and apply the transform to this value.
Let's see this in action with a basic graph/plot with scales rather than plain shapes:
var svg = d3.select("svg");
var data = [[0,300],[1,20],[2,300]];
// Area generators:
var leftArea = d3.area().curve(d3.curveBasis)
.x(function(d) { return leftX(d[0]); })
var rightArea = d3.area().curve(d3.curveBasis)
.x(function(d) { return rightX(d[0]); })
// Scales
var leftX = d3.scaleLinear().domain([0,2]).range([0,250]);
var rightX = d3.scaleLinear().domain([0,2]).range([300,550]);
var leftX2 = leftX.copy();
var rightX2 = rightX.copy();
// Zooms
var leftZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", leftZoomed);
var rightZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", rightZoomed);
// Graphs
var leftGraph = svg.append("path")
.attr("d", leftArea(data))
.call(leftZoom);
var rightGraph = svg.append("path")
.attr("d", rightArea(data))
.call(rightZoom);
function leftZoomed() {
var t = d3.event.transform;
leftX.domain(t.rescaleX(leftX2).domain());
leftGraph.attr("d",leftArea(data));
if(d3.event.sourceEvent.target == this) {
rightGraph.call(rightZoom.transform,t);
}
}
function rightZoomed() {
var t = d3.event.transform;
rightX.domain(t.rescaleX(rightX2).domain());
rightGraph.attr("d",rightArea(data));
if(d3.event.sourceEvent.target == this) {
leftGraph.call(leftZoom.transform,t);
}
}
path {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Zoom on one plot to update the other (zoom on the path area itself)
<svg width="600" height="300"></svg>
Put simply, to synchronize multiple zoomable scaled graphs in one page or across clients, you should be:
updating each zoom with selection.call(zoom.transform,transform)
rescaling each scale using the current transform and a reference scale.
I haven't dug into trying this with multiple clients and sockets. But, the above should help in explaining how to approach the problem. However, with multiple clients, you might need to modify how I've stopped the infinite loop of zoom events, using or setting a property in the transform object might be the easiest. Also, as rioV8 notes, you should probably be passing the zoom parameters (or better yet, d3.event itself), not the domain, though a domain only option is possible.
With sockets, I did have some trouble in sending objects - I'm not familiar with socket.io, and didn't spend a tonne of time looking, but I got this to work with zoomed and passivezoom functions as so:
function zoomed() {
let t = d3.event.transform;
// 1. update the scale, same as in brush and zoom:
x.domain(t.rescaleX(x2).domain());
// 2. redraw the graph and axis, same as in brush and zoom:
path.attr("d", area); // where path is the graph
svg.select(".xaxis").call(xAxis);
// 3. Send the transform, if needed:
if(t.alreadySent == undefined) {
t.alreadySent = true; // custom property.
sendMessage([t.k,t.x,t.y,t.alreadySent]);
}
}
function passiveZoom(rcv){
// build a transform object (since I was unable to successfully transmit the transform)
var t = d3.zoomIdentity;
t.k = rcv[0];
t.x = rcv[1];
t.y = rcv[2];
t.alreadySent = rcv[3];
//trigger a zoom event (invoke zoomed function with new transform data).
rect.call(zoom.transform,t); // where rect is the selection that zoom is called on.
}
Rather than sending the event, I send the transform parameters (only) along with a flag to note that the zoom event that a passive zoom function triggers doesn't need to be passed onwards again. This is based in principle exactly on the above snippets.
No modification to server side script. Here's the client side that I used - it's more basic than your code, as I stripped out y scales, y axis, csv data source, etc.

Related

How to get map to show longitude and latitude accurately with d3

I'm using d3.js to try to map coordinates for a csv data file that contains an order id, latitude & longitude of the address the order was shipped to, and the amount that was spent on the order. I've tried mapping them out linearly and tried using a log scale, but the plot points still seem skewed. I was trying to get it to look like a map of the US which it slightly resembles, but the map seems warped/skewed. I made sure that the longitude was set to the x-axis and that latitude was set for the y-axis. The radius of the circles are related to the amount spent on orders. I'm wondering if it has something to do with the scale used, but this is my first time trying to mess with d3, so any help/advice would be appreciated!
var outerWidth = 500;
var outerHeight = 250;
var margin = { left: -50, top: 0, right: -50, bottom: 0 };
var xColumn = "longitude";
var yColumn = "latitude";
var rColumn = "total";
var dollarPerPixel = 10;
var innerWidth = outerWidth - margin.left - margin.right;
var innerHeight = outerHeight - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", outerWidth)
.attr("height", outerHeight);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xScale = d3.scale.log().range([0, innerWidth]);
var yScale = d3.scale.log().range([innerHeight, 0]);
var rScale = d3.scale.sqrt();
function render(data){
xScale.domain( d3.extent(data, function (d){ return d[xColumn]; }));
yScale.domain( d3.extent(data, function (d){ return d[yColumn]; }));
rScale.domain([0, d3.max(data, function (d){ return d[rColumn]; })]);
// Compute the size of the biggest circle as a function of dollarPerPixel.
var dollarMax = rScale.domain()[1];
var rMin = 0;
var rMax = Math.sqrt(dollarMax / (dollarPerPixel * Math.PI));
rScale.range([rMin, rMax]);
var circles = g.selectAll("circle").data(data);
circles.enter().append("circle");
circles
.attr("cx", function (d){ return xScale(d[xColumn]); })
.attr("cy", function (d){ return yScale(d[yColumn]); })
.attr("r", function (d){ return rScale(d[rColumn]); });
circles.exit().remove();
}
function type(d){
d.latitude = +d.latitude;
d.longitude = +d.longitude;
d.total = +d.total;
return d;
}
d3.csv("data.csv", type, render);
While scales may seem to be an appropriate method for plotting geographic points: don't use this approach.
You lose control over rotation of a projection and you cannot use a non cylindrical projection (only unrotated cylindrical projections can plot lat and long independently). But it also makes it very hard to align features positioned by scales with other map elements if they don't use the same approach.
Instead, D3 has a wide range of built in projections.
The projections take a [longitude,latitude] pair and return a [x,y] coordinate. Latitudes and longitudes must be in degrees, x and y are in pixels.
To create a projection you can use:
var projection = d3.geoMercator() // or geoAlbers, geoSupportedProjection, etc.
To use it, just pass it a coordinate:
projection([long,lat]) // [x,y]
In your case this might look like (for the cx, cy looks similar)
.attr("cx", function(d) { return projection([d.long,d.lat])[0] })
Now this projection is centered at 0,0 degrees by default and set up for a 960x500 pixel map. But you can modify scale, center and rotation, for example:
var projection = d3.geoMercator().center([-100,35]).scale(1000)
For a more complete run down of projection methods you should look at the documentation for d3-geo.
In your case there is a special composite projection that covers the US, d3.geoAlbersUsa, which has room for Hawaii and Alaska. But, because of its composite nature is less flexible, though you can still scale it. The default scale anticipates 960x600 pixel map (setting larger map scales spreads the map over a larger area).

How to use d3fc-label-label.js on a map?

I'm trying to position labels on map overlapping-free by using using d3fc-label-label.js in combination with d3.js. While labeling the map by basic d3 functions works well, the approach with the help of d3fc-label-label.js (heavily inspired by this example) produces a map with all the labels placed in top left corner.
Here's the javascript part that does the job
var width = 1300,
height = 960;
var projection = d3.geoMercator()
.scale(500)
// Center the Map to middle of shown area
.center([10.0, 50.5])
.translate([width / 2, height / 2]);
// ??
var path = d3.geoPath()
.projection(projection)
.pointRadius(2);
// Set svg width & height
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// var g = svg.append("g");
d3.json("europe_wgs84.geojson", function(error, map_data) {
if (error) return console.error(error);
// var places = topojson.feature(map_data, map_data.objects.places);
// "path" instead of ".subunit"
svg.selectAll("path")
.data(map_data.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) { return "label " + d.id})
var labelPadding = 2;
// the component used to render each label
var textLabel = fc.layoutTextLabel()
.padding(labelPadding)
//.value(function(d) { return map_data.properties.iso; });
.value(function(d) { return d.properties.iso; });
// use simulate annealing to find minimum overlapping text label positions
var strategy = fc.layoutGreedy();
// create the layout that positions the labels
var labels = fc.layoutLabel(strategy)
.size(function(_, i, g) {
// measure the label and add the required padding
var textSize = d3.select(g[i])
.select('text')
.node()
.getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(function(d) { return projection(d.geometry.coordinates); })
.component(textLabel);
// render!
svg.datum(map_data.features)
.call(labels);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.0/d3.min.js"></script>
See the gist that includes the data and a HTML file.
I would guess the issue is related to append the labels correctly to path of the map. Sadly, I haven't figured it out and would greatly appreciate any help!
I believe the problem lies in the fact that you are not passing single coordinates as the label's position.
layoutLabel.position(accessor)
Specifies the position for each item in the associated array. The
accessor function is invoked exactly once per datum, and should return
the position as an array of two values, [x, y].
In the example you show, that you are basing the design on, the variable places contains point geometries, it is to these points that labels are appended. Looking in the topojson we find places looking like:
"places":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[5868,5064],"properties":{"name":"Ayr"}},{"type":"Point","coordinates":[7508,6637],"properties":{"name":"Aberdeen"}},{"type":"Point","coordinates":[6609,5933],"properties":{"name":"Perth"}},...
Note that geometries.coordinates of each point contains one coordinate. However, in your code, d.geometry.coordinates contains an array of coordinates as it contains the boundary points of the entire path of each feature. This will cause errors in label placement. Instead, you might want to use path.centroid(d), this will return a single coordinate that is at the center of each country/region/path. Placement might not be perfect, as an extreme example, a series of countries arranged as concentric rings will have the same centroid. Here is a basic block showing placement using path.centroid (this shows only the placement - not the formatting of the labels as I'm not familiar with this library extension).
If you were wondering why the linked example's regional labels appear nicely, in the example each region has a label appended at its centroid, bypassing d3fc-label-layout altogether:
svg.selectAll(".subunit-label")
.data(subunits.features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });

Ghost line path when zooming and panning a D3 chart

Learning D3 I created a chart based off this example. The chart is implemented as a JS closure, with Mike Bostock’s convention for creating reusable components in D3. (or as close as I can get)
When zooming and panning, the line path is not redrawn correctly.
In my chart I have a scatter plot and a line path joining the dots. The dots work but not the line. It's (maybe) something to do with rebinding the xScale during the onzoom behavior.... I've tried exposing the line function / object and a bunch of trial and error stuff but am at my wits end. Any help much appreciated.
Please see this codepen, or run the embedded code snippet.
http://codepen.io/Kickaha/pen/epzNyw
var MyNS = MyNS || {};
MyNS.EvalChartSeries = function () {
var xScale = d3.time.scale(),
yScale = d3.scale.linear();
//I tried exposing the line function / object to be able to call it in the on zoom ... no dice.
//var line = d3.svg.line();
var EvalChartSeries = function (selection) {
selection.each(function (dataIn) {
//select and bind data for scatter dots
spots = d3.select(this).selectAll("circle")
.data(dataIn);
//enter and create a circle for any unassigned datum
spots.enter().append("circle");
//update the bound items using the x-y scale function to recalculate
spots
.attr("r", 8)
.attr("cx", function (d) { return xScale(d.dt); })
.attr("cy", function (d) { return yScale(d.spot); })
.style("fill", function (d) {
switch (d.eva) {
case 1: return "green"; break;
case 2: return "red"; break;
case 3: return "blue"; break;
case 4: return "yellow"; break;}
});
//exit to remove any unused data, most likely not needed in this case as the data set is static
spots.exit().remove();
//here the line function/object is assigned it's scale and bound to data
var line = d3.svg.line().x(function (d) { return xScale(d.dt); })
.y(function (d) { return yScale(d.spot); }).interpolate("linear");
//and here is where the line is drawn by appending a set of svg path points
//, it does not use the select, enter, update, exit logic because a line while being a set of points is one thing (http://stackoverflow.com/questions/22508133/d3-line-chart-with-enter-update-exit-logic)
lines = d3.select(this)
.append("path");
lines
.attr('class', 'line')
.attr("d", line(dataIn))
.attr("stroke", "steelblue").attr("stroke-width", 1);
});
};
//The scales are exposed as properties, and they return the object to support chaining
EvalChartSeries.xScale = function (value) {
if (!arguments.length) {
return xScale;
}
xScale = value;
return EvalChartSeries;
};
EvalChartSeries.yScale = function (value) {
if (!arguments.length) {
return yScale;
}
yScale = value;
return EvalChartSeries;
};
/*
Here I tried to expose the line function/object as a property to rebind it to the xAxis when redrawing ... didnt work
EvalChartSeries.line = function (value) {
if (!arguments.length) {
return line;
}
line = value;
//linePath.x = function (d) { return xScale(d.dt); };
return EvalChartSeries;
};*/
//the function returns itself to suppport method chaining
return EvalChartSeries;
};
//The chart is defined here as a closure to enable Object Orientated reuse (encapsualtion / data hiding etc.. )
MyNS.DotsChart = (function () {
data = [{"dt":1280780384000,"spot":1.3173999786376953,"eva":4},
{"dt":1280782184000,"spot":1.3166999816894531,"eva":4},
{"dt":1280783084000,"spot":1.3164000511169434,"eva":4},
{"dt":1280781284000,"spot":1.3167999982833862,"eva":4},
{"dt":1280784884000,"spot":1.3162000179290771,"eva":4},
{"dt":1280783984000,"spot":1.3163000345230103,"eva":4},
{"dt":1280785784000,"spot":1.315999984741211,"eva":4},
{"dt":1280786684000,"spot":1.3163000345230103,"eva":4},
{"dt":1280787584000,"spot":1.316100001335144,"eva":4},
{"dt":1280788484000,"spot":1.3162000179290771,"eva":4},
{"dt":1280789384000,"spot":1.3164000511169434,"eva":4},
{"dt":1280790284000,"spot":1.3164000511169434,"eva":4},
{"dt":1280791184000,"spot":1.3166999816894531,"eva":4},
{"dt":1280792084000,"spot":1.3169000148773193,"eva":4},
{"dt":1280792984000,"spot":1.3170000314712524,"eva":4},
{"dt":1280793884000,"spot":1.3174999952316284,"eva":4},
{"dt":1280794784000,"spot":1.3171000480651855,"eva":4},
{"dt":1280795684000,"spot":1.3163000345230103,"eva":2},
{"dt":1280796584000,"spot":1.315600037574768,"eva":2},
{"dt":1280797484000,"spot":1.3154000043869019,"eva":2},
{"dt":1280798384000,"spot":1.3147000074386597,"eva":2},
{"dt":1280799284000,"spot":1.3164000511169434,"eva":2},
{"dt":1280800184000,"spot":1.3178000450134277,"eva":4},
{"dt":1280801084000,"spot":1.3176000118255615,"eva":4},
{"dt":1280801984000,"spot":1.3174999952316284,"eva":4},
{"dt":1280802884000,"spot":1.3193000555038452,"eva":3},
{"dt":1280803784000,"spot":1.32260000705719,"eva":4},
{"dt":1280804684000,"spot":1.3216999769210815,"eva":4},
{"dt":1280805584000,"spot":1.3233000040054321,"eva":4},
{"dt":1280806484000,"spot":1.3229000568389893,"eva":4},
{"dt":1280807384000,"spot":1.3229999542236328,"eva":2},
{"dt":1280808284000,"spot":1.3220000267028809,"eva":2},
{"dt":1280809184000,"spot":1.3224999904632568,"eva":2},
{"dt":1280810084000,"spot":1.3233000040054321,"eva":2},
{"dt":1280810984000,"spot":1.3240000009536743,"eva":2},
{"dt":1280811884000,"spot":1.3250000476837158,"eva":4},
{"dt":1280812784000,"spot":1.3253999948501587,"eva":4},
{"dt":1280813684000,"spot":1.3248000144958496,"eva":4},
{"dt":1280814584000,"spot":1.3250000476837158,"eva":4},
{"dt":1280815484000,"spot":1.3249000310897827,"eva":4},
{"dt":1280816384000,"spot":1.3238999843597412,"eva":2},
{"dt":1280817284000,"spot":1.3238999843597412,"eva":2},
{"dt":1280818184000,"spot":1.322700023651123,"eva":2},
{"dt":1280819084000,"spot":1.32260000705719,"eva":2},
{"dt":1280819984000,"spot":1.3219000101089478,"eva":2},
{"dt":1280820884000,"spot":1.323199987411499,"eva":4},
{"dt":1280821784000,"spot":1.3236000537872314,"eva":4},
{"dt":1280822684000,"spot":1.3228000402450562,"eva":4},
{"dt":1280823584000,"spot":1.3213000297546387,"eva":2},
{"dt":1280824484000,"spot":1.3214999437332153,"eva":2},
{"dt":1280825384000,"spot":1.3215999603271484,"eva":2},
{"dt":1280826284000,"spot":1.320199966430664,"eva":2},
{"dt":1280827184000,"spot":1.3187999725341797,"eva":2},
{"dt":1280828084000,"spot":1.3200000524520874,"eva":2},
{"dt":1280828984000,"spot":1.3207000494003296,"eva":1}
];
var minDate = d3.min(data, function (d) { return d.dt; }),
maxDate = d3.max(data, function (d) { return d.dt; });
var yMin = d3.min(data, function (d) { return d.spot; }),
yMax = d3.max(data, function (d) { return d.spot; });
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the drawing area
var margin = {top: 20, right: 20, bottom: 30, left: 35},
width = 1600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
//select the single element chart in the html body (this is expected to exist) and append a svg element
var plotChart =d3.select('#chart')
.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 + ')');
var plotArea = plotChart.append('g')
.attr('clip-path', 'url(#plotAreaClip)');//http://stackoverflow.com/questions/940451/using-relative-url-in-css-file-what-location-is-it-relative-to
plotArea.append('clipPath')
.attr('id', 'plotAreaClip')
.append('rect')
.attr({ width: width, height: height });
// Scales
var xScale = d3.time.scale(),
yScale = d3.scale.linear();
// Set scale domains
xScale.domain([minDate, maxDate]);
yScale.domain([yMin, yMax]).nice();
// Set scale ranges
xScale.range([0, width]);
yScale.range([height, 0]);
// Axes
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.ticks(5);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left');
/* var line = d3.svg.line()
.x(function (d) { return xScale(d.dt); })
.y(function (d) { return yScale(d.spot); }).interpolate("linear");
*/
plotChart.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
plotChart.append('g')
.attr('class', 'y axis')
.call(yAxis);
// Data series
var series = MyNS.EvalChartSeries()
.xScale(xScale)
.yScale(yScale);
// .line(line); exposing this property did nothing
//appending a group 'g' tag binding the data and calling on our d3 line+dots chart object to process it
var dataSeries = plotArea.append('g')
.attr('class', 'series')
.datum(data)
.call(series);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Zooming and panning
//on zoom check extents , then most importantny redraw the chart
var zoom = d3.behavior.zoom()
.x(xScale)
.on('zoom', function() {
if (xScale.domain()[0] < minDate) {
zoom.translate([zoom.translate()[0] - xScale(minDate) + xScale.range()[0], 0]);
} else if (xScale.domain()[1] > maxDate) {
zoom.translate([zoom.translate()[0] - xScale(maxDate) + xScale.range()[1], 0]);
}
//most important to redraw "on zoom"
redrawChart();
});
//an overlay area to catch mouse events from the full area of the chart (not just the rendered dots and line)
var overlay = d3.svg.area()
.x(function (d) { return xScale(d.dt); })
.y0(0)
.y1(height);
//an area is a path object, not to be confused with our line path
plotArea.append('path')
.attr('class', 'overlay')
.attr('d', overlay(data))
.call(zoom);
redrawChart();
updateZoomFromChart();
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helper methods
function redrawChart() {
//redraws the scatter data series
dataSeries.call(series);
//redraws the xaxis to show the current zoom pan area
plotChart.select('.x.axis').call(xAxis);
// plotChart.select(".line")
// .attr("class", "line");
// .attr("d", line);
//filters the data set to what is visible given teh current zoom pan state
var yExtent = d3.extent(data.filter(function (d) {
var dt = xScale(d.dt);
return dt > 0 && dt < width;
}), function (d) { return d.spot; });
yScale.domain(yExtent).nice();
//this scales the y axis to maximum visibility as the line is zoomed and panned
plotChart.select(".y.axis").call(yAxis);
}
//takes care of zooming and panning past the ends of the data.
function updateZoomFromChart() {
var fullXDomain = maxDate - minDate,
currentXDomain = xScale.domain()[1] - xScale.domain()[0];
var minXScale = currentXDomain / fullXDomain,
maxXScale = minXScale * 20;
zoom.x(xScale)
.scaleExtent([minXScale, maxXScale]);
}})()
#chart {
margin-top: 20px;
margin-bottom: 20px;
width: 660px;
}.chart .overlay {
stroke-width: 0px;
fill-opacity: 0;
}
.overlay {
stroke-width: 0px;
fill-opacity: 0;
}
body {
padding: 10px 20px;
background: #ffeeee;
font-family: sans-serif;
text-align: center;
color: #7f7;
}.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart"></div>
How do I get the line to redraw correctly?
Thank you for a very well documented question.
What you are doing, is that on zoom you re-draw the line, without removing the one already existing in your SVG element. May I suggest the following:
Change your zoom method to:
var zoom = d3.behavior.zoom()
.x(xScale)
.on('zoom', function() {
if (xScale.domain()[0] < minDate) {
zoom.translate([zoom.translate()[0] - xScale(minDate) + xScale.range()[0], 0]);
} else if (xScale.domain()[1] > maxDate) {
zoom.translate([zoom.translate()[0] - xScale(maxDate) + xScale.range()[1], 0]);
}
// add the following line, to remove the lines already present
d3.selectAll('.line').remove()
//most important to redraw "on zoom"
redrawChart();
});
I am sure there are better ways of doing it, but I think this will get you started.
Hope it helps.

browser unresponsive upon loading larger files

I am download multiple json data files and then visualize that files using D3 data charts. When I load the page it takes lot of time 2-3 minutes to download and visualize, even more on mobile devices. and browser shows a dialog for unresponsiveness. Is there a way to improve load time and handle the load time gracefully?
Every file is of few hundred (100 - 500) KBs and there are 20 - 200 files
Here is a sample code for line chart only
drawLineChart it downloads the json file and extract the data from it, formatLineChartData formats that data to input d3 and finally lineChartConfig draws the chart. similarly there are functions for bar charts, pie charts, word clouds and maps.
var drawLineChart = function(lineChartData, suffix, el){
var n = lineChartData.length;
var dataArray = [];
var labels= '';
var allNull =true; // flag to check if every ajax does not return any data
for (var i=0; i<n; i++){
spark.loadCounterJSON(lineChartData[i].file + suffix,i,
function(res){
var data =res.values;
if(data){
if(lineChartData[res.counter].slug !== 'global'){
allNull = false;
}
var title = Object.keys(data.entities)[0];
graphValues = data[title];
if(graphValues!=''){
labels = data['properties'];
dataArray.push(
formatLineChartData(
graphValues,
lineChartData[res.counter].name,
true
)
);
}
}
if(res.counter === n && !allNull){ // all outer ajax done;
lineChartConfig(el,
dataArray,
false,
true,
''
,labels);
}
});
}
};
var formatLineChartData = function(graphValues, key, xDataType){
formatedData = [];
$.each(graphValues, function(i, v){
value = {};
if(xDataType !== undefined){
value['x'] = new Date(v.x);
}
else {value['x']=v.x;}
value['y']=parseInt(v.y)
formatedData.push(value);
});
return {values:formatedData,key:key};
}
var lineChartConfig = function (div, data, guideline, useDates, auxOptions,labels, width, height) {
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
//var parseDate = d3.time.format("%d-%b-%y").parse;
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");
var line = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
var svg = d3.select(div).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 dataResult;
for(var i=0;i<data.length;i++){
dataResult = data[i].values;
}
//console.log(dataResult);
dataResult.forEach(function(d) {
d.x = new Date(d.x);
d.y = d.y;
});
x.domain(d3.extent(dataResult, function(d) { return d.x; }));
y.domain(d3.extent(dataResult, function(d) { return d.y; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end");
svg.append("path")
.datum(dataResult)
.attr("class", "line")
.attr("d", line);
}
The first thing you want to do is figure out where the problem is. If you have profiling or performance tools you could try those, but the quick dirty way would probably just have your script echo when it finishes downloading a file, when it finishes creating a chart, etc with the current time. This should give you a rough idea of where your time is being spent.
To improve the download speed, you would need to either make the file smaller, not downloading files you don't need, or if the hold up is in the upload speed on your server rather than download speed at the client, improve your infrastructure.
To improve processing speed of the charts... you would need to optimize the code which if you are using a built API you might not have options for. But you definitely want to make sure you aren't making any redundant calls, and check the documentation for any optimization options. Your server side operations could also be improved by multithreading/multiprocessing if possible and you have the hardware to support it.
As for handling it gracefully, the general principle should be to use asynchronous operations as much as possible. For example, if you are loading multiple charts, start each as a progress bar that updates as the data downloads (etc), and then display a chart as soon as it's available. It won't make the process go any faster, but it will keep the page responsive and keep the user informed.

javascript d3.js: initialize brush with brush.extent and stop data from spilling over margin

I have 2 issues I want to fix with my current d3 app which is based off this:
Here's the fiddle: http://jsfiddle.net/zoa5m20z/
I want to initialize my brush so that only a small specific portion is brushed by default when the app starts up. I tried the following with .extent but with no luck.
//date parsing function
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;
//setting up brush and defaultExtent
var brush = d3.svg.brush()
.x(x2)
.extent([parseDate("2014-08-11 10:20:00"), parseDate("2014-08-11 18:20:00")]) //trying to intialize brush
.on("brush", brushed);
I want to stop my plotted circles from overlapping with the yaxis. I'd like to only plot/show circles to the right of the y-axis when the brush is moved. Just like the canonical Bostock Focus+Context via Brushing Block. I tried playing with the margins and the scales, domains, ranges but to no avail yet.
What I'm trying to avoid:
I'm new to d3.js, so all tips & advice is welcome and appreciated!
For your first question, you need to call brush.event to get the brush to pick up the new extent.
brush = d3.svg.brush()
.x(x)
.extent([config.start, d3.time.day.offset(config.start, config.range)])
.on("brush", brushmove)
.on("brushend", brushend);
gBrush = svg.select('.brush')
.call(brush);
gBrush.call(brush.event);
For your second question, I usually just filter out the data that is outside the brush extent such that I am only drawing the visible data. Here is an example that would be called on your brush move/zoom event:
// update the data so that only visible points are shown
var points= svg.selectAll('.point')
.data(function(d) { return onScreen(d, brush.extent); },
function(d) { return d.id; });
// draw the points that are now onscreen
var pointsEnter = points.enter().append('g').attr('class', 'point');
// remove any points that are now offscreen
points.exit().remove();
// up date the x/y position of your points based on the new axis.
// ... left up to you
It's helpful to have a unique id for the points so that they can just be translated to their new positions as the brush moves instead of having to destroy them and redraw them.
I have an example that uses these techniques at http://bl.ocks.org/bunkat/1962173.
Here is a working example based on your code: http://jsfiddle.net/26sd8uc9/4/
1 - You are right about the .extent, the problem is that you haven't specify the domain for you x2 scale. By adding the following code it works:
x2 = d3.time.scale()
.domain([
parseDate("2014-08-11 05:30:00"),
parseDate("2014-08-12 19:25:00")
])
.nice(d3.time.minute)
.range([0, width]);
And to initialize the circles, you also have to call the brushed event after creating the brush by adding .call(brush.event):
// brush slider display
context.append("g")
.attr("class", "x brush")
.call(brush)
.call(brush.event)
.selectAll("rect")
.attr("y", -6)
.attr("height", height2 + 7);
2 - use a variable to keep track where is the current range under the brush, and hide the circles that are not in the range by setting the radius to zero(alternatively you can set the visibility)
var currentRange;
var inRange = function(d) {
if(!currentRange || d.time < currentRange[0] || d.time > currentRange[1] ) {
return 0;
} else {
return 5;
}
}
function brushed() {
currentRange = (brush.empty()? undefined : brush.extent());
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select(".x.axis").call(xAxis);
mydots.selectAll(".circle")
.attr("cx", xMap)
.attr("cy", yMap)
.attr("r", inRange); // set radius to zero if it's not in range
console.log(brush.extent())
}
For the scale definition, it will be better to write something like this:
.domain([
d3.min(dataset, function(d){ return d.time; }),
d3.max(dataset, function(d){ return d.time; })
])
In your example, you have to make sure to parse and initialize the data before you do this.

Categories