d3 mouse event, add circles on click - html

I need to add in my code that when I click it, more circles and of different radius are added, I have been stuck up to this point
function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "black");
}
function dragged(event, d) {
d3.select(this).attr("cx", d.x = event.x).attr("cy", d.y = event.y);
}
function dragended(event, d) {
d3.select(this).attr("stroke", null);
}
const drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
const height = 300,
width = 700
radius = 20
const svg = d3.select("svg")
.attr("viewBox", [0, 0, width, height])
.attr("stroke-width", 2);
const circles = d3.range(10).map(i => ({
x: Math.random() * (width - radius * 1) + radius,
y: Math.random() * (height - radius * 1) + radius,
}));
svg.selectAll("circle")
.data(circles)
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", radius)
.attr("fill", (d, i) => d3.schemePastel1[i % 100])
.call(drag);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>
I need to create something like that, that can move the circles and add more circles by clicking

Consider the following:
Move the drawing to a callable function
On click, push a new circle to the array of circles, using the clientX and clientY of the event and redraw.
function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "black");
}
function dragged(event, d) {
d3.select(this).attr("cx", d.x = event.x).attr("cy", d.y = event.y);
}
function dragended(event, d) {
d3.select(this).attr("stroke", null);
}
const drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
const height = 300,
width = 700,
maxRadius = 20;
const circles = d3.range(10).map(i => ({
x: Math.random() * (width - maxRadius * 2) + maxRadius,
y: Math.random() * (height - maxRadius * 2) + maxRadius,
radius: Math.random() * (maxRadius - 2) + 2,
}));
const svg = d3.select("svg")
.attr("viewBox", [0, 0, width, height])
.attr("stroke-width", 2)
.on("click", (event) => {
console.log(event);
circles.push({
x: event.clientX,
y: event.clientY,
radius: Math.random() * (maxRadius - 2) + 2,
});
drawCircles();
})
function drawCircles() {
svg.selectAll("circle")
.data(circles)
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.radius)
.attr("fill", (d, i) => d3.schemePastel1[i % 9])
.call(drag);
}
drawCircles();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>

Related

Trying to draw a bar chart in D3 and I am not seeing the output [duplicate]

This question already has an answer here:
Cannot use attr with an object in D3 v4
(1 answer)
Closed 4 years ago.
I am learning D3 and trying to create a bar chart. I dont know why there is no output.
var w = 300;
var h = 100;
var padding = 2;
var dataset = [5, 10, 20, 12, 19];
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr({
x: function(d, i) {
return i * w / dataset.length;
},
y: function(d) {
return h - d;
},
width: function(d) {
return (w / dataset.length) - padding;
},
height: function(d) {
return d;
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
The issue is that you are using d3v4, which does not let you set multiple attributes with selection.attr(). This functionality was present in d3v3, but removed starting with d3v4.
With d3v4/5 you can either:
use d3.selection-multi
set attributes one at a time
To keep almost the same code as you have, you can import d3.selection-multi, this allows you to use selection.attrs({}) (not .attr) to set multiple attributes at once:
.attrs({
x: function(d, i) {
return i * w / dataset.length;
},
y: function(d) {
return h - d;
},
width: function(d) {
return (w / dataset.length) - padding;
},
height: function(d) {
return d;
}
})
The module is not included in the base d3 library so you'll need to import it for this approach to work:
var w = 300;
var h = 100;
var padding = 2;
var dataset = [5, 10, 20, 12, 19];
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attrs({
x: function(d, i) {
return i * w / dataset.length;
},
y: function(d) {
return h - d;
},
width: function(d) {
return (w / dataset.length) - padding;
},
height: function(d) {
return d;
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
The other option is to use selection.attr() to set one attribute at a time, rather than all at once:
var w = 300;
var h = 100;
var padding = 2;
var dataset = [5, 10, 20, 12, 19];
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x",function(d, i) {
return i * w / dataset.length;
})
.attr("y",function(d) {
return h - d;
})
.attr("width", function(d) {
return (w / dataset.length) - padding;
})
.attr("height",function(d) {
return d;
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

D3v4 zoom coordinates of visible area

I'm trying to calculate the coordinates of the visible area after zooming. Is there a way to calculate it using d3.event.transform?
I tried a lot but couldn't make it work, at least not for d3v4. Using v3 is no opinion since everything else of the project is using v4.
Isolated Code: https://jsfiddle.net/qyvnhvmj/
All you need to do is grab the inverse of the points from the transform,
var width = 800,
height = 400,
Radius = 20;
var Circles, Data = [];
for (var i = 0; i < 3; i++) {
Data.push({
x: 100 + i * Radius * 8,
y: 100,
});
}
var zoom = d3.zoom().scaleExtent([1, 128]).on("zoom", zoomed);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.style("fill", d3.color("gray"))
.attr("opacity", 0.1);
Circles = svg.selectAll("circle")
.data(Data)
.enter().append("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", Radius);
function getVisibleArea(t) {
var l = t.invert([0, 0]),
r = t.invert([width, height]);
return Math.trunc(l[0]) + " x " + Math.trunc(l[1]) + " - " + Math.trunc(r[0]) + " x " + Math.trunc(r[1]);
}
function zoomed(d) {
Circles.attr("transform", d3.event.transform);
console.log("zoom transform: ", d3.event.transform);
d3.select("#area span").text(getVisibleArea(d3.event.transform));
}
svg {
position: absolute;
top: 50;
}
<svg width="800" height="400"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<a id="area">visible area: <span>?</span></a>
<br>
<br>

D3.js mouseovers on html5 canvas scatter plot

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.

d3.scale.quantize producing unexpected result, how to debug?

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".

Duplicate row in CSV with D3.js

Here is my problem: I have a CSV file that have the following colums
Item type (car, moto, bus,...)
Owner
City/State
Number (how many cars/moto/bus a specific owner owns)
I want to make a dynamic bubble D3.js chart. Each bubble should represent an item and I want that the user can sort them by type, owner or state. I found a script that is working well except that it takes each line from the CSV to make a bubble. I'm trying to find a way to "duplicate" the bubble when number < 1 but until now I didn't find...
What is your advice? Should I use D3.js to do that? jQuery? Other?
Here is the code:
d3.csv('data/test.csv', function (error, data) {
var width = 600, height = 600;
var fill = d3.scale.ordinal().range(['#827d92','#827354','#523536','#72856a','#2a3285'])
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height);
_.each(data, function (elem) {
elem.radius = 5;
elem.x = _.random(0, width);
elem.y = _.random(0, height);
});
var padding = 2;
var maxRadius = d3.max(_.pluck(data, 'radius'));
function getCenters(vname, w, h) {
var nodes = [], c =[], result = {};
var v = _.uniq(_.pluck(data, vname));
var l = d3.layout.treemap().size([w, h]).ratio(1/1);
_.each(v, function (k, i) {
c.push({name: k, value: 1});
});
nodes = l.nodes({children: c})[0].children;
for (var i = 0; i < nodes.length; i++) {
result[nodes[i].name] = nodes[i];
}
return result;
}
var nodes = svg.selectAll("circle")
.data(data);
nodes.enter().append("circle")
.attr("class", "node")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", 2)
.style("fill", function (d) { return fill(d.cat_int); })
.on("mouseover", function (d) { showPopover.call(this, d); })
.on("mouseout", function (d) { removePopovers(); })
nodes.transition().duration(1000)
.attr("r", function (d) { return d.radius; })
var force = d3.layout.force()
.charge(0)
.gravity(0)
.size([width, height])
draw('cat_int');
$( ".btn" ).click(function() {
draw(this.id);
});
function draw (varname) {
var foci = getCenters(varname, 600, 600);
force.on("tick", tick(foci, varname, .45));
labels(foci)
force.start();
}
function tick (foci, varname, k) {
return function (e) {
data.forEach(function(o, i) {
var f = foci[o[varname]];
o.y += ((f.y + (f.dy / 2)) - o.y) * k * e.alpha;
o.x += ((f.x + (f.dx / 2)) - o.x) * k * e.alpha;
});
nodes
.each(collide(.1))
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
}
}
function labels (foci) {
svg.selectAll(".label").remove();
svg.selectAll(".label")
.data(_.toArray(foci)).enter().append("text")
.attr("class", "label")
.text(function (d) { return d.name })
.attr("transform", function (d) {
return "translate(" + (d.x + (d.dx / 2)) + ", " + (d.y + 20) + ")";
});
}
function removePopovers () {
$('.popover').each(function() {
$(this).remove();
});
}
function showPopover (d) {
$(this).popover({
placement: 'auto top',
container: 'body',
trigger: 'manual',
html : true,
content: function() {
return "Category: " + d.cat_int + "<br/> Owner: " + d.owner + "<br/>Sate: " + d.state; }
});
$(this).popover('show')
}
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
});