Skip to content

Commit 916c50b

Browse files
committed
Make sure x-axis ticks fit
1 parent 8c0b8c2 commit 916c50b

File tree

1 file changed

+98
-37
lines changed

1 file changed

+98
-37
lines changed

assets/js/dashboard/stats/graph/main-graph.tsx

Lines changed: 98 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -128,50 +128,110 @@ export const MainGraph = ({
128128
}
129129
])
130130

131-
// Create the SVG container.
131+
// create the SVG container
132132
const svg = d3.select(svgRef.current)
133133

134134
const maxXTicks = 8
135135
const xTickCount = Math.min(remappedData.length, maxXTicks)
136-
// Add the x-axis.
137-
svg
136+
// add the x-axis
137+
let xAxisBoundingRect: DOMRect
138+
const xAxisSelection = svg
138139
.append('g')
140+
.attr('class', 'x-axis--container')
139141
.attr('transform', `translate(0,${yBottomEdge})`)
140-
.call(
141-
d3
142-
.axisBottom(x)
143-
.ticks(xTickCount)
144-
.tickSize(0)
145-
.tickFormat((bucketIndex) => {
146-
// for low tick counts, it may try to render ticks
147-
// with the index 0.5, 1.5, etc which don't have data defined
148-
const datum = remappedData[bucketIndex.valueOf()]
149-
return datum
150-
? datum.timeLabel
151-
? getXLabel(datum.timeLabel, {
152-
shouldShowYear: hasMultipleYears,
153-
period,
154-
interval,
155-
bucketIndex: bucketIndex.valueOf(),
156-
totalBuckets: remappedData.length
157-
})
142+
.each(function () {
143+
const elem = this as SVGGraphicsElement
144+
xAxisBoundingRect = elem.getBoundingClientRect()
145+
})
146+
147+
// try a few x-axis labels until one fits
148+
const tries = xTickCount - 2
149+
for (const [index, tickCount] of new Array(tries)
150+
.fill(null)
151+
.map((_, index) => xTickCount - index)
152+
.entries()) {
153+
const axis = xAxisSelection
154+
.append('g')
155+
.attr('class', 'x-axis')
156+
.attr('opacity', 0)
157+
.call(
158+
d3
159+
.axisBottom(x)
160+
.ticks(tickCount)
161+
.tickSize(4)
162+
.tickFormat((bucketIndex) => {
163+
// for low tick counts, it may try to render ticks
164+
// with the index 0.5, 1.5, etc which don't have data defined
165+
const datum = remappedData[bucketIndex.valueOf()]
166+
return datum
167+
? datum.timeLabel
168+
? getXLabel(datum.timeLabel, {
169+
shouldShowYear: hasMultipleYears,
170+
period,
171+
interval,
172+
bucketIndex: bucketIndex.valueOf(),
173+
totalBuckets: remappedData.length
174+
})
175+
: ''
158176
: ''
159-
: ''
160-
})
161-
)
162-
.call((g) => g.select('.domain').remove())
163-
.call((g) => g.selectAll('.tick').attr('class', 'tick group'))
164-
.call((g) =>
165-
g
166-
.selectAll('.tick text')
167-
.attr('class', classNames(tickClass, 'translate-y-2'))
177+
})
178+
)
179+
.call((g) => g.select('.domain').remove())
180+
.call((g) => g.selectAll('.tick').attr('class', 'tick group'))
181+
.call((g) =>
182+
g.selectAll('.tick line').attr('class', classNames(xTickLineClass))
183+
)
184+
.call((g) =>
185+
g
186+
.selectAll('.tick text')
187+
.attr('class', classNames(tickTextClass, 'translate-y-2'))
188+
)
189+
190+
let overlapCount = 0
191+
let lastTickTextRightEdge = 0
192+
axis.call((g) =>
193+
g.selectAll('.tick text').each(function (_, index, groups) {
194+
const elem = this as SVGGraphicsElement
195+
let textRect = elem.getBoundingClientRect()
196+
197+
const minX = xAxisBoundingRect.left
198+
const maxX = xAxisBoundingRect.left + width
199+
200+
// nudge over first tick text if needed, remeasure
201+
if (index === 0) {
202+
const distanceFromAxisEdge = textRect.left - minX
203+
if (distanceFromAxisEdge < 0) {
204+
d3.select(elem).attr('dx', -distanceFromAxisEdge)
205+
textRect = elem.getBoundingClientRect()
206+
}
207+
}
208+
// nudge back last tick text if needed, remeasure
209+
if (index === groups.length - 1) {
210+
const distanceFromAxisEdge = maxX - textRect.right
211+
if (distanceFromAxisEdge < 0) {
212+
d3.select(elem).attr('dx', distanceFromAxisEdge)
213+
textRect = elem.getBoundingClientRect()
214+
}
215+
}
216+
const isOverlappingPrevious = textRect.left < lastTickTextRightEdge
217+
if (isOverlappingPrevious) {
218+
overlapCount++
219+
}
220+
lastTickTextRightEdge = textRect.right
221+
})
168222
)
169-
// Add the y-axis, remove the domain line, add grid lines and a label.
170-
// TODO: make dynamic
171-
// const maxYTicks = 8
223+
if (overlapCount > 0 && index !== tries - 1) {
224+
axis.remove()
225+
} else {
226+
break
227+
}
228+
}
229+
xAxisSelection.call((g) => g.select('.x-axis').attr('opacity', 1))
230+
172231
const yTickCount = 8
173232
svg
174233
.append('g')
234+
.attr('class', 'y-axis--container')
175235
.attr('transform', `translate(${xLeftEdge}, 0)`)
176236
.call(
177237
d3
@@ -182,13 +242,13 @@ export const MainGraph = ({
182242
)
183243
.call((g) => g.select('.domain').remove())
184244
.call((g) => g.selectAll('.tick').attr('class', 'tick group'))
185-
.call((g) => g.selectAll('.tick text').attr('class', tickClass))
245+
.call((g) => g.selectAll('.tick text').attr('class', tickTextClass))
186246
.call((g) =>
187247
g
188248
.selectAll('.tick line')
189249
.clone()
190250
.attr('x2', chartAreaWidth)
191-
.attr('class', tickLineClass)
251+
.attr('class', yTickLineClass)
192252
)
193253

194254
const mainGradientId = addGradient({
@@ -763,9 +823,10 @@ const paletteByTheme = {
763823
}
764824
}
765825

766-
const tickLineClass =
826+
const yTickLineClass =
767827
'stroke-gray-150 dark:stroke-gray-800/75 group-first:stroke-gray-300 dark:group-first:stroke-gray-700'
768-
const tickClass = 'fill-gray-500 dark:fill-gray-400 text-xs'
828+
const tickTextClass = 'fill-gray-500 dark:fill-gray-400 text-xs'
829+
const xTickLineClass = 'stroke-gray-300 dark:stroke-gray-700'
769830

770831
const mainDotClass = 'fill-indigo-500 dark:fill-indigo-400'
771832
const comparisonDotClass = 'fill-indigo-500/20 dark:fill-indigo-400/20'

0 commit comments

Comments
 (0)