I have found the following chart which uses V3 of d3:
http://mbostock.github.io/d3/talk/20111018/area-gradient.html
I have tried to port that sample to V5, but I got stuck with scaleTime(). The chart is not displayed correctly.
I have difficulties to debug var x as it is a function ... however if I look at the element svg:clipPath I see this (note the strange values):
<clipPath id="clip"><rect x="-8119106.125" y="0" width="0.000008575618267059326" height="361"></rect></clipPath>
These are the most relevant parts of my code:
var w=1280, h=800;
var svg = d3.select("#zoomable-area-chart").append("svg:svg");
(....)
// Scales
var x = d3.scaleTime().range([0, w]),
y = d3.scaleLinear().range([h, 0]),
svg.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", x(0))
.attr("y", y(1))
.attr("width", x(1) - x(0))
.attr("height", y(0) - y(1));
d3.csv("http://localhost/data.csv").then((data) => {
// Parse dates and numbers.
data.forEach(function(d) {
d.date = parse(d.date);
d.value = +d.value;
});
// Compute the maximum price.
x.domain([new Date(1999, 0, 1), new Date(2003, 0, 0)]);
y.domain([0, d3.max(data, function(d) {
return d.value;
})]);
draw();
});
Here you can find a playground for testing:
https://plnkr.co/edit/qWlLIg2avCe2Kt88r0T5?p=preview
the default domain of scaleTime is
Constructs a new time scale with the domain [2000-01-01, 2000-01-02], the unit range [0, 1]
Why use x(v) and y(v) to get there ranges when you set them as constants and you can get the range and use [0] and [1].
Make your zoom-rect fill none, it hides the graph
Related
I am trying to draw area graph but it is filling below x axis. In below code I have used y0(yScale(0)) as I have seen in many examples and also I tried to give y0(height) it is not giving me correct output. I want area to be filled only above x axis if y axis values are +ve and if y axis values are -ve then area is going above max tick of y axis.
const D3Node = require('d3-node');
getGraphString: (data,yAxisTickFormat) => {
const d3n = new D3Node() // initializes D3 with container element
const d3 = d3n.d3;
let margin = { top: 20, right: 30, bottom: 20, left: 40 },
width = 275,
height = 200;
let svg = d3n.createSVG(width, height+margin.top+margin.bottom);
let xScale = d3.scaleTime().range([margin.left, width - margin.right])
.domain(d3.extent(data, function (d) { return d.date })),
yScale = d3.scaleLinear().range([height - margin.top, margin.bottom])
.domain(d3.extent(data, function (d) { return d.value }));
svg.append('g').attr("class", "xAxis")
.attr("transform", "translate(0," + (height - margin.bottom) + ")") //The transforms are SVG transforms
.call(d3.axisBottom(xScale).ticks(d3.timeYear).tickFormat(d3.timeFormat('%Y')))
.selectAll("text")
.style("text-anchor","end")
.attr("dx", "-.9em")
.attr("dy", ".50em")
.attr("transform","rotate(-45)")
svg.append("g") //We create an SVG Group Element to hold all the elements that the axis function produces.
.attr("transform", "translate(" + (margin.left) + ",0)")
.attr("class","yAxis")
.call(d3.axisLeft(yScale).ticks(4).tickFormat(d3.format(yAxisTickFormat)))
.selectAll("text")
.style("text-anchor","end")
.attr("dy", "0.32em")
.attr("dx", "-0.4em")
let lineFunc = d3.line().x(function (obj) { return xScale(obj.date) })
.y(function (obj) { return yScale(obj.value) })
svg.append("path")
.attr("d", lineFunc(data))
.attr("stroke", '#002046')
.attr("stroke-width", 3)
.attr("fill", "none");
let area = d3.area()
.curve(d3.curveLinear)
.x(function (d) { return xScale(d.date); })
.y0(yScale(0))
.y1(function (d) { return yScale(d.value); });
svg.append("path")
.style("fill", "#002046")
.attr("d", area(data));
return d3n.svgString();
}
let data =[ { date: 2019-01-01T00:00:00.000Z, value: 0.6330419130189774 },
{ date: 2018-01-01T00:00:00.000Z, value: 0.6266752649582236 },
{ date: 2017-01-01T00:00:00.000Z, value: 0.6403446517126394 },
{ date: 2016-01-01T00:00:00.000Z, value: 0.6432956408788177 } ];
getGraphString(data,'.0%');
Problem
If your y axis (scale domain) starts at 0, then yScale(0) is an appropriate baseline for the area. However, your scale's domain extent does not start at 0, it is dependent on the dataset:
yScale = d3.scaleLinear().range([height - margin.top, margin.bottom])
.domain(d3.extent(data, function (d) { return d.value }));
The lowest value in your dataset is 0.6266... not zero. In using yScale(0) D3 interpolates where y(0) would be, which in your case would require extending the axis quite a bit down the page and off the SVG.
Solution
We can manually set the baseline with something like : area.y0(yScale(0.6266...)). This places the baseline at the base of your y axis. But you don't need to set it manually as you can can set it with:
area.y0(yScale.range()[0]);
yScale.range() returns an array containing the scaled extent of the yScale (and therefore the y axis), we want to have the area's base be the same as the axis.
yScale.range()[0] is the equivilant of yScale(yScale.domain()[0]); - if 0 is the minimum value of the domain (0 == yScale.domain()[0]), it's a short jump to the often used area.y0(yScale(0))
Alternative
Alternatively, if you want the axis to include zero, you could keep yScale(0) as the baseline and set 0 to be the minimum value of the scale's domain:
.domain([0,d3.max(function(d) { return d.value; })])
Either way, the value provided as the minimum value for the scale's range and area.y0 should be the same if you want the bottom of the area to be aligned to the bottom of the axis. (This value should generally also be equal to the y translate value for the x axis).
I have data whose values have a range (0, 100) but most of them have values ranging between 80 and 100.
Example of data: 97.00 93.30 92.20 92.70 91.10 89.10 89.90 89.10 89.70 88.90
89.00 89.30 88.76 88.46 87.45 85.05
I have to do a visualization using colors and using a linear scale is not the best because it does not allow me to distinguish colors quite easily.
So I thought about using a scaleQuantile.
I read this post that uses colors from black to red but I would like to use the Viridis scale.
How can I do that?
This is my piece of code:
var colorScale = d3.scaleQuantile(d3.interpolateViridis)
.domain([0, 100]);
// other code
var cells = svg.selectAll('rect')
.data(data)
.enter().append('g').append('rect')
.attr('class', 'cell')
.attr('width', cellSize)
.attr('height', cellSize)
.attr("rx", 4)
.attr("ry", 4)
.attr('y', function(d) {
return yScale(d.nuts_name);
})
.attr('x', function(d) {
return xScale(d.year);
})
.attr('fill', function(d) {
return colorScale(d.value);
}
})
Thanks
You have two problems here:
The domain in a quantile scale, unlike a quantize scale, is not a range between two values. It has to be the array with all the values. The API is clear about that:
If domain is specified, sets the domain of the quantile scale to the specified set of discrete numeric values. (emphasis mine)
That's not the correct way to use d3.interpolateViridis. Again, the API is clear:
Given a number t in the range [0,1], returns the corresponding color from the “viridis” perceptually-uniform color scheme
So, a simple solution is creating the quantile scale in such a way that it returns a number from 0 to 1 according to your data array (here, I'm creating 10 bins):
var colorScale = d3.scaleQuantile()
.domain(data)
.range(d3.range(0, 1.1, 0.1));
And then pass that value to d3.interpolateViridis:
d3.interpolateViridis(colorScale(d))
Here is a demo. The first row of <divs> use the data as they are, the second one uses a sorted array:
var data = [97.00, 93.30, 92.20, 92.70, 91.10, 89.10, 89.90, 89.10, 89.70, 88.90, 89.00, 89.30, 88.76, 88.46, 87.45, 85.05];
var sortedData = data.concat().sort();
var colorScale = d3.scaleQuantile()
.domain(data)
.range(d3.range(0, 1.1, 0.1));
var divs = d3.select("body").selectAll(null)
.data(data)
.enter()
.append("div")
.attr("class", "cell")
.style("background-color", function(d) {
return d3.interpolateViridis(colorScale(d))
});
d3.select("body").append("div")
.style("height", "40px")
var div2 = d3.select("body").selectAll(null)
.data(sortedData)
.enter()
.append("div")
.attr("class", "cell")
.style("background-color", function(d) {
return d3.interpolateViridis(colorScale(d))
});
.cell {
width: 20px;
height: 20px;
margin: 2px;
display: inline-block;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Let me preface this by saying I am brand new to D3.js and coding in general. I am an infographic artist and I've been using QGIS to generate maps, but am trying to use D3.js to generate a choropleth map for a story about Opioid deaths. Basically I am trying to recreate this map.
map from the Economist
I have tried to start by using this map by Mike Bostock and changing some of the parameters but am getting stuck with the color range and scale. The measurement is 1 per 100,000 population. I have a domain that starts at 1.543385761 and ends at 131.0814217.
The code I'm struggling with is around the scale input and output:
var x = d3.scaleLinear()
.domain([0, 132])
.rangeRound([600, 860]);
var color = d3.scaleThreshold()
.domain(d3.range(2, 10))
.range(d3.schemeBlues[9]);
var g = svg.append("g")
.attr("class", "key")
.attr("transform", "translate(0, 40)");
g.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d) { return x(d[0]); })
.attr("width", function(d) { return x(d[1]) - x(d[0]); })
.attr("fill", function(d) { return color(d[0]); });
I can see that I need some bit of code that will define everything 25 and over as the darkest color. Not even sure I want that to be my final legend but I'd love to know how to reproduce that. I am shocked I was able to get this far but feel a bit lost right now. thank you in advance!
Let's examine your scale:
var color = d3.scaleThreshold()
.domain(d3.range(2, 10))
.range(d3.schemeBlues[9]);
Your domain is an array of created like so:
d3.range(2,10) // [2,3,4,5,6,7,8,9]
These are your thresholds, colors will be mapped based on values that are less than or equal to 2, more than two up to three, more than three and up to four .... and over 9. This domain is mapped to nine values defined in the range:
d3.schemeBlues[9] // ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", #6baed6", #4292c6", "#2171b5", "#08519c", "#08306b"]
To set the thresholds for those colors so that values over 25 are one color, define the domain with array that has the appropriate threshold(s):
.domain([2,3,4,5,6,7,8,25]);
In the snippet below, this domain is applied. Rectangles have colors dependent on their location, all rectangles after the 25th (count left to right then line by line) one will be of one color.
var color = d3.scaleThreshold()
.domain([2,3,4,5,6,7,8,25])
.range(d3.schemeBlues[9]);
var svg = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height",500);
var rects = svg.selectAll("rect")
.data(d3.range(100))
.enter()
.append("rect")
.attr("width",15)
.attr("height", 15)
.attr("y", function(d,i) { return Math.floor(i / 10) * 20 + 10 })
.attr("x", function(d,i) { return i % 10 * 20 })
.attr("fill", function(d) { return color(d); })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
I'm trying to conditionally color these voronoi segments based on the 'd.lon' value. If it's positive, I want it to be green, if it's negative I want it to be red. However at the moment it's returning every segment as green.
Even if I swap my < operand to >, it still returns green.
Live example here: https://allaffects.com/world/
Thank you :)
JS
// Stating variables
var margin = {top: 20, right: 40, bottom: 30, left: 45},
width = parseInt(window.innerWidth) - margin.left - margin.right;
height = (width * .5) - 10;
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([0,0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.clipExtent([[0, 0], [width, height]]);
var g = svg.append("g");
// Map data
d3.json("/world-110m2.json", function(error, topology) {
// Cities data
d3.csv("/cities.csv", function(error, data) {
g.selectAll("circle")
.data(data)
.enter()
.append("a")
.attr("xlink:href", function(d) {
return "https://www.google.com/search?q="+d.city;}
)
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5)
.style("fill", "red");
});
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
d3.csv("/cities.csv", function(d) {
return [projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1]];
}, function(error, rows) {
vertices = rows;
console.log(vertices);
drawV(vertices);
}
);
function polygon(d) {
return "M" + d.join("L") + "Z";
}
function drawV(d) {
svg.append("g")
.selectAll("path")
.data(voronoi(d), polygon)
.enter().append("path")
.attr("class", "test")
.attr("d", polygon)
// This is the line I'm trying to get to conditionally fill the segment.
.style("fill", function(d) { return (d.lon < 0 ? "red" : "green" );} )
.style('opacity', .7)
.style('stroke', "pink")
.style("stroke-width", 3);
}
JS EDIT
d3.csv("/static/cities.csv", function(data) {
var rows = [];
data.forEach(function(d){
//Added third item into my array to test against for color
rows.push([projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1], [+d.lon]])
});
console.log(rows); // data for polygons and lon value
console.log(data); // data containing raw csv info (both successfully log)
svg.append("g")
.selectAll("path")
.data(voronoi(rows), polygon)
.enter().append("path")
.attr("d", polygon)
//Trying to access the third item in array for each polygon which contains the lon value to test
.style("fill", function(data) { return (rows[2] < 0 ? "red" : "green" );} )
.style('opacity', .7)
.style('stroke', "pink")
.style("stroke-width", 3)
});
This is what's happening: your row function is modifying the objects of rows array. At the time you get to the function for filling the polygons there is no d.lon anymore, and since d.lon is undefined the ternary operator is evaluated to false, which gives you "green".
Check this:
var d = {};
console.log(d.lon < 0 ? "red" : "green");
Which also explains what you said:
Even if I swap my < operand to >, it still returns green.
Because d.lon is undefined, it doesn't matter what operator you use.
That being said, you have to keep your original rows structure, with the lon property in the objects.
A solution is getting rid of the row function...
d3.csv("cities.csv", function(data){
//the rest of the code
})
... and creating your rows array inside the callback:
var rows = [];
data.forEach(function(d){
rows.push([projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1]])
});
Now you have two arrays: rows, which you can use to create the polygons just as you're using now, and data, which contains the lon values.
Alternatively, you can keep everything in just one array (just changing your row function), which is the best solution because it would make easier to get the d.lon values inside the enter selection for the polygons. However, it's hard providing a working answer without testing it with your actual code (it normally ends up with the OP saying "it's not working!").
I'm attempting to make a Paired Bar Graph between glob and local within my JS Object/Array. I've made bar graphs in D3 previously, but haven't used objects. I'm finding it difficult to access the correct data.
Eventually, the keyword data will be used in the axis. And the cpc will be used as a tooltip.
Here's the code that I have so far: (or see my JSFiddle)
var w = 600;
var h = 400;
var colors = ["#377EB8", "#4DAF4A"];
var dataset = {"keyword": ["payday loans", "title loans", "personal loans"],
"glob": ["1500000", "165000", "550000"],
"local": ["673000", "165000", "301000"],
"cpc": ["14.11", "12.53", "6.14"]
};
var series = 2; // Global & Local
var x0Scale = d3.scale.ordinal()
.domain(d3.range(dataset.glob.length))
.rangeRoundBands([0, w], 0.05);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {return d.glob;})])
.range([0, h]);
var glob = function(d) {
return d.glob;
};
//SVG element
var svg = d3.select("#searchVolume")
.append("svg")
.attr("width", w)
.attr("height", h);
// Graph Bars
svg.selectAll("rect")
.data(dataset, glob) //access the series here?
.enter()
.append("rect")
.attr("x", function(d, i){
return x0Scale(i);
})
.attr("y", function(d) {
return h - yScale(d.glob);
})
.attr("width", x0Scale.rangeBand())
.attr("height", function(d) {
return yScale(d.glob); // ***************
})
.attr("fill", colors[1]);
Currently, the chart doesn't get populated. I assume I am not accessing values correctly. I'm simply trying to get data from glob to make sure I'm accessing things correctly - and then from there I was going to populate both series, etc. Is my issue not accessing key/values correctly?
Take a look at this: http://jsfiddle.net/juY5E/2/
I was able to get three bars by changing .data(dataset, glob) to .data(dataset.glob) and then changing d.glob to +d for the 'y' attr, the 'height' attr and in yScale.domain
to be able to switch between glob and local, you may want to restructure the data.