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>
Related
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.
I'm newbie with D3 and I'm trying to set plots in a graphic where x Axis has labels.
I set the points, but not correctly over the line of each x label. You can check it on this image:
You can find the code in codesandbox:
The file where I define the Scatter Plot is: MultipleScatterPlot.js
What am I doing wrong?
Fixed: I have fixed my problem using the method padding(1)
/**
* Method which serves us to define the scale and range of the graphics
* #param {*} data
*/
setScales(data) {
let xRange = [
this.state.OFFSET_LEFT,
this.state.WIDTH - this.state.OFFSET_RIGHT
];
let yRange = [
this.state.OFFSET_TOP,
this.state.HEIGHT - this.state.OFFSET_BOTTOM
]; // flip order because y-axis origin is upper LEFT
let xScale = d3
.scaleBand()
.domain(
data.map(d => {
return d.value_x;
})
)
.range(xRange)
.padding(1);
let yScale = d3
.scaleLinear()
.domain([100, 0])
.range(yRange);
return {
xScale: xScale,
yScale: yScale,
xRange: xRange,
yRange: yRange
};
}
Is hire when if we add padding(1) all points are setted to their correspondiente x_label, like you can see now:
And now, is perfect!!! It's what I want to do!!!
I would like to create a circle in d3 that draws a zig-zaggy circle. This isn't a great explanation, so I drew this picture in a drawing app.
It would be great if i could control the size / number of zags (there should be more than i drew in this picture, maybe 50 - 100 instead of 15 - 20).
Thanks in advance on this!
For that task I'd use d3.lineRadial, which:
Constructs a new radial line generator with the default settings. A radial line generator is equivalent to the standard Cartesian line generator, except the x and y accessors are replaced with angle and radius accessors
This is how the generator would look like (for the data structure used in the demo below, made of an array of arrays):
const generator = d3.lineRadial()
.angle(d => d[0])
.radius(d => d[1])
Which given the demo data can be even simpler, just:
const generator = d3.lineRadial();
And here is a demo:
const svg = d3.select("svg")
.append("g")
.attr("transform", "translate(100,50)");
const data = d3.range(51).map(d => [(Math.PI * 2) / (50 / d), d % 2 ? 40 : 50]);
const generator = d3.lineRadial()
const path = svg.append("path")
.attr("d", d3.lineRadial()(data))
path {
fill: none;
stroke: black;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
In this demo, the number of zigzags and the size of the circle is controlled by the data. For instance, less zigzags and a bigger distance between the inner and outer radiuses:
const svg = d3.select("svg")
.append("g")
.attr("transform", "translate(100,70)");
const data = d3.range(27).map(d => [(Math.PI * 2) / (26 / d), d % 2 ? 30 : 70]);
const generator = d3.lineRadial()
const path = svg.append("path")
.attr("d", d3.lineRadial()(data))
path {
fill: none;
stroke: black;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
PS: this does not create a <circle> element, which obviously would never have zigzags, but a <path> element instead.
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.
I'm trying to draw a line using D3.js. They are samples taken at intervals over a period of time. I want to draw them with a time axis for x. Each point of data is just an index in an array and I can't figure out how to set up my axis in such a way that I don't have to manually re-scale the axis before calling d3.time.scale.
Does anyone know how to clean up the scale?
Snippets out of my code. My actual code downloads the data and draws a lot of lines over different time periods with different offsets translated in the graph.
// input data
var start_time = 1352684763;
var end_time = 1352771163;
// data is exactly 100 samples taken between start_time and end_time
var data = [140,141,140,140,139,140,140,140,140,141,139,140,54,0,0,0,0,0,0,0,0,0...]
var y_max = d3.max(data);
// graph
var scale_x = d3.time.scale().domain([start_time, end_time]).range([0, 100]);
var scale_y = d3.scale.linear().domain([0, y_max]).range([height, 0]);
var step = (end_time - start_time)/100;
function re_scale(x) { return start_time + x*step; }
// for x, rescale i (0..99) into a timestamp between start_time and end_time before returning it and letting scale_x scale it to a local position. Awkward.
var line = d3.svg.line()
.x(function(d, i) { return scale_x(re_scale(i)); })
.y(scale_y)
.interpolate('basis')
var g = graph.selectAll("g")
.append('svg:path')
.attr('d', function(d) { return line(data); })
// also draw axis here...
The "domain" should refer to the span in the data, and the "range" should refer to the span on the screen.
At the moment it would be interpreting .range([0, 100]) on scale_x as a number of pixels. If you change this to .range([0, width]) it should work without needing to re-scale.
d3.time.scale() only needs to know the start and end points to produce a good axis. However if you do want a tick for every data point there are options do do this in the docs.