d3.js. Stream chart shifts with different data sets. Why? - d3.js

Code available here http://jsfiddle.net/zeleniy/ea1uL7w9/
data = [47, 13, 61, 46, 26, 32, 6, 85, 1, 14, 86, 77, 13, 66, 0, 20, 11, 87, 5, 15];
data = [52, 33, 53, 45, 59, 45, 42, 50, 53, 50, 37, 45, 52, 50, 46, 48, 52, 56, 58, 59];
width = 300;
height = 100;
xScale = d3.scale.linear()
.domain([0, data.length])
.range([0, width]);
yScale = d3.scale.linear()
.domain([d3.min(data), d3.max(data)])
.range([0, height])
area = d3.svg.area()
.interpolate("basis")
.x(function(d, i) { return xScale(i); })
.y0(function(d) { return yScale(-d / 2); })
.y1(function(d) { return yScale(d / 2); });
svg = d3.select("#stream")
.append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("path")
.data([this.data])
.enter()
.append("path")
.attr("transform", "translate(0," + (height / 2) + ")")
.style("fill", "red")
.attr("d", area);
With first data set chart drawn in the center of svg element, as i expect. But with second data set stream shifts to the top of svg element. And i can't understand why. So why?

The first array contains values close to 0 and it's opening up your range. This line, then, is a fudge to shift the path into that open window:
.attr("transform", "translate(0," + (height / 2) + ")")
That said, you are setting up your scales in a confusing way (to me at least). I think about my domain as the min/max of my (plotted) dataset, in your case -max(d/2) and max(d/2). Further, I also think about my y-scale going from bottom to top as it would in a normal plot. With these changes, you don't need to artificially move anything:
var dataMax = d3.max(data);
yScale = d3.scale.linear()
.domain([ -dataMax/2, dataMax/2 ]) // real min/max of plotted data
.range([height, 0]) //<- bottom to top, although it still works without this change...
In this example, I left an axis overlayed for illustration.

Related

d3js: ordinal scale with not unique labels [duplicate]

I'm new to d3 and using it for creating a simple chart using array of numbers where the value '16' appears twice in it.
It generate the chart with one 'missing' 'rect' element for the 2nd '16' value, when I check the html I see that both '16' rect has same 'y' value of 72.
Please tell me what I'm doing wrong, thanks
code:
var data = [4, 8, 15, 16, 23, 16];
var chart = d3.select("body").append("svg")
.attr("class", "chart")
.attr("width", 420)
.attr("height", 20 * data.length);
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420])
var y = d3.scale.ordinal()
.domain(data)
.rangeBands([0, 120]);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", y)
.attr("width", x)
.attr("height", y.rangeBand());
The problem with your code is that you are trying to use the values from your data array to create range bands on an ordinal scale. Since the same input value will always be mapped to the same output value that means that both inputs 16 get mapped to the same range band 72.
If you want each input value to be mapped to its own "bar" then you need to use array indices instead of array values.
First you prepare the indices
var indices = d3.range(0, data.length); // [0, 1, 2, ..., data.length-1]
Then you use them to define the y scale domain
var y = d3.scale.ordinal()
.domain(indices)
// use rangeRoundBands instead of rangeBands when your output units
// are pixels (or integers) if you want to avoid empty line artifacts
.rangeRoundBands([0, chartHeight]);
Finally, instead of using array values as inputs use array indices when mapping to y
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (value, index) {
// use the index as input instead of value; y(index) instead of y(value)
return y(index);
})
.attr("width", x)
.attr("height", y.rangeBand());
As an added bonus this code will automatically rescale the chart if the amount of data changes or if you decide to change the chart width or height.
Here's a jsFiddle demo: http://jsfiddle.net/q8SBN/1/
Complete code:
var data = [4, 8, 15, 16, 23, 16];
var indices = d3.range(0, data.length);
var chartWidth = 420;
var chartHeight = 120;
var chart = d3.select("body").append("svg")
.attr("class", "chart")
.attr("width", chartWidth)
.attr("height", chartHeight);
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, chartWidth])
var y = d3.scale.ordinal()
.domain(indices)
.rangeRoundBands([0, chartHeight]);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (value, index) { return y(index); })
.attr("width", x)
.attr("height", y.rangeBand());
The way you are setting the y attribute of the rectangles will utilize the same value for all duplicate elements. You can use some offsetting like so:
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (d, i) {
return (i * y.rangeBand()) + y.rangeBand();})
.attr("width", x)
.attr("height", y.rangeBand());
Also you might have to adjust the height of your overall chart to see all the bands.

How to match up scaleBand with scaleLinear in D3.js

I have two series of scale, one is linear and the other is band, how can I make them to match up if there is some caps in the data.
Take a look at the example if necessary.
Mouse over and you see the boxes are not matching with the breaks of line.
If you want your scaleBand to be scaled (widened) where data is missing, I don't think that the scaleBand is the proper method for this, but it is unclear if that is something you want. Band scales are intended to provide equal spacing for each data value and that all values are present - it is an ordinal scale.
Assuming you only want the band scale to be aligned with your data where it is present:
If you log the domains of each of your x scales (scaleBand and scaleLinear) we find that the scaleBand has a domain of:
[ "1", "2", "8", "9", "13", "14", "20", "22" ] // 8 elements
And the scaleLinear has a domain of:
[ 1, 22 ] // a span of 22 'elements'
The scaleBand will need an equivalent domain to the scaleLinear. You can do this statically ( which I show mostly to demonstrate how d3.range will work):
let xBand = d3.scaleBand()
.domain(d3.range(1,23))
.rangeRound([0, width]);
This actually produces a domain that has 22 elements from 1 through 22.
or dynamically:
let xBand = d3.scaleBand()
.domain(d3.range(d3.min(testData1, d => d[0],
d3.max(testData1, d => d[0]+1)))
You could do this other ways, but the d3.range() function is nice and easy.
However, there is still one issue that remains, this is aligning the ticks between the two scales. For the linear scale, the tick for the first value (1) is on the y axis, but the band gap scale starts (and is not centered) on the y axis and fills the gap between 1 and 2. In other words, the center point of the band does not align vertically with the vertices of the line graph.
This can be addressed by adding 0.5 to both the lower and upper bounds of the linear scale's domain:
let xDomain = [
d3.min(testData1, d => d[0]-0.5),
d3.max(testData1, d => d[0]+0.5)
];
I've udpated your codepen with the relevant changes: codepen.
And in the event that that disappears, here is a snippet (the mouse over does not work for me for some reason in the snippet, it does in the codepen )
let width = 1000;
let height = 300;
let svg = d3.select(".wrapper-area-simple").append("svg")
.attr("width", width + 80)
.attr("height", height + 80)
.append('svg:g')
.attr('transform', 'translate(40, 30)');
let testData1 = [
[ 1, 10],
[ 2, 30],
[ 8, 34],
[ 9, 26],
[13, 37],
[14, 12],
[20, 23],
[22, 16],
];
let xDomain = [
d3.min(testData1, d => d[0]-0.5),
d3.max(testData1, d => d[0]+0.5)
];
let x = d3.scaleLinear()
.rangeRound([0, width])
.domain(xDomain);
let y = d3.scaleLinear()
.range([height, 0])
.domain(d3.extent(testData1, d => d[1]));
let line = d3.line()
.x(d => x(d[0]))
.y(d => y(d[1]));
svg.append('svg:g')
.datum(testData1)
.append('svg:path')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', '#000');
let xAxis = d3.axisBottom(x)
.ticks(testData1.length);
svg.append('svg:g')
.call(xAxis)
.attr('transform', `translate(0, 300)`);
let xBand = d3.scaleBand()
.domain(d3.range(d3.min(testData1, d => d[0]),
d3.max(testData1, d => d[0]+1)
))
.rangeRound([0, width]);
svg.append('svg:g')
.selectAll('rect')
.data(testData1)
.enter()
.append('svg:rect')
.attr('x', d => xBand(d[0]))
.attr('width', xBand.bandwidth())
.attr('height', height)
.attr('fill', '#000')
.on('mouseover', function() {
d3.select(this).classed('over', true);
})
.on('mouseout', function() {
d3.select(this).classed('over', false);
});
svg {
border: 1px solid red;
}
rect {
opacity: .1;
}
rect.over {
opacity: .2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"> </script>
<div class="wrapper-area-simple"></div>
Well, bad news for you: they will never match up (in your case). Let's see why.
This is your data:
let testData1 = [
[1, 10],
[2, 30],
[8, 34],
[9, 26],
[13, 37],
[14, 12],
[20, 23],
[22, 16],
];
As you can see, regarding the x coordinate, the line jumps from 1 to 2, but then from 2 to 8, from 8 to 9, and then from 9 to 13... That is, the x range intervals are not regular, evenly spaced. So far, so good.
However, when you pass the same data to the band scale, this is what it does: it divides the range ([0, width], which is basically the width) by testData1.length, that is, it divides the range by 8, and creates 8 equal intervals. Those are your bands, and that's the expected behaviour of the band scale. From the documentation:
Discrete output values are automatically computed by the scale by dividing the continuous range into uniform bands. (emphasis mine)
One solution here is simply using another linear scale:
let xBand = d3.scaleLinear()
.domain(xDomain)
.rangeRound([0, width]);
And this math to the width of the rectangles:
.attr('width', (d,i) => testData1[i+1] ? xBand(testData1[i+1][0]) - xBand(d[0]) : 0)
Here is your updated Codepen: http://codepen.io/anon/pen/MJdGyY?editors=0010

How does d3's path.bounds work?

I have a file world.topo.json in TopoJson format which I took from https://datamaps.github.io/ and use it in a d3 geo chart (using merchant projection).
It works well, but I find quite odd why, when I call path.bounds(<TopoJson File Content>.objects.world.feature) and get these values:
[
[-25.272818452358365, -114.9648719971861],
[917.2049776245796, 507.5180814546301]
]
So, why is the botom/left corner pointing to -25 and -114? Shouldn't them be either 0,0 or -917, -507 instead?
Update: I have a zoom behavior object bound to my d3 chart, which works for me exactly as expected. So, I've written a console.log for every zoom/drag even like below:
const topojson = <response of an ajax request>;
const bounds = path.bounds(topojson.objects.world.feature);
console.log(translate, JSON.stringify(path.bounds(feature))); // XXX
So, every single time zoom/drag even is called, this is the type of output I get:
[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
The first array being the current translate and the second being the bounds.
But, when I drag/pan or zoom, here is the output:
[0.021599999999999998, 0.10368] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[24.88185889212827, 119.4329226822157] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-15, 119] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-27, 117] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-27.32184332502962, 117.03468139278337] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-125.83796642848066, 127.65064293410353] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-165.15379127139124, 131.88726199045166] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.98081187505056, 132.83844955550114] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.98081187505056, 132.83844955550114] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.4557969093005, 132.7818746669505] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-89.06290511198648, 123.68781305086063] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-89.06290511198648, 123.68781305086063] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
As you can see, although the first argument changes constantly according to zoom and pan events, the bounds remain untouched.
The documentation about path.bounds(object)has it covered:
Returns the projected planar bounding box (typically in pixels) for the specified GeoJSON object. The bounding box is represented by a two-dimensional array: [[x₀, y₀], [x₁, y₁]], where x₀ is the minimum x-coordinate, y₀ is the minimum y-coordinate, x₁ is maximum x-coordinate, and y₁ is the maximum y-coordinate.
So, -25 and -114 are the minimum x and y values, and refer to the top left corner (in the SVG coordinates system), not the bottom left.
Have in mind that path.bounds is different from geoBounds, which:
Returns the spherical bounding box for the specified GeoJSON feature. The bounding box is represented by a two-dimensional array: [[left, bottom], [right, top]], where left is the minimum longitude, bottom is the minimum latitude, right is maximum longitude, and top is the maximum latitude.
How does it work?
path.bounds(object) will use your projection to drawn a "rectangle" around your object and will return an array with the four corners of that rectangle, as described above. Let's see how it works in these demos (this code is not mine):
In this first demo, the map of Japan has an scale of 1000. Check the console to see path.bounds.
var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
var width = 500,
height = 500,
scale = 1;
d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g").attr("id", "all-g");
var projection = d3.geo.mercator()
.center([138, 38])
.scale(1000)
.translate([width / 2, height / 2]);
d3.json(topoJsonUrl, onLoadMap);
function onLoadMap (error, jpn) {
var path = d3.geo.path()
.projection(projection);
var features = topojson.object(jpn, jpn.objects.japan);
var mapJapan = features;
console.log(JSON.stringify(path.bounds(mapJapan)))
d3.select("#all-g")
.append("g").attr("id", "path-g").selectAll("path")
.data(features.geometries)
.enter()
.append("path")
.attr("fill", "#f0f0f0")
.attr("id", function(d,i){ return "path" + i})
.attr("stroke", "#999")
.attr("stroke-width", 0.5/scale)
.attr("d", path);
}
path {
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>
It logs:
[[-12.878670523380151,73.71036362631844],[529.0014631418044,535.5463567314675]]
Which are [[x0, y0],[x1, y1]] values.
Now the same code, but with a scale of 500:
var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
var width = 500,
height = 500,
scale = 1;
d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g").attr("id", "all-g");
var projection = d3.geo.mercator()
.center([138, 38])
.scale(500)
.translate([width / 2, height / 2]);
d3.json(topoJsonUrl, onLoadMap);
function onLoadMap (error, jpn) {
var path = d3.geo.path()
.projection(projection);
var features = topojson.object(jpn, jpn.objects.japan);
var mapJapan = features;
console.log(JSON.stringify(path.bounds(mapJapan)))
d3.select("#all-g")
.append("g").attr("id", "path-g").selectAll("path")
.data(features.geometries)
.enter()
.append("path")
.attr("fill", "#f0f0f0")
.attr("id", function(d,i){ return "path" + i})
.attr("stroke", "#999")
.attr("stroke-width", 0.5/scale)
.attr("d", path);
}
path {
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>
It logs different values:
[[118.56066473830992,161.85518181315928],[389.5007315709022,392.77317836573377]]

D3js linear color scale with custom values (ticks)?

Background concept: Elevation maps and their color keys for human readers require a more detailed coverage of lower elevation (<200m), where 80% of the world population lives.
D3 project:
Given such elevations threshold levels (m) as : 0, 50, 100, 200, 500, 1000, 2000, 3000, 4000, 5000m.
Based on a linear color scale
ticks every 20px (regular span)
color picking at specific thresholds: 0, 50, 100, 200, 500, 1000, 2000, 3000, 4000, 5000m
ticks with specific labels: 0, 50, 100, 200, 500, 1000, 2000, 3000, 4000, 5000m
Examples of such custom scale, scale.ordinal(), scale.log(), etc. welcome.
Comment: I currently goes with an unsatisfying linear ramp code, see this, which cut my scale into 10 equal spans of 500m :
I should actually have 4 differents greens standing for 0, 50, 100, and 200m and less browns/greys/whites.
Var color declaration. Set up the color ramp table by stating the points where I have color shifts :
// Color-values equivalence
var color_elev = d3.scale.linear()
.domain([0, 200, 2000, 5000]) // values elevation (m)
.range(["#ACD0A5", "#E1E4B5", "#AA8753", "#FFFFFF"]) // colors
.interpolate(d3.interpolateHcl)
each of these 3 spans should indeed have linear color changes.
Injection of my SVG polygons
// Data (getJSON: TopoJSON)
d3.json("data/topo/final.json", showData);
// ---------- FUNCTION ------------- //
function showData(error, fra) {
... // do my topojson to svg map injection
}
Create, push my Color ramp box and key
/* START LEGEND_RAMP */
// Color ramp
var x = d3.scale.linear()
.domain([0, 5000]) // legend elevation (m)
.range([0, 280]); // width (px)
// Color ramp place ? ? ?
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(13)
.tickFormat(d3.format(".0f"));
// (JS shortcut)
var legend_key = svg.append("g")
.attr("class", "legend_key")
.attr("transform", "translate(" + (width - 300) + "," + (height - 30) + ")");
// Color ramp: white background
legend_key.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 310)
.attr("height", 40)
.style("fill", "white")
.style("fill-opacity", 0.5)
// Color ramp: bricks
legend_key.selectAll(".color_ramp")
.data(d3.pairs(x.ticks(10))) // is this forcing a 10 equal sizes slicing ?
.enter().append("rect")
.attr("class", "elev_color_brick") // or "band"
.attr("height", 8)
.attr("x", function(d) { return x(d[0]); })
.attr("width", function(d) { return x(d[1]) - x(d[0]); })
.style("fill", function(d) { return color_elev(d[0]); });
// ?
legend_key.call(xAxis);
/* END LEGEND */
I'd comment this, but can't at present, have you looked at colorbrewer? Also, have you considered using an ordinal scale. You'd be able to control the colour ramp by mapping ranges to particular colours. If you're looking for a more automated way you could use an equal area distribution (histogram equalisation) method? Otherwise power or log scales could be an improvement over linear.

what's the absolute shortest d3 area example?

I'm really struggling to understand d3's area and stack layout stuff. I tried making what I thought was the smallest example but nothing appears and in fact it prints errors in the console. What am I not getting? Here's the code.
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 300)
var testData = [
[ 0, 10],
[10, 20],
[20, 30],
[30, 20],
];
svg.selectAll("path.area")
.data(testData)
.enter().append("path")
.style("fill", "#ff0000")
.attr("d", d3.svg.area());
The dimension of the data is not correct. Each area path needs a 2D array, like this:
d3.svg.area()([[ 0, 10], [10, 20], [20, 30], [30, 20]])
results in:
"M0,10L10,20L20,30L30,20L30,0L20,0L10,0L0,0Z"
That means that you need to bind a 3D array to the selection. Each element (i.e. path) in the selection will then receive a 2D array.
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 300)
var testData = [
[ 0, 10],
[10, 20],
[20, 30],
[30, 20],
];
svg.selectAll("path.area")
.data([testData]) // dimension of data should be 3D
.enter().append("path")
.style("fill", "#ff0000")
.attr("class", "area") // not the cause of your problem
.attr("d", d3.svg.area());
Sometimes it's easier to picture what is going on by imagining that you would like to create multiple areas. Then it would look like:
var testData1 = [
[ 0, 10],
[10, 20],
[20, 30],
[30, 20],
];
var testData2 = [
[100, 110],
[110, 120],
[120, 130],
[130, 120],
];
svg.selectAll("path.area")
.data([testData1, testData2])
.enter().append("path")
.style("fill", "#ff0000")
.attr("class", "area")
.attr("d", d3.svg.area());

Resources