I just made a D3js globe localisator, which looks like this :
If you look carefully, the red square looks ugly since it doesn't follow the Earth's curve. I have the area bounding box in decimal degrees :
var bb = {W:-5.0, N:50.0, E:10.0, S:40.0 }
And I draw the lines as follow:
svg.append("path")
.datum({type: "LineString", coordinates:
[[-5, 40], [-5, 50], [10, 50], [10, 40], [-5, 40]]
})
.attr("d", path);
For larger areas, it's even the opposite curve from expectations (for a bounding box):
How to add rather elegant spherical arcs ?
Given a known decimal degrees bounding box (dig in start here for bb) such as :
bounds = [[-50.8,20.0][30,51.5]];
WNES0 = bounds[0][0], // West "W":-50.8
WNES1 = bounds[1][2], // North "N": 51.5
WNES2 = bounds[1][0], // East "E": 30
WNES3 = bounds[0][3], // South "S": 20.0
Some maths are needed.
// *********** MATH TOOLKIT ********** //
function parallel(φ, λ0, λ1) {
if (λ0 > λ1) λ1 += 360;
var dλ = λ1 - λ0,
step = dλ / Math.ceil(dλ);
return d3.range(λ0, λ1 + .5 * step, step).map(function(λ) { return [normalise(λ), φ]; });
}
function normalise(x) {
return (x + 180) % 360 - 180;
}
Then, let's both calculate the polygon's coordinates and project it:
// *********** APPEND SHAPES ********** //
svg.append("path")
.datum({type: "Polygon", coordinates: [
[[WNES0,WNES3]]
.concat(parallel(WNES1, WNES0, WNES2))
.concat(parallel(WNES3, WNES0, WNES2).reverse())
]})
.attr("d", path)
.style({'fill': '#B10000', 'fill-opacity': 0.3, 'stroke': '#B10000', 'stroke-linejoin': 'round'})
.style({'stroke-width': 1 });
180th meridian crossing: Boxes upon the 180th meridian need special management. By example, localising a set of pacific island between 155⁰ East and -155 West initially gives....
...with correct rotation (+180⁰) :
... and with correct boxing:
Localisator now perfect ! Live demo on blocks
var bb = { "item":"India", "W": 67.0, "N":37.5, "E": 99.0, "S": 5.0 },
localisator("body", 200, bb.item, bb.W, bb.N, bb.E, bb.S);
+1 welcome.
You can use d3's built in graticule generator for this:
var bb = {W: -5.0, N: 50.0, E: 10.0, S: 40.0 };
var arc = d3.geo.graticule()
.majorExtent([[bb.W, bb.S], [bb.E, bb.N]]);
Then use the outline function of the graticule generator to draw the path:
svg.append("path")
.attr("class", "arc")
.attr("d", path(arc.outline()));
Full working example can be found here.
Related
I've got this kind of non uniforme datas :
[{'time':0,'sum':0},{'time':600,'sum':2},{'time':700,'sum':4},{'time':1200,'sum':1},{'time':1300,'sum':3},{'time':1600,'sum':1},{'time':2000,'sum':0}];
"time" is on x axis and "sum" on y axis. If I make an area, I've got these shapes (curved in red, not curved in white) :
https://codepen.io/kilden/pen/podadRW
But the meaning of this is wrong. I have to interpret the "missing" datas. A bit like the "kernel density estimation" charts (example here :https://bl.ocks.org/mbostock/4341954) where values are at zero when there is no data, but there is a "fall off" around the point with data. (a gaussian curve)
It's hard to explain with words (and English is not my mother tongue). So I did this second codepen to show the idea of the shape. The area in red is the shape I want (White one is the reference of the first codepen) :
https://codepen.io/kilden/pen/VwrQrbo
I wonder if there is a way to make this kind of cumulative gaussian curves with a (hidden?) d3 function or a trick function ?
A. Your cheating yourself when you use the Epanechnikov kernel, evaluate these on a rather coarse grid and make a smooth line interpolation so that it looks gaussian. Just take a gaussian kernel to start with.
B. You're comparing apples and oranges. A kernel density estimate is an estimate of a probability density that cannot be compared to the count of observations. The integral of the kernel density estimate is always equal to 1. You can scale the estimate by the total count of observations, but even then your curve would not "stick" to the point, since the kernel spreads the observation away from the point.
C. What comes close to what you want to achieve is implemented below. Use a gaussian curve which is 1 at 0, i. e. without the normalizing factor and the rescaling by the bandwidth. The bandwidth now scales only the width of the curve but not its height. Then use your original data array and add up all these curves with the weight sum from your data array.
This will match your data points when there are no clustered observations. Naturally, when two observations are close to each other, their individual gaussian curves can add up to something bigger than each observation.
DISCLAIMER: As I already pointed out in the comments, this just produces a pretty chart and is mathematical nonsense. I strongly recommend working out the mathematics behind what it is you really want to achieve. Only then you should make a chart of your data.
const WIDTH = 600;
const HEIGHT = 150;
const BANDWIDTH = 25;
let data = [
{time: 0, sum: 0},
{time: 200, sum: 4},
{time: 250, sum: 2},
{time: 500, sum: 1},
{time: 600, sum: 2},
{time: 1500, sum: 5},
{time: 1600, sum: 4},
{time: 1800, sum: 3},
{time: 2000, sum: 0},
];
// svg
const svg = d3.select("body")
.append("svg")
.attr("width", WIDTH)
.attr("height", HEIGHT)
.style("background-color", "grey");
// scales
const x_scale = d3.scaleLinear()
.domain([0, 2000])
.range([0, WIDTH]);
const y_scale = d3.scaleLinear()
.range([HEIGHT, 0]);
// curve interpolator
const line = d3.line()
.x(d => x_scale(d.time))
.y(d => y_scale(d.sum))
.curve(d3.curveMonotoneX);
const grid = [...Array(2001).keys()];
svg.append("path")
.style("fill", "rgba(255,255,255,0.4");
// gaussian "kernel"
const gaussian = k => x => Math.exp(-0.5 * x / k * x / k);
// similar to kernel density estimate
function estimate(kernel, grid) {
return obs => grid.map(x => ({time: x, sum: d3.sum(obs, d => d.sum * kernel(x - d.time))}));
}
function render(data) {
data = data.sort((a, b) => a.time - b.time);
// make curve estimate with these kernels
const curve_estimate = estimate(gaussian(BANDWIDTH), grid)(data);
// set endpoints to zero for area plot
curve_estimate[0].sum = 0;
curve_estimate[curve_estimate.length-1].sum = 0;
y_scale.domain([0, 1.5 * Math.max(d3.max(data, d => d.sum), d3.max(curve_estimate, d => d.sum))]);
svg.select("path")
.attr("d", line(curve_estimate))
const circles = svg.selectAll("circle")
.data(data, d => d.time)
.join(
enter => enter.append("circle")
.attr("fill", "red"),
update => update.attr("fill", "white")
)
.attr("r", 2)
.attr("cx", d => x_scale(d.time))
.attr("cy", d => y_scale(d.sum));
}
render(data);
function randomData() {
data = [];
for (let i = 0; i < 10; i++) {
data.push({
time: Math.round(2000 * Math.random()),
sum: Math.round(10 * Math.random()) + 1,
});
}
render(data);
}
function addData() {
data.push({
time: Math.round(2000 * Math.random()),
sum: Math.round(10 * Math.random()) + 1,
});
render(data);
}
d3.select("#random_data").on("click", randomData);
d3.select("#add_data").on("click", addData);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<button id="random_data">
Random Data
</button>
<button id="add_data">
Add data point
</button>
I'm trying to draw LineStrings that follow various latitude segments, however the built-in geodesic arc interpolation doesn't seem to be drawing arcs that follow latitude. My question is: why not and how do I achieve this?
Here is my result:
And my code:
const width = 500;
const height = 500;
const scale = 200;
const svg = d3.select('svg').attr("viewBox", [0, 0, width, height]);
const projection = d3.geoStereographic().rotate([0, -90]).precision(0.1).clipAngle(90.01).scale(scale).translate([width / 2, height / 2]);
const path = d3.geoPath(projection);
const graticule = d3.geoGraticule().stepMajor([15, 15]).stepMinor([0, 0])();
svg
.append("path")
.datum(graticule)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", '#000000')
.attr("stroke-width", 0.3)
.attr("stroke-opacity", 1);
let curve = {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[-180, 15],
[-90, 15]
]
}
}
svg
.append("path")
.datum(curve)
.attr("d", path)
.attr('fill-opacity', 0)
.attr('stroke', 'red')
.attr("stroke-width", 1)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
My fiddle: https://jsfiddle.net/jovrtn/komfxycz/
D3 is fairly unique when it comes to geographic data: it uses spherical math (which despite many benefits, does lead to some challenges). d3.geoPath samples a line segment between two points so that the path follows a great circle (the shortest path between two points on a globe). Parallels do not follow great circle distances, so your path does not follow the parallel.
The behavior you are looking for requires us to draw a line between two points of latitude longitude as though they were Carteisan, even though they are not, and then preserve the points along that line when applying the stereographic projection.
When using an cylindrical projection the solution is easy enough, don't sample between points on a line. This answer contains such a solution.
This doesn't help with a stereographic projection - the linked approach would just result in a straight line between the first point and end point instead of a curved line along the parallel.
A solution is to manually sample points between start and end as though the data were Cartesian, then treat them as 3D in order to project them with a stereographic projection. This results in a path that follows parallels where start and end have the same north/south value. How frequently you sample reduces/eliminates the effect of great circle distances when using d3.geoPath.
In my solution I'm going to use two d3 helper functions:
d3.geoDistance which measures the distance between two lat long pairs in radians.
d3.interpolate which creates an interpolation function between two values.
let sample = function(line) {
let a = line.geometry.coordinates[0]; // first point
let b = line.geometry.coordinates[1]; // end point
let distance = d3.geoDistance(a, b); // in radians
let precision = 1*Math.PI/180; // sample every degree.
let n = Math.ceil(distance/precision); // number of sample points
let interpolate = d3.interpolate(a,b) // create an interpolator
let points = []; // sampled points.
for(var i = 0; i <= n; i++) { // sample n+1 times
points.push([...interpolate(i/n)]); // interpolate a point
}
line.geometry.coordinates = points; // replace the points in the feature
}
The above assumes a line with two points/one segment, naturally if your lines are more complex than that you'll need to adjust this. It's intended just as a starting point.
And in action:
const width = 500;
const height = 500;
const scale = 200;
const svg = d3.select('svg').attr("viewBox", [0, 0, width, height]);
const projection = d3.geoStereographic().rotate([0, -90]).precision(0.1).clipAngle(90.01).scale(scale).translate([width / 2, height / 2]);
const path = d3.geoPath(projection);
const graticule = d3.geoGraticule().stepMajor([15, 15]).stepMinor([0, 0])();
svg
.append("path")
.datum(graticule)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", '#000000')
.attr("stroke-width", 0.3)
.attr("stroke-opacity", 1);
let curve = {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[-180, 15],
[-90, 15]
]
}
}
svg
.append("path")
.datum(curve)
.attr("d", path)
.attr('fill-opacity', 0)
.attr('stroke', 'red')
.attr("stroke-width", 1)
let sample = function(line) {
let a = line.geometry.coordinates[0];
let b = line.geometry.coordinates[1];
let distance = d3.geoDistance(a, b); // in radians
let precision = 5*Math.PI/180;
let n = Math.ceil(distance/precision);
let interpolate = d3.interpolate(a,b)
let points = [];
for(var i = 0; i <= n; i++) {
points.push([...interpolate(i/n)]);
}
line.geometry.coordinates = points;
}
sample(curve);
svg
.append("path")
.datum(curve)
.attr("d", path)
.attr('fill-opacity', 0)
.attr('stroke', 'blue')
.attr("stroke-width", 1)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
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
In the d3.v4 documentation the following is stated:
To generate ticks every fifteen minutes with a time scale, say:
axis.tickArguments([d3.timeMinute.every(15)]);
Is there a similar approach that can be used with values other than time? I am plotting sine and cosine curves, so I'd like the ticks to begin at -2*Math.PI, end at 2*Math.PI, and between these values I'd like a tick to occur every Math.PI/2. I could, of course, explicitly compute the tick values and supply them to the tickValue method; however, if there is a simpler way to accomplish this, as in the time-related example quoted above, I'd prefer to use that.
Setting the end ticks and specifying the precise space of the ticks in a linear scale is a pain in the neck. The reason is that D3 axis generator was created in such a way that the ticks are automatically generated and spaced. So, what is handy for someone who doesn't care too much for customisation can be a nuisance for those that want a precise customisation.
My solution here is a hack: create two scales, one linear scale that you'll use to plot your data, and a second scale, that you'll use only to make the axis and whose values you can set at your will. Here, I choose a scalePoint() for the ordinal scale.
Something like this:
var realScale = d3.scaleLinear()
.range([10,width-10])
.domain([-2*Math.PI, 2*Math.PI]);
var axisScale = d3.scalePoint()
.range([10,width-10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0",
"0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
Don't mind the \u03c0, that's just π (pi) in Unicode.
Check this demo, hover over the circles to see their positions:
var width = 500,
height = 150;
var data = [-2, -1, 0, 0.5, 1.5];
var realScale = d3.scaleLinear()
.range([10, width - 10])
.domain([-2 * Math.PI, 2 * Math.PI]);
var axisScale = d3.scalePoint()
.range([10, width - 10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0", "0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var circles = svg.selectAll("circle").data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("fill", "teal")
.attr("cy", 50)
.attr("cx", function(d) {
return realScale(d * Math.PI)
})
.append("title")
.text(function(d) {
return "this circle is at " + d + " \u03c0"
});
var axis = d3.axisBottom(axisScale);
var gX = svg.append("g")
.attr("transform", "translate(0,100)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I was able to implement an x axis in units of PI/2, under program control (not manually laid out), by targetting the D3 tickValues and tickFormat methods. The call to tickValues sets the ticks at intervals of PI/2. The call to tickFormat generates appropriate tick labels. You can view the complete code on GitHub:
https://github.com/quantbo/sine_cosine
My solution is to customise tickValues and tickFormat. Only 1 scale is needed, and delegate d3.ticks function to give me the new tickValues that are proportional to Math.PI.
const piChar = String.fromCharCode(960);
const tickFormat = val => {
const piVal = val / Math.PI;
return piVal + piChar;
};
const convertSIToTrig = siDomain => {
const trigMin = siDomain[0] / Math.PI;
const trigMax = siDomain[1] / Math.PI;
return d3.ticks(trigMin, trigMax, 10).map(v => v * Math.PI);
};
const xScale = d3.scaleLinear().domain([-Math.PI * 2, Math.PI * 2]).range([0, 600]);
const xAxis = d3.axisBottom(xScale)
.tickValues(convertSIToTrig(xScale.domain()))
.tickFormat(tickFormat);
This way if your xScale's domain were changed via zoom/pan, the new tickValues are nicely generated with smaller/bigger interval
I have this simple linear scale:
var x = d3.scale.linear().domain([0, 250]);
x.ticks(6), as expected, returns:
[0, 50, 100, 150, 200, 250]
However, x.ticks(11) returns:
[0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240]
When what I want is:
[0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250]
How do I fix this?
I had a similar issue with ordinal scales, I simply wrote some code to pick evenly spaced intervals in my data. Since I wanted it to always choose the first and last data element on the axis, I calculate the middle part only. Since some things do not divide evenly, rather than having the residual in one or two bins, I spread it out across the bins as I go; until there is no more residual.
There is probably a simpler way to accomplish this but here's what I did:
function getTickValues(data, numValues, accessor)
{
var interval, residual, tickIndices, last, i;
if (numValues <= 0)
{
tickIndices = [];
}
else if (numValues == 1)
{
tickIndices = [ Math.floor(numValues/2) ];
}
else
{
// We have at least 2 ticks to display.
// Calculate the rough interval between ticks.
interval = Math.floor(data.length / (numValues-1));
// If it's not perfect, record it in the residual.
residual = Math.floor(data.length % (numValues-1));
// Always label our first datapoint.
tickIndices = [0];
// Set stop point on the interior ticks.
last = data.length-interval;
// Figure out the interior ticks, gently drift to accommodate
// the residual.
for (i=interval; i<last; i+=interval)
{
if (residual > 0)
{
i += 1;
residual -= 1;
}
tickIndices.push(i);
}
// Always graph the last tick.
tickIndices.push(data.length-1);
}
if (accessor)
{
return tickIndices.map(function(d) { return accessor(d); });
}
return tickIndices.map(function(i) { return data[i]; });
}
You call the function via:
getTickvalues(yourData, numValues, [optionalAccessor]);
Where yourData is your array of data, numvalues is the number of ticks you want. If your array contains a complex datastructure then the optional accessor comes in handy.
Lastly, you then feed this into your axis. Instead of ticks(numTicks) which is only a hint to d3 apparently, you call tickValues() instead.
I learned the hard way that your tickValues have to match your data exactly for ordinal scales. This may or may not be as helpful for linear scales, but I thought I'd share it anyways.
Hope this helps.
Pat
You can fix this by replacing the x.ticks(11) with your desired array.
So if your code looks like this and x is your linear scale:
chart.selectAll("line")
.data(x.ticks(11))
.enter()
.append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", 0)
.attr("y2",120)
.style("stroke", "#CCC");
You can replace x.ticks(11) with your array:
var desiredArray = [0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250]
chart.selectAll("line")
.data(desiredArray)
.enter()
.append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", 0)
.attr("y2",120)
.style("stroke", "#CCC");
The linear scale will automatically place your desired axes based on your input. The reason why the ticks() isn't giving you your desired separation is because d3 just treats ticks() as a suggestion.
axis.tickvalues((function(last, values) {
var myArray = [0];
for(var i = 1; i < values; i++) {
myArray.push(last*i/(values-1))
}
return myArray;
})(250, 11));
This should give you an evenly spaced out array for specifying the number of tick values you want in a particular range.