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;
});
};
}
});
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 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>
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>
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>
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".