My friends! I have the AreaChart React component. If i'm using data which date starts with time u can divide by three it's ok (like 09:00, 12:00 etc.), but if date starts with time u can't divide by 3 (like 10:00, 14:00 etc.), domain start to shift to the right or left sides.
Correct data:
const correctDataBy3 = [
{ time: new Date(2023, 10, 25, 18, 0, 0, 0), value: 230 },
{ time: new Date(2023, 10, 25, 19, 0, 0, 0), value: 10 },
{ time: new Date(2023, 10, 25, 20, 0, 0, 0), value: 341 },
{ time: new Date(2023, 10, 25, 21, 0, 0, 0), value: 23 },
{ time: new Date(2023, 10, 25, 22, 0, 0, 0), value: 10 },
{ time: new Date(2023, 10, 25, 23, 0, 0, 0), value: 45 },
{ time: new Date(2023, 10, 26, 0, 0, 0, 0), value: 24 },
{ time: new Date(2023, 10, 26, 1, 0, 0, 0), value: 450 },
{ time: new Date(2023, 10, 26, 2, 0, 0, 0), value: 23 },
{ time: new Date(2023, 10, 26, 3, 0, 0, 0), value: 24 },
{ time: new Date(2023, 10, 26, 4, 0, 0, 0), value: 15 },
{ time: new Date(2023, 10, 26, 5, 0, 0, 0), value: 24 },
{ time: new Date(2023, 10, 26, 6, 0, 0, 0), value: 45 },
{ time: new Date(2023, 10, 26, 7, 0, 0, 0), value: 83 },
{ time: new Date(2023, 10, 26, 8, 0, 0, 0), value: 24 },
{ time: new Date(2023, 10, 26, 9, 0, 0, 0), value: 30 },
{ time: new Date(2023, 10, 26, 10, 0, 0, 0), value: 180 },
{ time: new Date(2023, 10, 26, 11, 0, 0, 0), value: 105 },
{ time: new Date(2023, 10, 26, 12, 0, 0, 0), value: 45 },
{ time: new Date(2023, 10, 26, 13, 0, 0, 0), value: 230 },
{ time: new Date(2023, 10, 26, 14, 0, 0, 0), value: 15 },
{ time: new Date(2023, 10, 26, 15, 0, 0, 0), value: 10 },
{ time: new Date(2023, 10, 26, 16, 0, 0, 0), value: 0 },
{ time: new Date(2023, 10, 26, 17, 0, 0, 0), value: 0 },
{ time: new Date(2023, 10, 26, 18, 0, 0, 0), value: 0 }
];
Incorrect data:
const incorrectDataNotBy3 = [
{ time: new Date(2023, 10, 25, 19, 0, 0, 0), value: 230 },
{ time: new Date(2023, 10, 25, 20, 0, 0, 0), value: 10 },
{ time: new Date(2023, 10, 25, 21, 0, 0, 0), value: 341 },
{ time: new Date(2023, 10, 25, 22, 0, 0, 0), value: 23 },
{ time: new Date(2023, 10, 25, 23, 0, 0, 0), value: 10 },
{ time: new Date(2023, 10, 26, 0, 0, 0, 0), value: 45 },
{ time: new Date(2023, 10, 26, 1, 0, 0, 0), value: 24 },
{ time: new Date(2023, 10, 26, 2, 0, 0, 0), value: 450 },
{ time: new Date(2023, 10, 26, 3, 0, 0, 0), value: 23 },
{ time: new Date(2023, 10, 26, 4, 0, 0, 0), value: 24 },
{ time: new Date(2023, 10, 26, 5, 0, 0, 0), value: 15 },
{ time: new Date(2023, 10, 26, 6, 0, 0, 0), value: 24 },
{ time: new Date(2023, 10, 26, 7, 0, 0, 0), value: 45 },
{ time: new Date(2023, 10, 26, 8, 0, 0, 0), value: 83 },
{ time: new Date(2023, 10, 26, 9, 0, 0, 0), value: 24 },
{ time: new Date(2023, 10, 26, 10, 0, 0, 0), value: 30 },
{ time: new Date(2023, 10, 26, 11, 0, 0, 0), value: 180 },
{ time: new Date(2023, 10, 26, 12, 0, 0, 0), value: 105 },
{ time: new Date(2023, 10, 26, 13, 0, 0, 0), value: 45 },
{ time: new Date(2023, 10, 26, 14, 0, 0, 0), value: 230 },
{ time: new Date(2023, 10, 26, 15, 0, 0, 0), value: 15 },
{ time: new Date(2023, 10, 26, 16, 0, 0, 0), value: 10 },
{ time: new Date(2023, 10, 26, 17, 0, 0, 0), value: 0 },
{ time: new Date(2023, 10, 26, 18, 0, 0, 0), value: 0 },
{ time: new Date(2023, 10, 26, 10, 0, 0, 0), value: 0 },
];
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 - margin.right;
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 xScale = d3
.scaleUtc()
.domain([
d3.min([...mainData, ...(secondaryData || [])], (d) => new Date(d.time) as Date),
d3.max([...mainData, ...(secondaryData || [])], (d) => new Date(d.time) as Date)
])
.range([0, chartWidth]);
const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat(timeFormat))
const yMax = d3.max([...mainData, ...(secondaryData || [])], (d) => d.value) <= 5 ? 5 : d3.max([...mainData, ...(secondaryData || [])], (d) => d.value);
const yScale = d3.scaleLinear().domain([0, yMax]).nice().range([chartHeight, 0]);
const area = d3
.area<DataPoint>()
.x((d) => xScale(d.time))
.y0(chartHeight)
.y1((d) => yScale(d.value))
.curve(d3.curveCatmullRom.alpha(0.8));
const line = d3
.line<DataPoint>()
.x((d) => xScale(d.time))
.y((d) => yScale(d.value))
.curve(d3.curveCatmullRom.alpha(0.8));
const xTicks = xScale.ticks()
svg
.selectAll('.vertical-line')
.data(xTicks)
.enter()
.append('line')
.attr('class', 'vertical-line')
.attr('x1', (d) => xScale(new Date(d)))
.attr('x2', (d) => xScale(new Date(d)))
.attr('y1', chartHeight + margin.top * 2)
.attr('y2', 0)
.attr('stroke', '#E2E2E2')
.attr('stroke-dasharray', '0')
.attr('transform', `translate(${margin.left}, ${-margin.top})`);
svg
.append('line')
.attr('class', 'first-line')
.attr('x1', xTicks[xTicks.length - 1])
.attr('x2', xTicks[xTicks.length - 1])
.attr('y1', chartHeight + margin.top * 2)
.attr('y2', 0)
.attr('stroke', '#E2E2E2')
.attr('stroke-dasharray', '0')
.attr('transform', `translate(${margin.left}, ${-margin.top})`);
svg
.append('line')
.attr('class', 'last-line')
.attr('x1', xTicks[xTicks.length + 1])
.attr('x2', xTicks[xTicks.length + 1])
.attr('y1', chartHeight + margin.top * 2)
.attr('y2', 0)
.attr('stroke', '#E2E2E2')
.attr('stroke-dasharray', '0')
.attr('transform', `translate(${chartWidth + margin.right + 5}, ${-margin.top})`);
const yTicks = yScale.ticks();
svg
.selectAll('.horizontal-line')
.data(yTicks)
.enter()
.append('line')
.attr('class', 'horizontal-line')
.attr('x1', 0)
.attr('x2', chartWidth)
.attr('y1', (d) => yScale(d))
.attr('y2', (d) => yScale(d))
.attr('stroke', '#E2E2E2')
.attr('stroke-dasharray', '0')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
svg
.selectAll('.last-horizontal-line')
.data(yTicks)
.enter()
.append('line')
.attr('class', 'horizontal-line')
.attr('x1', 0)
.attr('x2', chartWidth)
.attr('y1', yTicks[yTicks.length - 1])
.attr('y2', yTicks[yTicks.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);
if (xTicks.length > 8) {
chart.selectAll('.tick:first-of-type text').remove();
chart.selectAll('.tick:last-of-type text').remove();
}
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(d3.axisLeft(yScale))
.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) => xScale(d.time))
.attr('cy', (d) => 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, d) => {
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>
);
}
AreaChart with incorrect data screenshot:
AreaChart with correct data screenshot:
Thanks a lot to everyone!
I'm tried to change xScale.domain and xScale.range but got no results.
Solution! It was a problem with domain.
I replaced this part of code:
const xScale = d3
.scaleUtc()
.domain([
d3.min([...mainData, ...(secondaryData || [])], (d) => new Date(d.time) as Date),
d3.max([...mainData, ...(secondaryData || [])], (d) => new Date(d.time) as Date)
])
.range([0, chartWidth]);
with:
const xValue = (d: DataPoint) => d.time;
const xScale = d3.scaleTime()
.domain(d3.extent([...mainData, ...(secondaryData || [])], xValue) as [Date, Date])
.range([0, chartWidth])
.nice();
It's work for me as i expected.