Personalized axis range - javascript

I have values going these kinds of range (400k to 600 million)
And I would like to create a comprehensive y-axis on a D3 plot.
If I use a log scale, all the variations in the huge numbers are erased, If I use a linearScale, all variations in the small numbers is also erased.
Therefore I thought of doing two-axis (one over the other like in the picture below) but I don't know if there is a simpler way.
Can I specify all the tick values to get an axis where all the variations would be visible?
Thank you.

Use a regular linear scale with more than two values in both the domain and range, thus creating a piecewise scale.
For instance:
const scale = d3.scaleLinear()
.domain([0, 5e7, 1e8, 6e8])
.range([h-10, h/2 + 10, h/2 - 10, 10]);
Here is the running code:
const svg = d3.select("svg");
const h = 600;
const scale = d3.scaleLinear()
.domain([0, 5e7, 1e8, 6e8])
.range([h - 10, h / 2 + 10, h / 2 - 10, 10]);
const axis = d3.axisLeft(scale).tickValues(scale.ticks().concat(d3.range(0, 5e7, 5e6)))(svg.append("g").attr("transform", "translate(100,0)"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="200", height="600"></svg>
Do not forget to make sure that the user understands the y axis is truncated, preferably with a clear annotation showing that.

Related

Y-axis should display whole number values from 0 to maxYValue on d3.js without repetition

I want to show values like [0,1,2,..., maxYValue] instead of [0, 0.5, 1, 1.5 ... maxYValue].
var y = d3.scaleLinear()
.range([height - margin, 0])
.domain([0, this.maxYValue]); //maxYValue can be any whole. number
Original:
d3.axisLeft(y).ticks(5).tickSize(-width + margin)
Modified:
d3.axisLeft(y).ticks(5).tickSize(-width + margin).tickFormat("f")
After using tickFormat("f"), it starts displaying the y-axis values like [0,0,0, 1, 1, 1]. It is rounding off the values. But, I want to set a step value to the y-axis values to get the data like [0,1,2,3,.., maxYValue]
If you simply wants all integers up to the maxYValue, instead of using axis.tickFormat on the auto-generated ticks just use d3.range to generate an array based on the scale's domain, and pass that to axis.tickValues:
d3.axisLeft(y).tickValues(d3.range(y.domain()[0], y.domain()[1] + 1, 1));
For instance:
const y = d3.scaleLinear()
.domain([0, 12]);
console.log(d3.range(y.domain()[0], y.domain()[1] + 1, 1))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
If maxYValue is not the last value in the scale's domain, just use that variable instead.

How is the number of ticks on an axis defined?

I'm new to d3 and have the following code for creating the x-axis on my graph:
export const drawXAxis = (svg, timestamps, chartWidth, chartHeight) => {
console.log(chartWidth); // 885
console.log(timestamps.length); // 310
const xScale = d3.scaleLinear()
.domain([-1, timestamps.length])
.range([0, chartWidth]);
const xBand = d3.scaleBand()
.domain(
d3.range(-1, timestamps.length))
.range([0, chartWidth])
.padding(0.3);
const xAxis = d3.axisBottom()
.scale(xScale)
.tickFormat(function(d) {
const ts = moment.utc(timestamps[d]);
return ts.format('HH') + 'h';
});
const gX = svg.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + chartHeight + ")")
.call(xAxis);
return [xScale, xBand, xAxis, gX];
};
As I understand it, d3 decides on the number of ticks that appears on the X-axis.
In order to gain more control over the values appearing on the X-axis for zooming purposes, I would like to understand how d3 determines that - in this case - I have 16 ticks.
What If I want to space the ticks more evenly, for example, I want to see a tick on every 12 or 6 hours? My data contains 0 -> 23 hour values per day consistently, but d3 displays random hours on my graph.
I'm gonna answer just the question in the title ("how is the number of ticks on an axis defined?"), not the one you made at the end ("What If I want to space the ticks more evenly, for example, I want to see a tick on every 12 or 6 hours?"), which is not related and quite simple to fix (and, besides that, it's certainly a duplicate).
Your question demands a detective work. Our journey starts, of course, at d3.axisBottom(). If you look at the source code, you'll see that the number of ticks in the enter selection...
tick = selection.selectAll(".tick").data(values, scale).order()
...depends on values, which is:
var values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues
What this line tells us is that, if tickValues is null (no tickValues used), the code should use scale.ticks for scales that have a ticks method (continuous), our just the scale's domain for ordinal scales.
That leads us to the continuous scales. There, using a linear scale (which is the one you're using), we can see at the source code that scale.ticks returns this:
scale.ticks = function(count) {
var d = domain();
return ticks(d[0], d[d.length - 1], count == null ? 10 : count);
};
However, since ticks is imported from d3.array, we have to go there for seeing how the ticks are calculated. Also, since we didn't pass anything as count, count defaults to 10.
So, finally, we arrive at this:
start = Math.ceil(start / step);
stop = Math.floor(stop / step);
ticks = new Array(n = Math.ceil(stop - start + 1));
while (++i < n) ticks[i] = (start + i) * step;
Or this:
start = Math.floor(start * step);
stop = Math.ceil(stop * step);
ticks = new Array(n = Math.ceil(start - stop + 1));
while (++i < n) ticks[i] = (start - i) / step;
Depending on the value of steps. If you look at the tickIncrement function below, you can see that steps can only be 1, 2, 5 or 10 (and their negatives).
And that's all you need to know the length of the array in the variable ticks above. Depending on the start and stop values (i.e., depending on the domain), sometimes we have more than 10 ticks (16 in your case), sometimes we have less than 10, even if the default count is 10. Have a look here:
const s = d3.scaleLinear();
console.log(s.domain([1,12]).ticks().length);
console.log(s.domain([100,240]).ticks().length);
console.log(s.domain([10,10]).ticks().length);
console.log(s.domain([2,10]).ticks().length);
console.log(s.domain([1,4]).ticks().length);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The last example, as you can see, gives us 16 ticks.

Display dynamic frequencies on axis

If I've got a list of frequencies, all in the same unit (Hz, MHz, etc.), how can I use a scale to display them nicely on the x-axis? I want them to be labeled Hz/kHz/MHz and scaled appropriately.
What you actually want is simply a number with a SI prefix followed by Hz as the unit of measurement.
That being said, you can use a linear scale and a specific tickFormat:
const format = d3.format(".0s");
axis.tickFormat(function(d) {
return format(d) + "Hz";
});
Where axis is your axis generator, whatever it is. Here, ".0s" in the specifier formats the number to use SI prefixes and no decimal places. Then, you get the resulting string and add "Hz" to it.
Here is a demo, the domain changes continually between [0, 1] and [0, 100000000000]:
const svg = d3.select("svg");
const scale = d3.scaleLinear()
.domain([0, 1000])
.range([30, 470]);
const format = d3.format(".0s");
const axis = d3.axisBottom(scale)
.tickFormat(function(d) {
return format(d) + "Hz";
});
const g = svg.append("g")
.attr("transform", "translate(0,50)");
g.call(axis);
d3.interval(function() {
scale.domain([0, Math.pow(10, ~~(Math.random() * 12))]);
g.transition()
.duration(1000)
.call(axis)
}, 2000);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="100"></svg>

Make circles not go outside of the chart bounds with D3

I am working on a chart looking like this now:
I use d3 scales and ranges to setup sizes and coordinates of circles, from JSON data.
All works fine but I need to make sure those circles that are close to extreme values don't overlap the sides of the chart (like orange circle on the top right and blue one on the bottom side), so I think I need to play with ranges and change coordinates in case they overlap or is there a better tried way to do this?
When drawing circles, in addition to the x and y scaling functions we also use an r scaling function:
var rScale = d3.scale.linear()
.domain([0, maxR])
.range([0, maxBubbleRadius]);
var xScale = d3.scale.linear()
.domain([minX, maxX])
.range([0, chartWidth]);
var yScale = d3.scale.linear()
.domain([minY, maxY])
.range([chartHeight, 0]);
where maxR is the largest r value in your dataset and maxBubbleRadius is however large you want the largest circle to be, when you plot it.
Using the x and y scaling functions it is easy to calculate where the centre of each circle will be plotted, we can then add on the (scaled) r value to see if the circle will spill over a chart boundary. With a scenario like the first chart below we can see that 4 of the circles spill over. The first step to remedy this is to find out how many vertical and horizontal units we spill over by and then increase the minimum and maximum x and y values to take this into account, before recalculating the xScale and yScale vars. If we were to then plot the chart again, the boundary would move out but there would probably still be some visible spillage (depending on actual values used); this is because the radius for a given circle is a fixed number of pixels and will therefore take up a different number of x and y units on the chart, from when we initially calculated how much it spilled over. We therefore need to take an iterative approach and keep applying the above logic until we get to where we want to be.
The code below shows how I iteratively achieve an acceptable scaling factor so that all the circles will plot without spilling. Note that I do this 10 times (as seen in the loop) - I've just found that this number works well for all the data that I've plotted so far. Ideally though, I should calculate a delta (the amount of spillage) and iterate until it is zero (this would also require overshooting on the first iteration, else we'd never reach our solution!).
updateXYScalesBasedOnBubbleEdges = function() {
var bubbleEdgePixels = [];
// find out where the edges of each bubble will be, in terms of pixels
for (var i = 0; i < dataLength; i++) {
var rPixels = rScale(_data[i].r),
rInTermsOfX = Math.abs(minX - xScale.invert(rPixels)),
rInTermsOfY = Math.abs(maxY - yScale.invert(rPixels));
var upperPixelsY = _data[i].y + rInTermsOfY;
var lowerPixelsY = _data[i].y - rInTermsOfY;
var upperPixelsX = _data[i].x + rInTermsOfX;
var lowerPixelsX = _data[i].x - rInTermsOfX;
bubbleEdgePixels.push({
highX: upperPixelsX,
highY: upperPixelsY,
lowX: lowerPixelsX,
lowY: lowerPixelsY
});
}
var minEdgeX = d3.min(bubbleEdgePixels, function(d) {
return d.lowX;
});
var maxEdgeX = d3.max(bubbleEdgePixels, function(d) {
return d.highX;
});
var minEdgeY = d3.min(bubbleEdgePixels, function(d) {
return d.lowY;
});
var maxEdgeY = d3.max(bubbleEdgePixels, function(d) {
return d.highY;
});
maxY = maxEdgeY;
minY = minEdgeY;
maxX = maxEdgeX;
minX = minEdgeX;
// redefine the X Y scaling functions, now that we have this new information
xScale = d3.scale.linear()
.domain([minX, maxX])
.range([0, chartWidth]);
yScale = d3.scale.linear()
.domain([minY, maxY])
.range([chartHeight, 0]);
};
// TODO: break if delta is small, rather than a specific number of interations
for (var scaleCount = 0; scaleCount < 10; scaleCount++) {
updateXYScalesBasedOnBubbleEdges();
}
}

Custom axis function in D3JS

I'm using D3JS and I want an axis in x with this kind of values : 125, 250, 500, 1000 ... until 8000. So multiply by 2 my values each time.
So I tried a Quantize Scales but axis do not support it.
How can I do this ? Can I do a custom mathematical function like y = mx + b (where m = 2 in my case and b = 0) and use it in axis?
Here you can see my code
Podelo
The linear scale is pretty flexible if you mass in multiple values for the range and domain to create a polylinear scale:
tickWidth = (ChartWidth - padding)/7
xScale = d3.scale.linear()
.domain([0, 125, 250, 500, 1000, 2000, 4000, 8000])
.range(d3.range(8).map(function(d){ return d*tickWidth; }));
http://jsfiddle.net/h2juD/6/

Categories