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)
}
CodePudding user response:
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>