How to create skewed axes? - javascript

I am trying to create something like this geology visual below that maps soil by its composition (of a given three components):
To my knowledge there is only d3.axisBottom() but nothing like d3.axis45Degrees(). So unfortunately my attempts at this visual were cut short virtually right off the bat because I can't even figure out how to set up the axes for a triangular coordinate plane.
Question
Can d3 handle such an axis configuration, or are there any other d3 methods that would be relevant for a task such as this?

This question is borderline "too broad". However, I believe that it's an interesting question, since the documentation may lead someone to believe that only vertical/horizontal axes are possible.
You can always rotate the axis (any axis, be it axisBottom, axisTop, axisRight or axisLeft) and rotate the <text> elements back.
Here is a simple demo (full of magic numbers):
const svg = d3.select("svg");
const scale = d3.scaleLinear([10, 380]);
const axis = d3.axisLeft(scale);
const axis2 = d3.axisRight(scale);
const axis3 = d3.axisBottom(scale);
const axisGroup = svg.append("g")
.attr("transform", "rotate(30, 100, 400)")
.call(axis)
.selectAll("text")
.attr("transform", "rotate(-30, -10, 0)");
const axisGroup2 = svg.append("g")
.attr("transform", "rotate(-30, 108, -378)")
.call(axis2)
.selectAll("text")
.attr("transform", "rotate(30, 10, 0)");
const axisGroup3 = svg.append("g")
.attr("transform", "translate(14,333)")
.call(axis3)
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="450" height="400"></svg>
Change those magic numbers accordingly. By the way, in that triangle you shared in your question the internal angles are 60 degrees (not 45), so here I'm rotating the axes by 30 degrees.
Finally, it's worth mentioning that I just transformed (translate, rotate etc...) those axes. For a real plot, like the one in your image, you'll have to create a whole math just to calculate where in the SVG the values of the 3 coordinates will fall.

Related

d3.js v5+ Angle / degree axis support

I've made a chart similar to the one pictured here.
Of course d3 can create the y axis, but it turns out that d3 can also give us the logic for making these arcs. Example snippet below:
var dScale = d3.scaleLinear()
.domain([-90,90])
.range([0,Math.PI]);
let arc = d3.arc()
.innerRadius(0)
.outerRadius(function(d) {return yScale(d.y)})
.startAngle(function(d) {return dScale(d.top)})
.endAngle(function(d) {return dScale(d.bottom)});
However, creating an "axis" for these degrees (dScale()) in my snippet seems way harder -- to the point where it doesn't feel like d3 anymore.
Question
Unless I'm mistaken, we'd have to get real hacky at this point and hardcode lines, ticks and axis labels using dScale, but is anyone able to help me see a more "d3-ic" (analog of pythonic?) way of achieving a degree scale?

How to get D3 to plot all elements of a GeoJson file? [duplicate]

I am trying to visualize russians regions. I got data from here, validate here and all was well - picture.
But when I try to draw it, I receive only one big black rectangle.
var width = 700, height = 400;
var svg = d3.select(".graph").append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.style("max-width", "700px")
.style("margin", "10px auto");
d3.json("83.json", function (error, mapData) {
var features = mapData.features;
var path = d3.geoPath().projection(d3.geoMercator());
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", path)
});
Example - http://ustnv.ru/d3/index.html
Geojson file - http://ustnv.ru/d3/83.json
The issue is the winding order of the coordinates (see this block). Most tools/utilities/libraries/validators don't really care about winding order because they treat geoJSON as containing Cartesian coordinates. Not so with D3 - D3 uses ellipsoidal math - benefits of this is include being able to cross the antimeridian easily and being able to select an inverted polygon.
The consequence of using ellipsoidal coordinates is the wrong winding order will create a feature of everything on the planet that is not your target (inverted polygon). Your polygons actually contain a combination of both winding orders. You can see this by inspecting the svg paths:
Here one path appears to be accurately drawn, while another path on top of it covers the entire planet - except for the portion it is supposed to (the space it is supposed to occupy covered by other paths that cover the whole world).
This can be simple to fix - you just need to reorder the coordinates - but as you have features that contain both windings in the same collection, it'll be easier to use a library such as turf.js to create a new array of properly wound features:
var fixed = features.map(function(feature) {
return turf.rewind(feature,{reverse:true});
})
Note the reverse winding order - through an odd quirk, D3, which is probably the most widespread platform where winding order matters actually doesn't follow the geoJSON spec (RFC 7946) on winding order, it uses the opposite winding order, see this comment by Mike Bostock:
I’m disappointed that RFC 7946 standardizes the opposite winding order
to D3, Shapefiles and PostGIS. And I don’t see an easy way for D3 to
change its behavior, since it would break all existing (spherical)
GeoJSON used by D3. (source)
By rewinding each polygon we get a slightly more useful map:
An improvement, but the features are a bit small with these projection settings.
By adding a fitSize method to scale and translate we get a much better looking map (see block here):
Here's a quick fix to your problem, projection needs a little tuning, also path has fill:#000 by default and stroke: #FFF could make it more legible.
var width = 700, height = 400;
var svg = d3.select(".graph").append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.style("max-width", "700px")
.style("margin", "10px auto");
d3.json("mercator_files/83.json", function (error, mapData) {
var features = mapData.features;
var center = d3.geoCentroid(mapData);
//arbitrary
var scale = 7000;
var offset = [width/2, height/2];
var projection = d3.geoMercator().scale(scale).center(center)
.translate(offset);
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", path)
});

d3.geoPath().projection is rendering black rectangle instead of map

I want to create a US map based on county data. I'm using this JSON topology data to create the graph: https://cdn.freecodecamp.org/testable-projects-fcc/data/choropleth_map/counties.json
In the first step, I created the map like this, and it works fine:
var path = d3.geoPath();
svgContainer.selectAll("path")
.data(topojson.feature(countyData, countyData.objects.counties).features)
.enter()
.append("path")
.attr("d", path)
Picture: US map renders OK but too large
However, it is too large for my purpose so I'm trying to scale it down. I tried projections which I saw in several other projects (for example here: https://www.d3-graph-gallery.com/graph/choropleth_basic.html). Unfortunately it just renders a black rectangle. I also tried geoAlbersUsa() and some other projections, but it did not help. How can I get the map data to scale?
var projection = d3.geoAlbersUsa() // geoMercator() also does not work
.scale(200)
.translate([width / 2, height / 2]);
var path = d3.geoPath().projection(projection);
svgContainer.selectAll("path")
.data(topojson.feature(countyData, countyData.objects.counties).features)
.enter()
.append("path")
.attr("d", path)
Picture: projection renders black rectangle
What am I doing wrong here?
Everything looks good in the second block of code (using d3.geoAlbersUSA()) except I think you are zoomed in too close with .scale(200) and only seeing the middle of a county. As explained in this post, if you zoom out with smaller scale value you may start to see more of your map.(What does it mean to scale a projection in d3?)
You may be better off using .fitSize() instead of .scale since you seem to be trying to fit the whole topojson data set inside an area rather than zooming into part of it. Updated your example below using a variable margin.
var margin = 20; //amount of whitespace you want around the map
var projection = d3.geoAlbersUsa()
.translate([width / 2, height / 2]);
var path = d3.geoPath().projection(projection);
var countiesFeatureCollection = topojson.feature(countyData, countyData.objects.counties);
//make the map projection fit into size of screen minus margin on all sides
projection.fitSize([width - margin*2, height - margin*2], countiesFeatureCollection);
svgContainer.selectAll("path")
.data(countiesFeatureCollection.features)
.enter()
.append("path")
.attr("d", path)

Updating x axis d3js

I'm trying to update my x axis in a D3js bar chart (is partially working) depending on a user filter, the bars are actually changing but is not doing it well. I don't really know where is the problem and I need some help.
in this part of the code I'm updating the bar chart
function update(selectedGroup) {
svg.selectAll("rect").remove()
var groups = d3.map(dataFilter, function(d){return(d.group)}).keys();
x.domain(groups);
var dataFilter = result.filter(function(d){return d.group==selectedGroup});
console.log(dataFilter);
var rectG=rectangulos(dataFilter)
}
the complete bar chart
how is working now:
the result should be something like this
I have an live example here
There is a relatively straightforward reason you are seeing this behavior.
When the domain of the scale x is all the groups x.bandwidth() is small. But when the domain of x is only one value, x.bandwidth() is large. In both cases, the first band starts in the same location.
Next we have a nested scale here xSubgroup - the range of this is equal to x.bandwidth(). When the domain of x changes, we need to update the range of xSubgroup. If we don't do this, the bars will still be very thin and start at the beginning of the axis (as the bars' bandwidth aren't changing even if the group's bandwidth does). You don't update the sub scale's range, but we need to do that:
x.domain(groups);
xSubgroup.range([0, x.bandwidth()])
With this we get the update we're looking for.
But the axis labels remain unchanged. Updating a scale doesn't update the axis unless we explicitly do so. I'll break up your method chaining and store a reference for the g holding the axis:
var xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
xAxis.selectAll("text")
...
Now we can update the axis, I'm carrying forward the text styling as well. You can simplify the code by using an update function to do all the entering/exiting/updating of axes and data - here we have some duplication in that both the initial set up and the update function have overlap.
To update the axis we use:
// Call the axis again to update
xAxis.call(d3.axisBottom(x))
xAxis.selectAll("text")
.style("text-anchor", "end")
.attr("font-size", "55px")
.attr("y", "-7")
.attr("x", "-7")
.attr("transform", "rotate(-90)");
Which gives the desired behavior if I understand correctly, updated plunkr

D3: Are "parabolic" scales possible?

I'm working on a chart for which each observation has a value between [-100,100], and I want to plot each point's position on a scale. The challenge is that the vast majority of the points have values in one region of the scale (the distribution is essentially Gaussian with mean 0).
In the past, when I've needed to plot something like a Zipf probability density distribution, I've used log scales to spread out the points in the congested region. Now my situation is similar, except that I have two distributions for which I need to spread out the points (the positive scale from [0, max] and the mirrored negative scale from [0, min]).
I know I could create one scale for positive values and one for negative values, but I'm wondering if it's possible to achieve this layout with only one scale. It seems that something like a parabolic scale could help out here (if that exists). Is it possible to achieve something like this in D3?
Before explaining my proposed solution, some considerations about the comments in this question: you cannot use a log scale in your situation. This is an easy mathematical principle: Log(0) is minus infinity. Actually, this is explicitly stated in the docs:
As log(0) = -∞, a log scale domain must be strictly-positive or strictly-negative; the domain must not include or cross zero.
That being said, let's go to the proposed solution.
You could create your own scale (it's not that complicated). However, here, I'll use a interpolate function, based on this excellent answer (not a duplicate, though, because you want the opposite) and this code from Mike Bostock.
Using a linear scale, we set an interpolator:
var xScale = d3.scaleLinear()
.domain([-100, 100])
.interpolate(easeInterpolate(d3.easeQuadInOut));
Then, we use an easing in the easeInterpolate function:
function easeInterpolate(ease) {
return function(a, b) {
var i = d3.interpolate(a, b);
return function(t) {
return i(ease(t));
};
};
}
Here I'm using d3.easeQuadInOut, which I think suits you, but you can change this for another one, or even creating your own.
Have a look at this demo. I'm creating 50 circles, evenly spaced from -100 to +100 (-100, -96, -92, -88... until +100). You can see that they are moved away from the center. If you use this scale with your data, you'll avoid the crowded data points around zero:
var data = d3.range(51).map(function(d) {
return -100 + (d * 4)
});
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 100);
var xScale = d3.scaleLinear()
.domain([-100, 100])
.range([20, 580])
.interpolate(easeInterpolate(d3.easeQuadInOut));
svg.append("g")
.attr("transform", "translate(0,70)")
.call(d3.axisBottom(xScale));
svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d)
})
.attr("cy", 50)
.attr("r", 4)
.attr("fill", "teal")
function easeInterpolate(ease) {
return function(a, b) {
var i = d3.interpolate(a, b);
return function(t) {
return i(ease(t));
};
};
}
<script src="https://d3js.org/d3.v4.min.js"></script>
In case you ask, that last tick is not 80100. That's just the 80 tick overlapping with the 100 tick (the same thing happens with the -80 and the -100).
Also, it is worth noting that there is nothing wrong in using transformed scales, and that even if it does deform or skew the chart, it's perfect valid and does not lead to misinterpretations, as long as you inform the users about the transformation.

Categories