forked from ddrilling/asb_cloud_front
Исправлена работа компонента D3HorizontalPercentChart
This commit is contained in:
parent
2aca41da83
commit
336fe6e0d4
@ -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 (
|
|
||||||
<LoaderPortal show={false} style={{width: givenWidth, height: givenHeight}}>
|
|
||||||
<div ref={rootRef} style={{width: '100%', height: '100%'}}>
|
|
||||||
<svg id={'d3-horizontal-chart'}></svg>
|
|
||||||
</div>
|
|
||||||
</LoaderPortal>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default D3HorizontalChart
|
|
95
src/components/d3/D3HorizontalPercentChart.tsx
Normal file
95
src/components/d3/D3HorizontalPercentChart.tsx
Normal file
@ -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<ChartOffset>
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOffset = { top: 50, right: 100, bottom: 50, left: 100 }
|
||||||
|
|
||||||
|
export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
|
||||||
|
width: givenWidth = '100%',
|
||||||
|
height: givenHeight = '100%',
|
||||||
|
offset: givenOffset,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset)
|
||||||
|
|
||||||
|
const [divRef, { width, height }] = useElementSize()
|
||||||
|
const rootRef = useRef<SVGGElement | null>(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<SVGGElement>('.axis.x.bottom').call(xAxisBottom)
|
||||||
|
root.selectChild<SVGGElement>('.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<SVGGElement>('.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<SVGGElement>('.data')
|
||||||
|
.selectAll<SVGRectElement, PercentChartDataType>('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 (
|
||||||
|
<LoaderPortal show={false} style={{ width: givenWidth, height: givenHeight }}>
|
||||||
|
<div ref={divRef} style={{ width: '100%', height: '100%' }}>
|
||||||
|
<svg id={'d3-horizontal-chart'} width={'100%'} height={'100%'}>
|
||||||
|
<g ref={rootRef} transform={`translate(${offset.left}, ${offset.top})`}>
|
||||||
|
<g className={'axis x top'}></g>
|
||||||
|
<g className={'axis x bottom'} transform={`translate(0, ${inlineHeight})`}></g>
|
||||||
|
<g className={'data'}></g>
|
||||||
|
<g className={'axis y left'}></g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</LoaderPortal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default D3HorizontalPercentChart
|
Loading…
Reference in New Issue
Block a user