From 336fe6e0d470e307bbbfee838187628038aa868b Mon Sep 17 00:00:00 2001 From: goodmice Date: Thu, 6 Oct 2022 14:36:26 +0500 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=B0=20?= =?UTF-8?q?D3HorizontalPercentChart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/d3/D3HorizontalChart.tsx | 149 ------------------ .../d3/D3HorizontalPercentChart.tsx | 95 +++++++++++ 2 files changed, 95 insertions(+), 149 deletions(-) delete mode 100644 src/components/d3/D3HorizontalChart.tsx create mode 100644 src/components/d3/D3HorizontalPercentChart.tsx diff --git a/src/components/d3/D3HorizontalChart.tsx b/src/components/d3/D3HorizontalChart.tsx deleted file mode 100644 index 421108b..0000000 --- a/src/components/d3/D3HorizontalChart.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React, { memo, useEffect } from 'react' -import * as d3 from 'd3' - -import LoaderPortal from '@components/LoaderPortal' -import { useElementSize } from 'usehooks-ts' - - -import '@styles/d3.less' - -type DataType = { - name: string - percent: number -} - -type D3HorizontalChartProps = { - width?: string - height?: string - data: DataType[] - colors?: string[] -} - -const D3HorizontalChart = memo(( - { - width: givenWidth = '100%', - height: givenHeight = '100%', - data, - colors - }: D3HorizontalChartProps) => { - - const [rootRef, { width, height }] = useElementSize() - - const margin = { top: 50, right: 100, bottom: 50, left: 100 } - - useEffect(() => { - if (width < 100 || height < 100) return - const _width = width - margin.left - margin.right - const _height = height - margin.top - margin.bottom - - const svg = d3.select('#d3-horizontal-chart') - .attr('width', '100%') - .attr('height', '100%') - .append('g') - .attr('transform', `translate(${margin.left},${margin.top})`) - - const percents = ['percents'] - const names = data.map(d => d.name) - - const stackedData = d3.stack() - //@ts-ignore - .keys(percents)(data) - - const xMax = 100 - - // scales - - const x = d3.scaleLinear() - .domain([0, xMax]) - .range([0, _width]) - - const y = d3.scaleBand() - .domain(names) - .range([0, _height]) - .padding(0.25) - - // axes - - const xAxisTop = d3.axisTop(x) - .tickValues([0, 25, 50, 75, 100]) - .tickFormat(d => d + '%') - - const xAxisBottom = d3.axisBottom(x) - .tickValues([0, 25, 50, 75, 100]) - .tickFormat(d => d + '%') - - const yAxisLeft = d3.axisLeft(y) - - const gridlines = d3.axisBottom(x) - .tickValues([0, 25, 50, 75, 100]) - .tickFormat(d => '') - .tickSize(_height) - - const yAxisRight = d3.axisRight(y) - .ticks(0) - .tickValues([]) - .tickFormat(d => '') - - svg.append('g') - .attr('transform', `translate(0,0)`) - .attr("class", "grid-line") - .call(g => g.select('.domain').remove()) - .call(gridlines) - - svg.append('g') - .attr('transform', `translate(0,0)`) - .call(xAxisTop) - - svg.append("g") - .call(yAxisLeft) - - svg.append('g') - .attr('transform', `translate(0,${_height})`) - .call(xAxisBottom) - - svg.append('g') - .attr('transform', `translate(${_width},0)`) - .call(yAxisRight) - - const layers = svg.append('g') - .selectAll('g') - .data(stackedData) - .join('g') - - // transition for bars - const duration = 1000 - const t = d3.transition() - .duration(duration) - .ease(d3.easeLinear) - - layers.each(function() { - d3.select(this) - .selectAll('rect') - //@ts-ignore - .data(d => d) - .join('rect') - .attr('fill', (d, i) => colors ? colors[i] : 'black') - //@ts-ignore - .attr('y', d => y(d.data.name)) - .attr('height', y.bandwidth()) - //@ts-ignore - .transition(t) - //@ts-ignore - .attr('width', d => x(d.data.percent)) - }) - - return () => { - svg.selectAll("g").selectAll("*").remove() - } - }, [width, height, data]) - - return ( - -
- -
-
- ) -}) - -export default D3HorizontalChart \ No newline at end of file diff --git a/src/components/d3/D3HorizontalPercentChart.tsx b/src/components/d3/D3HorizontalPercentChart.tsx new file mode 100644 index 0000000..631edf0 --- /dev/null +++ b/src/components/d3/D3HorizontalPercentChart.tsx @@ -0,0 +1,95 @@ +import { memo, useEffect, useMemo, useRef } from 'react' +import { useElementSize } from 'usehooks-ts' +import { Property } from 'csstype' +import * as d3 from 'd3' + +import LoaderPortal from '@components/LoaderPortal' +import { ChartOffset } from './types' + +import '@styles/d3.less' +import { usePartialProps } from '@asb/utils' + +export type PercentChartDataType = { + name: string + percent: number + color?: Property.Color +} + +export type D3HorizontalChartProps = { + width?: Property.Width + height?: Property.Height + data: PercentChartDataType[] + offset?: Partial +} + +const defaultOffset = { top: 50, right: 100, bottom: 50, left: 100 } + +export const D3HorizontalPercentChart = memo(({ + width: givenWidth = '100%', + height: givenHeight = '100%', + offset: givenOffset, + data, +}) => { + const offset = usePartialProps(givenOffset, defaultOffset) + + const [divRef, { width, height }] = useElementSize() + const rootRef = useRef(null) + + const root = useMemo(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current]) + + const inlineWidth = useMemo(() => width - offset.left - offset.right, [width]) + const inlineHeight = useMemo(() => height - offset.top - offset.bottom, [height]) + + const xScale = useMemo(() => d3.scaleLinear().domain([0, 100]).range([0, inlineWidth]), [inlineWidth]) + const yScale = useMemo(() => d3.scaleBand().domain(data.map((d) => d.name)).range([0, inlineHeight]).padding(0.25), [data, inlineHeight]) + + useEffect(() => { /// Отрисовываем оси X сверху и снизу + if (width < 100 || height < 100 || !root) return + const xAxisTop = d3.axisTop(xScale).tickFormat((d) => `${d}%`).ticks(4).tickSize(-inlineHeight) + const xAxisBottom = d3.axisBottom(xScale).tickFormat((d) => `${d}%`).ticks(4) + + root.selectChild('.axis.x.bottom').call(xAxisBottom) + root.selectChild('.axis.x.top').call(xAxisTop) + .selectAll('.tick') + .attr('class', 'tick grid-line') + }, [root, width, height, xScale, inlineHeight]) + + useEffect(() => { /// Отрисовываем ось Y слева + if (width < 100 || height < 100 || !root) return + root.selectChild('.axis.y.left').call(d3.axisLeft(yScale)) + }, [root, width, height, yScale]) + + useEffect(() => { + if (width < 100 || height < 100 || !root) return + + const delay = d3.transition().duration(500).ease(d3.easeLinear) + + const rects = root.selectChild('.data').selectAll('rect').data(data) + rects.enter().append('rect') + rects.exit().remove() + root.selectChild('.data') + .selectAll('rect') + .attr('fill', (d) => d.color || 'black') + .attr('y', (d) => yScale(d.name) ?? null) + .attr('height', yScale.bandwidth()) + .transition(delay) + .attr('width', (d) => xScale(d.percent)) + }, [data, width, height, root, yScale, xScale]) + + return ( + +
+ + + + + + + + +
+
+ ) +}) + +export default D3HorizontalPercentChart