Is there an "axis equal" in D3? - javascript

I am using D3 and Python to create some plots. With Python I am able to do axis equal, as shown in image 1:
I want to achieve the same in an SVG using D3. Please refer to the image 2, without axis equal:
The axes scale which I am using are as follows:
var xScale = d3.scaleLinear()
.range([0, width])
.domain([minX, maxX]);
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([minY, maxY]);
var xAxis = d3.axisBottom(xScale).ticks(5),
yAxis = d3.axisLeft(yScale).ticks(5);
Could anyone tell me how can I achieve the axis equal with D3?

Short answer: no, there is no "axis equal" in D3. D3 is quite low level, it's just a collection of methods, so you have to do almost everything by hand...
The good news is that all you need for solving your problem is some math to calculate the new ranges.
As an introduction, since this is a d3.js tagged question, "axis equal" is a term used in some programs like Matlab, which...
Use the same length for the data units along each axis.
It can be better explained visually. Have a look at this image, with different domains and ranges (source):
After this brief introduction, let's go back to your problem. Suppose this simple code, generating two axes:
const minX = 0,
minY = 0,
maxX = 120,
maxY = 50,
width = 500,
height = 300,
paddings = [10, 10, 20, 30];
const xScale = d3.scaleLinear()
.range([paddings[3], width - paddings[1]])
.domain([minX, maxX]);
const yScale = d3.scaleLinear()
.range([height - paddings[2], paddings[0]])
.domain([minY, maxY]);
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("transform", `translate(0,${height - paddings[2]})`)
.call(d3.axisBottom(xScale).tickSizeInner(-height + paddings[2] + paddings[0]));
svg.append("g")
.attr("transform", `translate(${paddings[3]},0)`)
.call(d3.axisLeft(yScale).tickValues(xScale.ticks()).tickSizeInner(-width + paddings[3] + paddings[1]));
svg {
border: 2px solid tan;
}
line {
stroke-dasharray: 2, 2;
stroke: gray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
As we can clearly see, the units are not equally spaced. We can do two things for creating axes with equally spaced units:
1. Change the domains;
2. Change the ranges.
According to your comment, changing the domains is not an option. So, let's change the ranges instead.
All we need is calculating the number of units per user space, and using the biggest among them (x or y) to change the range of the opposite scale.
For instance, given the scales of the snippet above:
const xStep = Math.abs((xScale.domain()[1] - xScale.domain()[0]) / (xScale.range()[1] - xScale.range()[0]));
const yStep = Math.abs((yScale.domain()[1] - yScale.domain()[0]) / (yScale.range()[1] - yScale.range()[0]));
xStep is bigger than yStep, showing us that the x axis has more units per pixels. Therefore, we'll change the y axis range:
yScale.range([yScale.range()[0], yScale.range()[0] - (yScale.domain()[1] - yScale.domain()[0]) / xStep]);
And here is the demo:
const minX = 0,
minY = 0,
maxX = 120,
maxY = 50,
width = 500,
height = 300,
paddings = [10, 10, 20, 30];
const xScale = d3.scaleLinear()
.range([paddings[3], width - paddings[1]])
.domain([minX, maxX]);
const yScale = d3.scaleLinear()
.range([height - paddings[2], paddings[0]])
.domain([minY, maxY]);
const xStep = Math.abs((xScale.domain()[1] - xScale.domain()[0]) / (xScale.range()[1] - xScale.range()[0]));
const yStep = Math.abs((yScale.domain()[1] - yScale.domain()[0]) / (yScale.range()[1] - yScale.range()[0]));
yScale.range([yScale.range()[0], yScale.range()[0] - (yScale.domain()[1] - yScale.domain()[0]) / xStep]);
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("transform", `translate(0,${height - paddings[2]})`)
.call(d3.axisBottom(xScale).tickSizeInner(-height + paddings[2] + yScale.range()[1]));
svg.append("g")
.attr("transform", `translate(${paddings[3]},0)`)
.call(d3.axisLeft(yScale).tickValues(yScale.ticks().filter(function(d){return !(+d%10)})).tickSizeInner(-width + paddings[3] + paddings[1]));
svg {
border: 2px solid tan;
}
line {
stroke-dasharray: 2, 2;
stroke: gray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
As you can see using the gridlines, the units are equally spaced now.
Finally, have in mind that determining programmatically what scale will be changed, readjusting the correct range (your y scale, for instance, goes from the bottom to the top), centralising the axes etc are whole different issues.

Related

How can I remove non-integer ticks while zooming in with D3.js?

Problem: I am trying to create a pannable/zoomable Linear X axis with ticks on each integer from 0 to a predefined large number (let's say 1000 but could be much higher). Obviously this will not all fit on the screen and be readable, so I need it to go offscreen and be pannable. Starting with 0 through 10 is acceptable. There are a number of problems with my solution so far, but the first being that if I zoom in, D3 automatically adds ticks in between the integers at decimal places to continue to have 10 ticks on the screen. The second being when I zoom out, the range will display beyond 1000.
A quick note: The data that will be displayed is irrelevant. The axis has to go from 0 to 1000 regardless if there is a datapoint on 1000 or not so I cannot determine the domain based on my dataset.
Solutions I've tried:
.ticks(). This lets me specify the number of ticks I want. However this appears to put all the ticks into the initial range specified. If I increase the width of the range past the svg size, it spreads out the ticks, but not in any controllable way (to my knowledge). Additionally I run into performance issues where the panning and zooming. I find this behavior strange since if I don't specify ticks, it makes an infinite number I can pan through without any lag. I assume the lag occurs because it's trying to render all 1000 ticks immediately and the default d3.js functionality renders them dynamically as you scroll. This seems like a potential solution, but I'm not sure how to execute it.
.tickValues(). I can provide an array of 0 through 1000 and this works but exhibits the exact same lag behavior as .ticks(). This also doesn't dynamically combine ticks into 10s or 100s as I zoom out.
.tickFormat(). I can run a function through so that any non-integer number is converted to an empty string. However, this still leaves the tick line.
Here are the relevant parts of my code using the latest version of D3.js(7.3):
const height = 500
const width = 1200
const margin = {
top: 20,
right: 20,
bottom: 20,
left: 35
}
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const xScale = d3.scaleLinear()
.domain([0, 10])
.range([0, innerWidth])
const svg = d3.select('svg')
.attr("width", width)
.attr("height", height)
svg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', innerWidth)
.attr('height', innerHeight)
const zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent([
[0, 0],
[xScale(upperBound), 0]
])
.on('zoom', zoomed)
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
const xAxis = d3.axisBottom()
.scale(xScale)
const xAxisG = g.append('g')
.style('clip-path', 'url(#clip)')
.attr("class", "x-axis")
.call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`)
svg.call(zoom)
function zoomed(event) {
const updateX = event.transform.rescaleX(xScale)
const zx = xAxis.scale(updateX)
xAxisG.call(zx)
}
Instead of dealing with the axis' methods, you can simply select the ticks in the container group itself, removing the non-integers:
xAxisG.call(zx)
.selectAll(".tick")
.filter(e => e % 1)
.remove();
Here is your code with that change:
const upperBound = 1000
const height = 100
const width = 600
const margin = {
top: 20,
right: 20,
bottom: 20,
left: 35
}
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const xScale = d3.scaleLinear()
.domain([0, 10])
.range([0, innerWidth])
const svg = d3.select('svg')
.attr("width", width)
.attr("height", height)
svg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', innerWidth)
.attr('height', innerHeight)
const zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent([
[0, 0],
[xScale(upperBound), 0]
])
.on('zoom', zoomed)
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
const xAxis = d3.axisBottom()
.scale(xScale)
.tickFormat(d => ~~d)
const xAxisG = g.append('g')
.style('clip-path', 'url(#clip)')
.attr("class", "x-axis")
.call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`)
svg.call(zoom)
function zoomed(event) {
const updateX = event.transform.rescaleX(xScale)
const zx = xAxis.scale(updateX)
xAxisG.call(zx)
.selectAll(".tick")
.filter(e => e % 1)
.remove();
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg></svg>

D3.js: Calculating the y Scale from a Stack function

I want a ‘mirrored’ bar chart (i.e. one that looks like a sound wave) and have come up with the following using the d3 stack generator and a linear y scale:
import * as d3 from "d3";
const WIDTH = 300;
const HEIGHT = 300;
const LIMIT = 4;
const container = d3.select("svg").append("g");
var data = [];
for (var i = 0; i < 100; i++) {
data.push({
index: i,
value: Math.random() * LIMIT
});
}
var stack = d3
.stack()
.keys(["value"])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetSilhouette);
var series = stack(data);
var xScale = d3
.scaleLinear()
.range([0, WIDTH])
.domain([0, data.length]);
var yScale = d3
.scaleLinear()
.range([HEIGHT, 0])
.domain([0, LIMIT / 2]);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
container
.selectAll(".bar")
.data(series[0])
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", d => {
return xScale(d.data.index);
})
.attr("y", d => {
return yScale(d[0]) / 2 - HEIGHT / 2;
})
.attr("width", WIDTH / series[0].length)
.attr("height", d => yScale(d[1]));
However, I feel like I’ve hacked the calculations for both the y scale domain and for positioning the blocks.
For the domain I currently use 0 to the data's upper limit / 2.
For my y position I use yScale(d[0]) / 2 - HEIGHT / 2; despite the height being directly based off the scale i.e. d => yScale(d[1]).
Is there a better, more idiomatic way to achieve what I want?
It seems the way the stack function calculates values has changed since D3 v2, and therefore I had to do two things to achieve this in a nicer way.
I switched my y scale domain to be the extents of the data and then translated by -0.5 * HEIGHT
I modified my calculation for the y position and height:
.attr('y', d => yScale(d[1]))
.attr('height', d => yScale(d[0]) - yScale(d[1]));

Show only max values in d3 bar chart

I have the following bar chart.
https://jsfiddle.net/zyjp1abo/
As you can see the values are between 1000 and 1005. Showing all the data from 0 to 1005 does not sense since the differences aren't visible.
I'd like to show the bars from 1000 and 1005 and change the y axis accordingly. Simply using extent and changing the domain does not work since the bars are drawn through the bottom margin. I want them to stop at the lowest value, i.e 1000.
https://jsfiddle.net/zyjp1abo/1/
Any ideas? Thank you!
If you want the domain to go from 1000 to 1005, you should use d3.extent. That's not the problem.
The problem is that you are using d3.extent but you keep using y(0) both for translating your x axis and for calculating the bars heights, which is wrong. You have to use your height and your margins.
Here is your code with those changes:
var defaults = {
target: '#chart',
width: 500,
height: 170,
margin: {
top: 20,
right: 20,
bottom: 20,
left: 50
},
yTicks: 5
}
class Barchart {
constructor(config) {
Object.assign(this, defaults, config)
const {
target,
width,
height,
margin
} = this
const w = width - margin.left - margin.right
const h = height - margin.top - margin.bottom
const {
yTicks
} = this
this.chart = d3.select(target)
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
this.x = d3.scaleBand()
.rangeRound([0, w])
.padding(0.1)
this.y = d3.scaleLinear()
.rangeRound([h, 0])
this.xAxis = d3.axisBottom(this.x)
this.chart.append('g')
.attr('class', 'x axis')
this.yAxis = d3.axisLeft(this.y)
.ticks(yTicks)
this.chart.append('g')
.attr('class', 'y axis')
}
render(data) {
const {
x,
y,
xAxis,
yAxis,
chart
} = this
// y.domain(d3.extent(data, v => v.value))
y.domain(d3.extent(data, v => v.value))
const domain = data.map(d => d.timestamp)
x.domain(domain)
chart.select('.x.axis')
.attr('transform', `translate(0, ${(defaults.height - defaults.margin.bottom - defaults.margin.top)})`)
.call(xAxis)
chart.select('.y.axis')
.call(yAxis)
const bars = chart.selectAll('.bar')
.data(data)
bars
.enter()
.append('rect')
.attr('class', 'bar')
.merge(bars)
.attr('x', d => x(d.timestamp))
.attr('y', d => y(d.value))
.attr('width', x.bandwidth())
.attr('height', d => defaults.height - defaults.margin.bottom - defaults.margin.top - y(d.value))
}
}
const random = (min = -10, max = 10) => (
window.Math.floor(window.Math.random() * (max - min + 1)) + min
)
let bar = []
for (let i = 0; i < 20; i++) {
bar.push({
timestamp: Date.now() - (19 - i) * 500,
value: random(1000, 1005)
})
}
const barchart = new Barchart()
barchart.render(bar)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg id="chart">
</svg>
PS: You could simplify the math for the SVG size and margins.
PPS: There is a lot of debate regarding if a dataviz can have a non-zero baseline. I believe that some kinds of charts, in some situations, can and should have a non-zero baseline, specially line charts, when the change rate is more important than the absolute value, for instance. However, bar charts should always have a zero baseline.
After you change your domain, this line:
.attr('height', d => Math.abs(y(d.value) - y(0)))
Is still calculating the height of the bar based of a 0 value. The conventional way to calculate the height as in this example, is to base it off the inner height of your chart (your variable h).
Here's an updated fiddle.
y.domain([0, d3.max(data, v => v.value)])
change 0 here to 1000, and other places respectively

Scale in different units

How to use D3 to convert and display the right information from different units
E.g.
All data is in mm..
[
{ label: 'sample1', x: 300 },
{ label: 'sample2', x: 1200 },
{ label: 'sample3', x: 4150 }
]
So, the question is, how can I create a scale that understand the sample3 should be point in same place after the 4 and before 5.
Consider
10000, its just a sample, can be 102301 or any value
I want to use D3 scale if possible to do this conversion
Attempt
let scaleX = d3.scale.linear().domain([-10, 10]).range([0, 500]) // Missing the mm information...
You have a conceptual problem here:
Mapping an input (domain) to an output (range): that's the task of the scale.
Formatting the number and the unit (if any) in the axis: that's the task of the axis generator
Thus, in your scale, you'll have to set the domain to accept the raw, actual data (that is, the data the way it is) you have:
var scale = d3.scaleLinear()
.domain([-10000, 10000])//the extent of your actual data
.range([min, max]);
Then, in the axis generator, you change the value in the display. Here, I'm simply dividing it by 1000 and adding "mm":
var axis = d3.axisBottom(scale)
.tickFormat(d => d / 1000 + "mm");
Note that I'm using D3 v4 in these snippets.
Here is a demo using these values: -7500, 500 and 4250. You can see that the circles are in the adequate position, but the axis shows the values in mm.
var data = [-7500, 500, 4250];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var scale = d3.scaleLinear()
.domain([-10000, 10000])
.range([20, 480]);
var axis = d3.axisBottom(scale)
.tickFormat(d => d / 1000 + "mm");
var circles = svg.selectAll("foo")
.data(data)
.enter()
.append("circle")
.attr("r", 4)
.attr("fill", "teal")
.attr("cy", 40)
.attr("cx", d => scale(d));
var g = svg.append("g")
.attr("transform", "translate(0,60)")
.call(axis);
<script src="https://d3js.org/d3.v4.js"></script>
I found a way to do that..
const SIZE_MM = 10000
const SIZE_PX = 500
const scaleFormat = d3.scale.linear().domain([0, SIZE_MM]).range([-10, 10])
const ticksFormat = d => Math.round(scaleFormat(d))
const ticks = SIZE_MM / SIZE_PX
const lineScale = d3.scale.linear().domain([0, SIZE_MM ]).range([0, SIZE_PX])
lineScale(9500)
// 475

d3 Hexagonal Binning domain and range

I'm trying to create a hexagonal binning graph based on this example: http://bl.ocks.org/mbostock/4248145
I've already figured out how to set the domain of each axis. But I'm having troubles setting the points, and I think this is because of the range.
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 800 - margin.left - margin.right, // 740px
height = 500 - margin.top - margin.bottom; // 450px
var points = [[1000,30],[5000,40],[8000,50]]
var x = d3.scale.linear().domain([0, 10000]).range([0, width]);
var y = d3.scale.linear().domain([18, 65]).range([height, 0]);
Using these points, the graph comes out blank.
But if I try these points:
var points = [[100,30],[200,40],[300,50]]
They appear on the graph but not even close to where they should be. [100,30], for example, it appears what should be something like [1400,62].
I already read this post about scales on d3. But I haven't figured out how to display the points correctly.
JSFiddle: http://jsfiddle.net/P6KWZ/
There are two things in your jsfiddle. First, you're not actually using the scales you've created to position the hexagons. Second, you're computing the domains based on the original values and the drawing the values computed by the hexbin.
So first compute the domains based on the binned values:
var projectedPoints = hexbin(points);
var x = d3.scale.linear()
.domain(d3.extent(projectedPoints, function(d) { return d.x; }))
.range([0, width]);
var y = d3.scale.linear()
.domain(d3.extent(projectedPoints, function(d) { return d.y; }))
.range([height, 0]);
And then translate the hexagons using the scales:
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; })
Complete demo here.

Categories

Resources