Let's say we draw our scatter plot not with SVG but with canvas.
So something like:
var scale = 1 + Math.floor(Math.random() * 10);
// Redraw axes
x.domain([0, scale]);
y.domain([0, scale]);
xg.call(xAxis);
yg.call(yAxis);
var points = randomPoints(scale),
colors = {};
// Update canvas
context.clearRect(0, 0, width, height);
picker.clearRect(0, 0, width, height);
points.forEach(function(p,i){
// Space out the colors a bit
var color = getColor(i * 1000 + 1);
colors[color] = p;
picker.fillStyle = "rgb(" + color + ")";
context.beginPath();
picker.beginPath();
context.arc(x(p[0]), y(p[1]), 10, 0, 2 * Math.PI);
picker.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
context.fill();
picker.fill();
});
Now how would we pass data into a mouseover event, say, to draw a tooltip?
The examples I've seen for tooltips all take for granted that you are handling an event with data bound to an element moused-over.
But what about for canvas?
I assume you'll need to use d3.bisector or something similar based on the x y coordinates of the mouse event.
One way is to iterate trough all the points and check if the x and y matches the vicinity of the click. This will definitely be slow when the scatter chart points are too many.{I think in your case you are making scatter chart in canvas just to over come that issue}
Other way is to make the use of quad tree.
First I am making some random 10000 points.
sampleData = d3.range(1000).map(function(d) {
var datapoint = {};
datapoint.id = "Sample Node " + d;
datapoint.x = Math.random() * 500;
datapoint.y = Math.random() * 500;
return datapoint;
})
Store all the points in the scatter chart in quad tree like this.
quadtree = d3.geom.quadtree()
.extent([[0,0], [500,500]]) //here 500 is the width and height of the canvas or the max x/y range of the points in scatter chart.
.x(function(d) {return d.x})
.y(function(d) {return d.y});
Pass all the points into quadtree:
quadData = quadtree(sampleData);
Now on click find the associated node data:
quadData = quadtree(sampleData);
d3.select("canvas").on("click", function(){
found = [];
//find in the vicinity of 10 pixel around the click.
search(quadData, d3.event.pageX -10, d3.event.pageY-10, d3.event.pageX +10, d3.event.pageY+10);
var message = "";
//iterate the found and make the message
found.forEach(function(d){
message += d.id + " ";
});
alert("selected Node" + message);
var data
})
Finally my search function to check for the nodes in the quadtree rectangle:
function search(quadtree, x0, y0, x3, y3) {
quadtree.visit(function(node, x1, y1, x2, y2) {
var p = node.point;
if (p) {
p.selected = (p.x >= x0) && (p.x < x3) && (p.y >= y0) && (p.y < y3);
if(p.selected){
found.push(p);
}
}
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
});
}
Click on any of the circle and you will be alerted with the data it holds
working code here
I ended up using a solution suggested by Noah Veltman, as follows:
var margin = {top: 20, right: 10, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
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 factory = d3.geom.quadtree()
.extent([
[0, 0],
[width, height]
]);
var x = d3.scale.linear()
.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("left");
var xg = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
var yg = svg.append("g")
.attr("class", "y axis");
var chartArea = d3.select("body").append("div")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
var canvas = chartArea.append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
context.fillStyle = "#f0f";
// Layer on top of canvas, example of selection details
var highlight = chartArea.append("svg")
.attr("width", width)
.attr("height", height)
.append("circle")
.attr("r", 7)
.classed("hidden", true);
redraw();
function redraw() {
// Randomize the scale
var scale = 1 + Math.floor(Math.random() * 10);
// Redraw axes
x.domain([0, scale]);
y.domain([0, scale]);
xg.call(xAxis);
yg.call(yAxis);
var points = randomPoints(scale);
var tree = factory(points);
// Update canvas
context.clearRect(0, 0, width, height);
points.forEach(function(p,i){
context.beginPath();
context.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
context.fill();
});
canvas.on("mousemove",function(){
var mouse = d3.mouse(this),
closest = tree.find([x.invert(mouse[0]), y.invert(mouse[1])]);
highlight.attr("cx", x(closest[0]))
.attr("cy", y(closest[1]));
});
canvas.on("mouseover",function(){
highlight.classed("hidden", false);
});
canvas.on("mouseout",function(){
highlight.classed("hidden", true);
});
}
function randomPoints(scale) {
// Get points
return d3.range(1000).map(function(d){
return [
Math.random() * scale,
Math.random() * scale
];
});
}
The key is the use of
var factory = d3.geom.quadtree()
.extent([
[0, 0],
[width, height]
]);
Which has a getter setter function based on the x, y value pair of each point.
Related
Axises drawn well done, but path is invisible.And no error messages in the console. Please help understand where is the problem.
function Chart(id, data) {
var chartData = JSON.parse(data);
const height = 800;
const width = 900;
var svg = d3.select(id)
.append("svg")
.attr("width", width)
.attr("height", height);
var parseDate = d3.timeParse("%d.%m.%Y %H:%M:%S");
var lineLvl = d3.line()
.x(function (d) {
return Date.parse(parseDate(d.timeData));
})
.y(function (d)
{
return d.lvlData;
});
var xscale =
d3.scaleTime()
.domain([Date.parse(parseDate(chartData[0].timeData))-60000,
Date.parse(parseDate(chartData[chartData.length - 1].timeData)) + 60000])
.range([0, width*0.95]);
var yscale = d3.scaleLinear()
.domain([0, d3.max(chartData, function (d) { return d.lvlData*1.1; } )] )
.range([height / 2, 0]);
var x_axis = d3.axisBottom().scale(xscale)
.tickFormat(d3.timeFormat("%H:%M"));
svg.append("path")
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("d", lineLvl(chartData));
var y_axis = d3.axisLeft().scale(yscale);
svg.append("g")
.attr("transform", "translate(50, 10)")
.call(y_axis);
var xAxisTranslate = height / 2 + 10;
svg.append("g")
.attr("transform", "translate(50, " + xAxisTranslate + ")")
.call(x_axis);
}
Data represented by array of structures like this
{"timeData":"17.07.2020 14:38:10","lvlData":"146575","temperatureData":"24","volumeData":"6155"}
You need to apply the scales to your data when plotting it:
var lineLvl = d3.line()
.x(function (d) {
return xscale(Date.parse(parseDate(d.timeData))); // run the x value through the x scale
})
.y(function (d)
{
return yscale(d.lvlData); // run the y value through the y scale
});
The reason you see no error is because the line is drawn, but with unscaled values it is drawn a long ways off the SVG. If you inspect the SVG, you'll see the path, it's drawn, just that its coordinates reflect the raw data, not the scaled data.
I am new to the d3 graphing library and all the tutorials I've seen for loading JSON data for a graph have been using a url and a callback function. I already have the JSON data in a JSON object in the same function in which I am looking to graph (the JSON data is stored in a variable in the same file as the graphing function). How do I use that JSON object in my graphs? This is my code. The JSON object is called 'data'.
function showBargraph() {
var calories = 0;
var protein = 0;
var carbohydrates = 0;
var fat = 0;
for (var i in graphdata) {
calories = calories + parseInt(graphdata[i].calories);
protein = protein + parseInt(graphdata[i].protein);
carbohydrates = carbohydrates + parseInt(graphdata[i].carbohydrates);
fat = fat + parseInt(graphdata[i].fat);
}
//THIS IS THE JSON OBJECT!!!!!!!!!!!!!!!!!!!!!!!!!!
var data = {
'calories': calories,
'protein': protein,
'carbohydrates': carbohydrates,
'fat': fat
}
// set the dimensions and margins of the graph
var margin = {top: 30, right: 30, bottom: 70, 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("#myBarChart")
.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 + ")");
//THIS IS WHERE I NEED HELP!!!!!!!!!!!!!!!!!!!!!!!!
d3.json(data, function(d) {
console.log(d);
});
// X axis
var x = d3.scaleBand()
.range([ 0, width ])
.domain(data.map(function(d) { return d.Country; }))
.padding(0.2);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "translate(-10,0)rotate(-45)")
.style("text-anchor", "end");
// Add Y axis
var y = d3.scaleLinear()
.domain([0, 13000])
.range([ height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
var trace1 = {
x: ['Calories', 'Protein', 'Carbohydrates', 'Fat'],
y: [calories, protein, carbohydrates, fat],
type: 'bar'
};
I have a working example of a choropleth that takes data values from .csv file and calculates the color ranges.
There are four possible colors that a region can take, depending on the population.
For application I am trying to develop, I need data to be loaded by server from a database. As a dummy example, I made a route that returns json.
json.features contains the geojson information, and json.features.properties.value - population value.
Here is the function I use. It draws a map correctly, and the population values on tooltips are correct as well. The color domain is [98, 629725]. However there is just one region that takes the very dark color, only four regions that take a little brighter one, and all other regions are taking the other color. If you look at the link I provide at the top - there the distribution of color is more uniform. But in the map made with this code - I have the same color for place with 5000 population and 90000 population.
I'm stuck right now because I don't really see where could the problem come from, and I don't have a good idea on how to investigate it. Can you suggest what would be the first things to check in this situation? Maybe you already see something wrong with this code?
function draw_map (dataroute) {
var w = 500;
var h = 800;
var projection = d3.geo.transverseMercator()
.rotate([-27,-65,0])
.translate([w/2, h/2])
.scale([3500])
var path = d3.geo.path()
.projection(projection);
var color = d3.scale.quantize()
.range(["#c6dbef","#6baed6","#2171b5","#084594"])
.domain([0, 100]);
var svg = d3.select("#map")
.attr("width", w)
.attr("height", h);
svg.append("rect")
.attr("class", "background")
.attr("width", w)
.attr("height", h);
var g = svg.append("g")
queue()
.defer(d3.json, dataroute)
.await(ready);
function ready(error, json) {
if (error) throw error;
color.domain([
d3.min(json.features, function(d) { return d.properties.value; }),
d3.max(json.features, function(d) { return d.properties.value; })
]);
console.log([
d3.min(json.features, function(d) { return d.properties.value; }),
d3.max(json.features, function(d) { return d.properties.value; })
]);
// returns [98, 629725]
g.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d",path)
.style("fill", colormap)
.style("stroke", "#08306b")
.attr("class", "feature")
.on("mouseover", function(d) {
d3.select(this)
.style("fill", "#08306b");
var coordinates = [0, 0];
coordinates = d3.mouse(this);
var target = d3.select("#tooltip")
.style("left", coordinates[0] + "px")
.style("top", coordinates[1]-80 + "px");
target.select("#name")
.text(d.properties.text);
target.select("#stat")
.text(json.statdata_name + ": " + d.properties.value);
d3.select("#tooltip").classed("hidden", false);
})
.on("mouseout", function(d){
// If this is active node, keep the color on mouseout
d3.select(this)
.style("fill", colormap(d));
d3.select("#tooltip").classed("hidden", true);
});
}
//Return color for path
var colormap = function(d) {
var value = d.properties.value;
//console.log(value);
// If value exists, map to color
if (value) {
return color(value);
//console.log(color(value));
// if not, set color red
} else {
console.log("Undefined: " + d.properties.text + " " + value)
return "red";
}
}
}
In the original version that uses .csv file, the code is like this:
//Width and height
var w = 800;
var h = 800;
active = d3.select(null);
previous = d3.select(null);
var projection = d3.geo.transverseMercator()
.rotate([-27,-65,0])
.translate([w/2, h/2])
.scale([3500])
var path = d3.geo.path()
.projection(projection);
var color = d3.scale.quantize()
.range(["#c6dbef","#6baed6","#2171b5","#084594"])
.domain([0, 100]);
var svg = d3.select("#map")
.attr("width", w)
.attr("height", h);
svg.append("rect")
.attr("class", "background")
.attr("width", w)
.attr("height", h)
.on("click", reset);
var g = svg.append("g")
// Click to zoom
function clicked(d) {
// If this node is zoomed, unzoom
if (active.node() === this) {
d3.select(this).style("fill",colormap)
return reset();
} else {
// Otherwise recolor previous one, to avoid contamination
previous.style("fill",colormap)
}
active.classed("active", false);
active = d3.select(this).classed("active", true);
// store previous to uncolor it after clicking new one
previous = d3.select(this)
d3.select(this).style("fill","#08306b")
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .3 / Math.max(dx / w, dy / h),
translate = [w / 2 - scale * x, h / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
function reset() {
active.classed("active", false);
active = d3.select(null);
g.transition()
.duration(750)
.style("stroke-width", "1px")
.attr("transform", "");
}
queue()
.defer(d3.json, "/static/finland.geojson")
.defer(d3.csv, "/static/kuntavakiluku.csv")
.await(ready);
var finland_geojson_data
var csv_data
function ready(error, json, data) {
if (error) throw error;
finland_geojson_data = json;
csv_data = data;
color.domain([
d3.min(data, function(d) { return d.Vakiluku; }),
d3.max(data, function(d) { return d.Vakiluku; })
]);
console.log(data.length);
for (var i = 0; i < data.length; i++) {
var dataState = data[i].Kunta;
var dataValue = parseInt(data[i].Vakiluku);
//Find the corresponding state inside the GeoJSON
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.text;
//console.log(jsonState)
if (dataState == jsonState ) {
//Copy the data value into the JSON
json.features[j].properties.value = dataValue;
//Stop looking through the JSON
break;
}
}
}
g.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d",path)
.style("fill", colormap)
.style("stroke", "#08306b")
.attr("class", "feature")
.on("mouseover", function(d) {
d3.select(this)
.style("fill", "#08306b");
var coordinates = [0, 0];
coordinates = d3.mouse(this);
var target = d3.select("#tooltip")
.style("left", coordinates[0] + "px")
.style("top", coordinates[1]-80 + "px");
target.select("#kunta")
.text(d.properties.text);
target.select("#vakiluku")
.text("Väestö: " + d.properties.value);
d3.select("#tooltip").classed("hidden", false);
})
.on("mouseout", function(d){
// If this is active node, keep the color on mouseout
if (active.node() === this) {
d3.select(this).style("fill","#08306b")
} else {
d3.select(this)
.style("fill", colormap(d));
}
d3.select("#tooltip").classed("hidden", true);
})
.on("click",clicked);
}
//Return color for path
var colormap = function(d) {
var value = d.properties.value;
// If value exists, map to color
if (value) {
return color(value);
// if not, set color red
} else {
console.log("Undefined: " + d.properties.text + " " + value)
return "red";
}
}
Quantized scales are linear scales with discreet ranges (as you can see in the link that #seb provided in the comments).
So in your case, if you provide 4 colors, the domain, figuratively speaking, will be split up in 4 sections (and your scale is a "map" for that).
Thus, the first quarter of the domain will be the first color, etc.
In your expl, the domain is [98, 629725], so the first color will be starting at 98 and end at (629725-98)/4+98 = 157'504.75
In code:
var scale = d3.scale.quantize()
.range(["#c6dbef", "#6baed6", "#2171b5", "#084594"])
.domain([98, 629725]);
So you can test that, e.g. here (check console output!)
You can see that 157504 yields col1, 157505 the second.
So it it no surprise that 5000 and 90000 are in the same "section".
Long time lurker, enthusiastic about learning D3! Anywhoo, I'm working on following Mr. Bostock's paths examples.
Now, I have a fiddle that is working just wonderfully - it is smooth, updates quickly, scales the x-axis and y-axis quite nicely.
This is a time series chart I'm trying to tackle. Basically, I'm taking sensor integers that go into a JSON array. My target is to receive this data in realtime; Sensor sends a temp, and I get a smooth reflection of that data on the chart in near realtime. Here's the rub. As soon as I add in the JSON fetching code, everything goes from smooth as butter to choppy and crazy. Here's what I'm working with... Please be understand that I am very new to D3.
Here is the nicely randomized code which displays the graph in the way I want:
var n = 40,
duration = 750,
now = new Date(Date.now() - duration),
random = d3.random.normal(0, 0.2),
count = 0,
data = d3.range(n).map(random);
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 40
},
width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var x = d3.time.scale()
.domain([now - (n - 2) * duration, now - duration])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(data)])
.range([height, 0]);
var line = d3.svg.line()
.interpolate("basis")
.x(function (d, i) {
return x(now - (n - 1 - i) * duration);
})
.y(function (d, i) {
return y(d);
});
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 + ")");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var axis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
var yAx = svg.append("g")
.attr("class", "y axis")
.call(y.axis = d3.svg.axis().scale(y).orient("left"));
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "line");
var transition = d3.select({}).transition()
.duration(750)
.ease("linear");
tick();
function tick() {
transition = transition.each(function () {
// update the domains
now = new Date();
x.domain([now - (n - 2) * duration, now - duration]);
y.domain([0, d3.max(data) + 10]);
// push a new data point onto the back
data.push(20 + Math.random() * 100);
// redraw the line, and slide it to the left
path.attr("d", line)
.attr("transform", null);
// slide the x-axis left, rescale the y-axis
axis.call(x.axis);
yAx.call(y.axis);
// slide the line left
path.transition()
.attr("transform", "translate(" + x(now - (n - 1) * duration) + ")");
// pop the old data point off the front
data.shift();
}).transition().each("start", tick);
}
svg {
font: 10px sans-serif;
}
.line {
fill: none;
stroke: orange;
stroke-width: 1.5px;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Now, I realize that my poor understanding of transitions is probably getting me stuck. That being said, I need help! My trouble starts when I add the following:
First, I add var JSONdata = []; as a global, then right after function tick() {
transition = transition.each(function () { I add my JSON code:
d3.json("tempdata.php", function(error, data){
JSONdata = data;
data.forEach(function(d) {
JSONdata.temp = +d.temp;
})
I finish by wrapping up the end of the whole tick block with the final })
So it works... but it's slow and choppy! Methinks there be dragons in my transitions...? I'd like to have as near real time as possible, but even up to 5 seconds as a buffered delay would be acceptable.
Can anyone lend a hand or some enlightenment?
I'd like the graph to display a constant line with the last updated value, so even if the sensor goes offline, I'll see the last value. Granted, eventually I need to work out a round robin database type deal and actually face the issue of better timestamp handling. But, that can be for another day.
My JSFiddle with just the randomized data: http://jsfiddle.net/6z9qe46e/7/
I am quite new to d3.js. I am trying to make a scatter plot using the data present in a csv file. From the csv file, I am utilizing data from two columns.
d3.csv("test.csv",function (data) {
var margin = {top: 30, right: 10, bottom: 50, left: 60},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var xMax = d3.max(data, function(d) { return +d.Survivaltime; }),
xMin = 0,
yMax = d3.max(data, function(d) { return +d.Year; }),
yMin = 1950;
//Define scales
var x = d3.scale.linear()
.domain([xMin, xMax])
.range([0, width]);
var y = d3.scale.linear()
.domain([yMin, yMax])
.range([height, 0]);
});
// the chart object, includes all margins
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
// the main object where the chart and axis will be drawn
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
.tickSize(-height)
.tickFormat(d3.format("s"));
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
.ticks(5)
.tickSize(-width)
.tickFormat(d3.format("s"));
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
// draw the graph object
var svg = main.append("svg:g");
g.selectAll("scatter-dots")
.data(d.Year) // using the values in the ydata array
.enter().append("svg:circle") // create a new circle for each value
.attr("cy", function (d) { return y(d.Year); } ) // translate y value to a pixel
.attr("cx", function (d) { return x(d.Survivaltime); } ) // translate x value
.attr("r", 10) // radius of circle
.style("opacity", 0.6); // opacity of circle
here'sa link to the csv file:
http://bit.ly/14oLdml
Please help me.
Looks good for the most part. The only problems I could see (although there may be others that I'm missing) are near the end:
// draw the graph object
//var svg = main.append("svg:g");// <---- this is causing a bug. Should be:
var g = main.append("svg:g");
Then fix:
g.selectAll(".scatter-dots"); // <---- added a period before scatter-dots
The other thing, which is not breaking your code, but should be fixed:
After you .append("svg:circle"), you should call .attr("class", "scatter-dots") on it.