My problem is when i have a lot of data, the ticks on xAxis displaying too close together.
My AreaChart component:
import * as d3 from 'd3';
import dayjs from 'dayjs';
import React, { useEffect, useRef } from 'react';
import declOfNum from '../../utils/declOfNum';
import styles from './areachart.module.css';
type AreaChartType = 'date' | 'time';
interface DataPoint {
/**
* The time type.
* */
time: Date;
/**
* The value type.
* */
value: number;
}
interface AreaChartProps {
/**
* The AreaChart type.
* */
areaChartType: AreaChartType;
/**
* The AreaColor prop.
* */
mainAreaColor: string;
/**
* The data prop.
* */
mainData: DataPoint[];
/**
* The LineColor prop.
* */
mainLineColor: string;
/**
* The CompetitorArea prop.
* */
secondaryAreaColor?: string;
/**
* The secondaryData prop.
* */
secondaryData?: DataPoint[];
/**
* The Competitor line color prop.
* */
secondaryLineColor?: string;
}
export function AreaChart({
areaChartType,
mainAreaColor,
mainData,
mainLineColor,
secondaryAreaColor,
secondaryData,
secondaryLineColor
}: AreaChartProps) {
const svgRef = useRef<null | SVGSVGElement>(null);
useEffect(() => {
if (!svgRef.current) return;
const margin = { bottom: 40, left: 25, right: 20, top: 20 };
const chartWidth = svgRef.current.clientWidth - margin.left;
const chartHeight = svgRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(svgRef.current);
let timeFormat;
areaChartType === 'time' ? timeFormat = '%H:%M' : timeFormat = '%d.%m.%Y';
const xValue = (d: DataPoint) => new Date(d.time);
const xScale = d3.scaleTime()
.domain(d3.extent([...mainData, ...(secondaryData || [])], xValue) as [Date, Date])
.range([0, chartWidth])
.nice();
const yMax = d3.max([...mainData, ...(secondaryData || [])], (d: DataPoint) => d.value) <= 5 ? 5 : d3.max([...mainData, ...(secondaryData || [])], (d: DataPoint) => d.value);
const yScale = d3.scaleLinear()
.domain([0, yMax])
.range([chartHeight, 0])
.nice();
const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat(timeFormat));
const yTicksCount = 5;
const yTicks = d3.ticks(0, yMax, yTicksCount).map(Number);
const yAxis = yMax <= 5 ? d3.axisLeft(yScale).tickValues(yTicks).tickFormat(d3.format('.0f')) : d3.axisLeft(yScale);
svg.selectAll('*').remove()
const area = d3
.area<DataPoint>()
.x((d: DataPoint) => xScale(d.time))
.y0(chartHeight)
.y1((d: DataPoint) => yScale(d.value))
.curve(d3.curveBumpX);
const line = d3
.line<DataPoint>()
.x((d: DataPoint) => xScale(d.time))
.y((d: DataPoint) => yScale(d.value))
.curve(d3.curveBumpX);
const xTicksLength = xScale.ticks()
svg
.selectAll('.vertical-line')
.data(xTicksLength)
.enter()
.append('line')
.attr('class', 'vertical-line')
.attr('x1', (d: DataPoint) => xScale(d))
.attr('x2', (d: DataPoint) => xScale(d))
.attr('y1', chartHeight + margin.top * 2)
.attr('y2', 0)
.attr('stroke', '#E2E2E2')
.attr('stroke-dasharray', '0')
.attr('transform', `translate(${margin.left}, ${-margin.top})`);
const yTicksLength = yScale.ticks();
svg
.selectAll('.horizontal-line')
.data(yTicksLength)
.enter()
.append('line')
.attr('class', 'horizontal-line')
.attr('x1', 0)
.attr('x2', chartWidth)
.attr('y1', (d: DataPoint) => yScale(d))
.attr('y2', (d: DataPoint) => yScale(d))
.attr('stroke', '#E2E2E2')
.attr('stroke-dasharray', '0')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
svg
.selectAll('.last-horizontal-line')
.data(yTicksLength)
.enter()
.append('line')
.attr('class', 'horizontal-line')
.attr('x1', 0)
.attr('x2', chartWidth)
.attr('y1', yTicksLength[yTicksLength.length - 1])
.attr('y2', yTicksLength[yTicksLength.length - 1])
.attr('stroke', '#E2E2E2')
.attr('stroke-dasharray', '0')
.attr('transform', `translate(${margin.left}, -5)`);
const chart = svg.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`);
chart
.append('path')
.datum(mainData)
.attr('d', area)
.attr('fill', mainAreaColor);
chart
.append('path')
.datum(mainData)
.attr('fill', 'none')
.attr('stroke', mainLineColor)
.attr('stroke-width', 1)
.attr('d', line);
if (secondaryData) {
chart
.append('path')
.datum(secondaryData)
.attr('d', area)
.attr('fill', secondaryAreaColor);
chart
.append('path')
.datum(secondaryData)
.attr('fill', 'none')
.attr('stroke', secondaryLineColor)
.attr('stroke-width', 1)
.attr('d', line);
}
chart
.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${chartHeight})`)
.call(xAxis);
chart
.selectAll('text')
.attr('font-family', 'var(--font-default)')
.attr('font-size', '10px')
.attr('line-height', '14px')
.attr('letter-spacing', '0.3px')
.attr('color', 'var(--old-color-neutral-dark)')
.attr('display', 'block')
.attr('text-anchor', 'middle')
.attr('transform', 'translate(0, 5)');
chart
.append('g')
.attr('class', 'y-axis')
.call(yAxis)
.attr('font-family', 'var(--font-default)')
.attr('font-size', '10px')
.attr('line-height', '14px')
.attr('letter-spacing', '0.3px')
.attr('color', 'var(--old-color-neutral-dark)')
.attr('display', 'block')
.attr('transform', 'translate(5, 0)');
chart.selectAll('.domain').attr('display', 'none');
chart.selectAll('line').attr('display', 'none');
const tooltip = d3
.select('body')
.append('div')
.attr('class', styles.tooltip)
.style('opacity', 0);
const linesGroup = svg
.append('g')
.attr('class', 'perpendicular-lines')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
chart
.selectAll('circle')
.data(mainData)
.enter()
.append('circle')
.attr('cx', (d: DataPoint) => xScale(d.time))
.attr('cy', (d: DataPoint) => yScale(d.value))
.attr('r', 1)
.style('fill', 'white')
.style('stroke', mainLineColor)
.style('stroke-width', 5)
.attr('transform', `translate(0, 0)`)
.attr('z-index', '1000')
.attr('opacity', '0')
.style('cursor', 'pointer')
.on('mouseover', (event: MouseEvent, d: DataPoint) => {
const x = xScale(d.time);
const y = yScale(d.value);
d3.select(event.currentTarget).attr('opacity', 1);
tooltip.style('opacity', 0.9);
tooltip
.style('position', 'absolute')
.html(`
<span class=${styles.areachart__tooltip__text}>${d.value} ${declOfNum(d.value, [
'клик',
'клика',
'кликов',
])}
</span >
<span class=${styles.areachart__tooltip__text}>${dayjs(d.time, { locale: 'ru' }).format('DD MMMM HH:00')}</span>
`)
.style('left', `${event.pageX + 28}px`)
.style('top', `${event.pageY}px`)
.style('opacity', '1');
linesGroup
.selectAll('.perpendicular-line')
.data([d])
.join('line')
.attr('class', 'perpendicular-line')
.attr('x1', x)
.attr('x2', x)
.attr('y1', y)
.attr('y2', chartHeight)
.attr('stroke', mainLineColor)
.attr('stroke-width', 1)
.attr('stroke-dasharray', '8 8');
})
.on('mouseout', () => {
d3.select(event?.currentTarget).attr('opacity', 0);
tooltip.transition().duration(100).style('opacity', 0);
linesGroup.selectAll('.perpendicular-line').remove();
});
}, [mainData, areaChartType, secondaryAreaColor, secondaryLineColor, mainAreaColor, mainLineColor, secondaryData]);
return (
<svg
ref={svgRef}
style={{ height: '230px', width: '100%' }}
>
</svg>
);
}
My data example, i'm using this data on expected result screenshot:
[
{
"time": "2023-11-09T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-10T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-11T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-12T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-13T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-14T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-15T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-16T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-17T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-18T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-19T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-20T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-21T00:00:00.000Z",
"value": 1
},
{
"time": "2023-11-22T00:00:00.000Z",
"value": 1
},
{
"time": "2023-11-23T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-24T00:00:00.000Z",
"value": 0
},
{
"time": "2023-11-25T00:00:00.000Z",
"value": 0
}
]
I'm tried to change xMin and xMax values and use it in domain, also i tried to use extent function but every time i have the same result.
The solution is:
const xScale = d3.scaleTime()
.domain(d3.extent([...mainData, ...(secondaryData || [])], xValue) as [Date, Date])
.range([0, chartWidth])
const yScale = d3.scaleLinear()
.domain([0, yMax])
.range([chartHeight, 0])
const ticksAmount = 8;
const tickStep = (maxValue - minValue) / (ticksAmount);
const step = Math.ceil(tickStep / 5) * 5;
const xAxis =
d3.axisBottom(xScale)
.ticks(ticksAmount)
.tickValues(d3.range(minValue, maxValue + step, step))
.tickFormat(d3.timeFormat(timeFormat))
.tickSizeInner(-chartWidth)
.tickSizeOuter(0)
.tickPadding(5);