d3 Hexagonal Binning domain and range - javascript

I'm trying to create a hexagonal binning graph based on this example: http://bl.ocks.org/mbostock/4248145
I've already figured out how to set the domain of each axis. But I'm having troubles setting the points, and I think this is because of the range.
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 800 - margin.left - margin.right, // 740px
height = 500 - margin.top - margin.bottom; // 450px
var points = [[1000,30],[5000,40],[8000,50]]
var x = d3.scale.linear().domain([0, 10000]).range([0, width]);
var y = d3.scale.linear().domain([18, 65]).range([height, 0]);
Using these points, the graph comes out blank.
But if I try these points:
var points = [[100,30],[200,40],[300,50]]
They appear on the graph but not even close to where they should be. [100,30], for example, it appears what should be something like [1400,62].
I already read this post about scales on d3. But I haven't figured out how to display the points correctly.
JSFiddle: http://jsfiddle.net/P6KWZ/

There are two things in your jsfiddle. First, you're not actually using the scales you've created to position the hexagons. Second, you're computing the domains based on the original values and the drawing the values computed by the hexbin.
So first compute the domains based on the binned values:
var projectedPoints = hexbin(points);
var x = d3.scale.linear()
.domain(d3.extent(projectedPoints, function(d) { return d.x; }))
.range([0, width]);
var y = d3.scale.linear()
.domain(d3.extent(projectedPoints, function(d) { return d.y; }))
.range([height, 0]);
And then translate the hexagons using the scales:
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; })
Complete demo here.

Related

Is there an "axis equal" in D3?

I am using D3 and Python to create some plots. With Python I am able to do axis equal, as shown in image 1:
I want to achieve the same in an SVG using D3. Please refer to the image 2, without axis equal:
The axes scale which I am using are as follows:
var xScale = d3.scaleLinear()
.range([0, width])
.domain([minX, maxX]);
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([minY, maxY]);
var xAxis = d3.axisBottom(xScale).ticks(5),
yAxis = d3.axisLeft(yScale).ticks(5);
Could anyone tell me how can I achieve the axis equal with D3?
Short answer: no, there is no "axis equal" in D3. D3 is quite low level, it's just a collection of methods, so you have to do almost everything by hand...
The good news is that all you need for solving your problem is some math to calculate the new ranges.
As an introduction, since this is a d3.js tagged question, "axis equal" is a term used in some programs like Matlab, which...
Use the same length for the data units along each axis.
It can be better explained visually. Have a look at this image, with different domains and ranges (source):
After this brief introduction, let's go back to your problem. Suppose this simple code, generating two axes:
const minX = 0,
minY = 0,
maxX = 120,
maxY = 50,
width = 500,
height = 300,
paddings = [10, 10, 20, 30];
const xScale = d3.scaleLinear()
.range([paddings[3], width - paddings[1]])
.domain([minX, maxX]);
const yScale = d3.scaleLinear()
.range([height - paddings[2], paddings[0]])
.domain([minY, maxY]);
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("transform", `translate(0,${height - paddings[2]})`)
.call(d3.axisBottom(xScale).tickSizeInner(-height + paddings[2] + paddings[0]));
svg.append("g")
.attr("transform", `translate(${paddings[3]},0)`)
.call(d3.axisLeft(yScale).tickValues(xScale.ticks()).tickSizeInner(-width + paddings[3] + paddings[1]));
svg {
border: 2px solid tan;
}
line {
stroke-dasharray: 2, 2;
stroke: gray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
As we can clearly see, the units are not equally spaced. We can do two things for creating axes with equally spaced units:
1. Change the domains;
2. Change the ranges.
According to your comment, changing the domains is not an option. So, let's change the ranges instead.
All we need is calculating the number of units per user space, and using the biggest among them (x or y) to change the range of the opposite scale.
For instance, given the scales of the snippet above:
const xStep = Math.abs((xScale.domain()[1] - xScale.domain()[0]) / (xScale.range()[1] - xScale.range()[0]));
const yStep = Math.abs((yScale.domain()[1] - yScale.domain()[0]) / (yScale.range()[1] - yScale.range()[0]));
xStep is bigger than yStep, showing us that the x axis has more units per pixels. Therefore, we'll change the y axis range:
yScale.range([yScale.range()[0], yScale.range()[0] - (yScale.domain()[1] - yScale.domain()[0]) / xStep]);
And here is the demo:
const minX = 0,
minY = 0,
maxX = 120,
maxY = 50,
width = 500,
height = 300,
paddings = [10, 10, 20, 30];
const xScale = d3.scaleLinear()
.range([paddings[3], width - paddings[1]])
.domain([minX, maxX]);
const yScale = d3.scaleLinear()
.range([height - paddings[2], paddings[0]])
.domain([minY, maxY]);
const xStep = Math.abs((xScale.domain()[1] - xScale.domain()[0]) / (xScale.range()[1] - xScale.range()[0]));
const yStep = Math.abs((yScale.domain()[1] - yScale.domain()[0]) / (yScale.range()[1] - yScale.range()[0]));
yScale.range([yScale.range()[0], yScale.range()[0] - (yScale.domain()[1] - yScale.domain()[0]) / xStep]);
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("transform", `translate(0,${height - paddings[2]})`)
.call(d3.axisBottom(xScale).tickSizeInner(-height + paddings[2] + yScale.range()[1]));
svg.append("g")
.attr("transform", `translate(${paddings[3]},0)`)
.call(d3.axisLeft(yScale).tickValues(yScale.ticks().filter(function(d){return !(+d%10)})).tickSizeInner(-width + paddings[3] + paddings[1]));
svg {
border: 2px solid tan;
}
line {
stroke-dasharray: 2, 2;
stroke: gray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
As you can see using the gridlines, the units are equally spaced now.
Finally, have in mind that determining programmatically what scale will be changed, readjusting the correct range (your y scale, for instance, goes from the bottom to the top), centralising the axes etc are whole different issues.

D3.js violin plot not showing up

Sorry in advance, I am very new to javascript. I am trying to use this code https://www.d3-graph-gallery.com/graph/violin_basicDens.html with my own data.
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 40},
width = 1200 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Read the data and compute summary statistics for each specie
d3.csv("violinsummary.csv", function(data) {
// Show the X scale
var x = d3.scaleBand()
.range([0, width])
.domain(["2017-09", "2017-10","2018-02","2018-03"])
.paddingInner(1)
.paddingOuter(.5);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Show the Y scale
var y = d3.scaleLinear()
.domain([80, 100])
.range([height, 0]);
svg.append("g").call(d3.axisLeft(y));
// Features of density estimate
var kde = kernelDensityEstimator(kernelEpanechnikov(.2), y.ticks(50));
// Compute the binning for each group of the dataset
var sumstat = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) {
return d.DATE;})
.rollup(function(d) { // For each key..
input = d.map(function(g) {
return g.Power;});
density = kde(input); // And compute the binning on it.
return(density);
})
.entries(data);
console.log(input);
console.log(density);
// What is the biggest value that the density estimate reach?
var maxNum = 0;
for ( i in sumstat ){
allBins = sumstat[i].value;
kdeValues = allBins.map(function(a){return a[1]});
biggest = d3.max(kdeValues);
if (biggest > maxNum) { maxNum = biggest }
}
console.log(allBins);
console.log(kdeValues);
console.log(biggest);
// The maximum width of a violin must be x.bandwidth = the width dedicated to a group
var xNum = d3.scaleLinear()
.range([0, x.bandwidth()])
.domain([-maxNum,maxNum]);
console.log(sumstat);
// Add the shape to this svg!
svg
.selectAll("myViolin")
.data(sumstat)
.enter() // So now we are working group per group
.append("g")
.attr("transform", function(d){ return("translate(" + x(d.key) +" ,0)") } ) // Translation on the right to be at the group position
.append("path")
.datum(function(d){ return(d.value)}) // So now we are working density per density
.style("stroke", "none")
.style("fill","#69b3a2")
.attr("d", d3.area()
.x0(function(d){ return(xNum(-d[1])) } )
.x1(function(d){ return(xNum(d[1])) } )
.y(function(d){ return(y(d[0])) } )
.curve(d3.curveCatmullRom) // This makes the line smoother to give the violin appearance. Try d3.curveStep to see the difference
)
});
// 2 functions needed for kernel density estimate
function kernelDensityEstimator(kernel, X) {
return function(V) {
return X.map(function(x) {
return [x, d3.mean(V, function(v) { return kernel(x - v); })];
});
};
}
function kernelEpanechnikov(k) {
return function(v) {
return Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0;
};
}
</script>
I believe the part of the code where I am adding the shape to the svg is incorrect. All my console log outputs show the correct data. I also ran console log outputs for the example and my data and the example's data have the same data types throughout.
Data (violinsummary.csv):
Power,DATE
89.29,2017-09
89.9,2017-09
91.69,2017-09
89.23,2017-09
91.54,2017-09
88.49,2017-09
89.15,2017-09
90.85,2017-09
89.59,2017-09
93.38,2017-10
92.41,2017-10
90.65,2017-10
91.07,2017-10
90.13,2017-10
91.73,2017-10
91.09,2017-10
93.21,2017-10
91.62,2017-10
89.58,2017-10
90.59,2017-10
92.57,2017-10
89.99,2017-10
90.59,2017-10
88.12,2017-10
91.3,2017-10
89.59,2018-02
91.9,2018-02
87.83,2018-02
90.36,2018-02
91.38,2018-02
91.56,2018-02
91.89,2018-02
90.95,2018-02
90.15,2018-02
90.24,2018-02
94.04,2018-02
85.4,2018-02
88.47,2018-02
92.3,2018-02
92.46,2018-02
92.26,2018-02
88.78,2018-02
90.13,2018-03
89.95,2018-03
92.98,2018-03
91.94,2018-03
90.29,2018-03
91.2,2018-03
94.22,2018-03
90.71,2018-03
93.03,2018-03
91.89,2018-03
x.paddingInner is too large, so the bandwidth is too narrow for the violins to display. A value of 1 means a bandwidth of zero.
If x.paddingInner is set to a lower value, for example 0.1, then the bandwidth for the x scale will be wider, so the range for xNum scale will be wider and the violins are viewable.

plotting the points x and y, the axes y and x are not shown correctly

I'm trying to generate a line chart loading csv files by placing points on the x and y axis. for some strange reason the axes are not shown correctly. I have negative values for x and y. so it should have a Cartesian plane like the one in this image. in this case the complete Cartesian plane must be shown. In my points there are negative values in x and y.
I do not know why the points are showing wrong.
the update button, load new points updating the values of the graph.
http://plnkr.co/edit/dDx8gRZIfjvemWpgikAp?p=preview
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Set the ranges
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// Define the line
var valueline = d3.svg.line()
.x(function(d) { return x(d.datax); })
.y(function(d) { return y(d.datay); });
// 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 + ")");
The negative values in x and y is not displayed because your minimum domain value is 0:
x.domain([0, d3.max(data, function(d) { return d.datax; })]);
y.domain([0, d3.max(data, function(d) { return d.datay; })]);
It should have been(so that it sets the minimum value of dataset as domains minimum)
x.domain(d3.extent(data, function(d) { return d.datax; }));
y.domain(d3.extent(data, function(d) { return d.datay; }));
working code here

How to draw a line in d3 v4 using javascript objects?

I have the following object
var data =[
{"steps":200,calories:200,distance:200,date:new Date(2012,09,1)},
{"steps":200,calories:200,distance:200,date:new Date(2012,09,2)},
{"steps":200,calories:200,distance:200,date:new Date(2012,09,3)},
{"steps":200,calories:200,distance:200,date:new Date(2012,09,4)},
{"steps":200,calories:200,distance:200,date:new Date(2012,09,5)},
]
I'd like to draw a graph between the steps and the date object in d3 v4
I'm doing something like this to draw a line. Here's the full code..
var dataLine = [
{"x":new Date(2012,0,1),"y":10},
{"x":new Date(2012,0,2),"y":9},
{"x":new Date(2012,0,3),"y":3},
{"x":new Date(2012,0,4),"y":2}
];
var parseTime = d3.timeParse("%d-%b-%y");
var svgContainer = d3.select(".dsh-svg-element");
var MARGIN = {left:50,right:20,top:20,bottom:30};
var WIDTH = 960 - (MARGIN.left + MARGIN.right);
var HEIGHT = 500 - (MARGIN.top + MARGIN.bottom);
svgContainer.attr("width",WIDTH+(MARGIN.left + MARGIN.right))
.attr("height",HEIGHT+(MARGIN.top+MARGIN.bottom))
.attr("transform","translate(" + MARGIN.left + "," + MARGIN.top + ")");
var xMax =100;
var yMax =100;
var x = d3.scaleTime().domain([new Date(2012,0,1), new Date(2012,0,31)]).range([0, WIDTH])
var y = d3.scaleLinear().domain([0,yMax]).range([HEIGHT,0]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
svgContainer.append("g").attr("transform", "translate(50," + (HEIGHT+MARGIN.top) + ")").call(xAxis)
svgContainer.append("g").attr("transform", "translate(50,20)").call(yAxis).attr("id","yAxis")
var lineFunction = d3.line().x(function(d){return x(d.y)}).y(function(d){return y(d.x)})
svgContainer.append("path")
.attr("d",lineFunction(dataLine))
.attr("stroke","blue").attr("stroke-width", 2).attr("fill", "none");
I checked the inspector, the x() and y() functions seem to be returning the right pixels to be drawn on the graph.
But the path of the line is "M-455079.8680521219,-5964102899550L-455079.86805246526,-5964491699550L-455079.86805452546,-5964880499550L-455079.8680548688,-5965269299550" in the inspector.. It seems to be drawing the line outside the svg element. Any idea how to fix this?
Any tips or simple code on drawing a line are appreciated.
Fiddle
You have 2 main problems:
First, your x domain is completely unrelated to your data array. Just use d3.extent to get the first and last date:
var x = d3.scaleTime()
.domain(d3.extent(dataLine, function(d) {
return d.x
}))
.range([MARGIN.left, WIDTH])
Second, your line generator is wrong, you're using d.x with the y scale and d.y with the x scale. It should be:
var lineFunction = d3.line()
.x(function(d) {
return x(d.x)
}).y(function(d) {
return y(d.y)
})
Here is your updated fiddle: https://jsfiddle.net/mxsLdntg/
Have in mind that the line in the fiddle is based on dataLine, not data. You have to decide which data array you want to use and set the domains accordingly.

Why the line in this D3 chart isn't properly displayed when updated?

I'm a beginner with D3.js and I want to display a dynamic line chart where the line is always growing with random fluctuations.
I don't need an X axis but I'd like to get a dynamic Y axis based on the last point inserted in the line.
var n = 40,
random = function(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; },
data = d3.range(n).map(random);
var margin = {top: 20, right: 20, bottom: 20, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([1, n - 2])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, 100])
.range([height, 0]);
var line = d3.svg.line()
.interpolate("basis")
.x(function(d, i) { return x(i); })
.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);
svg.append("g")
.attr("class", "y axis")
.call(d3.svg.axis().scale(y).orient("left"));
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
var min = 0, max = min + 40;
tick();
//Update the chart
function tick() {
// push a new data point onto the back
var r = random(min, max);
data.push(r);
min += 10;
max += 10;
// update Y Axis
var y = d3.scale.linear().domain([r - 20,r + 20]).range([height, 0]);
var yAxis = d3.svg.axis().scale(y).orient("left");
svg.selectAll(".y.axis").call(yAxis);
// redraw the line, and slide it to the left
path
.attr("d", line)
.attr("transform", null)
.transition()
.duration(500)
.ease("linear")
.attr("transform", "translate(" + x(0) + ",0)")
.each("end", tick);
// pop the old data point off the front
data.shift();
}
JSFiddle : https://jsfiddle.net/ugj8g9wu/
If I didn't increase the min / max and don't update the Y Axis everything is ok.
But with the code above, my line quickly go above the the Y axis, which doesn't make any sens since the randomized value is include in the domain of the Y axis...
Could you tell me what's going on and why my line isn't properly displayed?
The issue is a bit hidden. In tick(), you made a new y to handle the new domain and range, but you only updated yAxis with this y. What about the line which is still referencing the original y? It also needs update! You can either add code to update the line:
// update Y Axis
var y = d3.scale.linear().domain([r - 20,r + 20]).range([height, 0]);
var yAxis = d3.svg.axis().scale(y).orient("left");
svg.selectAll(".y.axis").call(yAxis);
// NEW CODE
line.y(function(d, i) { return y(d); });
Or (better I think), instead of creating a new y every tick, you can modify the existing one, saving all the efforts to assign it to everywhere else using it. Just change this line:
var y = d3.scale.linear().domain([minY, maxY]).range([height, 0]);
into:
y.domain([minY, maxY]);
Then you'll be able to see the newest point coming in the right.
But there's one more problem with the code: you are increasing the value too quickly so that it's hard to see old points on the chart, so I tuned the arguments a bit to make it look better. Ideally, the minY and maxY should be calculated according to the values in data, not guessing magic boundarys. :)
Fiddle: https://jsfiddle.net/gbwycmrd/

Categories

Resources