import React, { useRef, useState } from 'react'; export interface IDonutChartData { label: string; value: number; color: string; } interface ChartSegmentProps { label: string; value: number; array: string; offset: string; color: string; } export interface IDonutChart { className?: string; style?: React.CSSProperties; data: IDonutChartData[]; width?: number; height?: number; segmentOffset?: number; strokeWidth?: number; useTooltip?: boolean; onSegmentClick?(e: React.MouseEvent, segment: IDonutChartData): void; [x: string]: any; } export function DonutChart(props: IDonutChart) { const { className, data, width = 300, height = 300, segmentOffset = 0, strokeWidth = 15, useTooltip = true, onSegmentClick, ...otherProps } = props; const refTooltip = useRef(null); const refContainer = useRef(null); function classNames(className: string): string { let classList = 'b-donut-chart'; if (className) { classList += ` ${className}`; } return classList; } function getTotalCount(chartData: IDonutChartData[]): number { return chartData.reduce((prev, cur) => prev + cur.value, 0); } function getPercent(count: number, total: number) { return (100 * count) / total; } function getChartData(donutChartData: IDonutChartData[]): ChartSegmentProps[] { const firstSegmentOffset = segmentOffset; let total = getTotalCount(donutChartData); let preSegmentsTotalLength = getPercent(donutChartData[0].value, total); const chartSegments: ChartSegmentProps[] = []; donutChartData.forEach((d) => { let segmentValues = { label: d.label, value: d.value, array: '0 0', offset: '0 0', color: d.color, }; let percent = getPercent(d.value, total); let strokeDasharray = `${percent} ${100 - percent}`; segmentValues.array = strokeDasharray; if (percent != 0) { segmentValues.offset = `${100 - preSegmentsTotalLength + firstSegmentOffset}`; preSegmentsTotalLength += percent; } chartSegments.push(segmentValues); }); return chartSegments; } function getRelativeCoordinates(event: React.MouseEvent, referenceElement: React.RefObject) { const position = { x: event.pageX, y: event.pageY, }; const offset = { left: referenceElement.current.offsetLeft, top: referenceElement.current.offsetTop, }; let reference = referenceElement.current.offsetParent as any; while (reference) { offset.left += reference.offsetLeft; offset.top += reference.offsetTop; reference = reference.offsetParent; } return { x: position.x - offset.left, y: position.y - offset.top, }; } function handleSegmentClick(e: React.MouseEvent, segment: ChartSegmentProps, useTooltip: boolean) { onSegmentClick && onSegmentClick(e, { label: segment.label, value: segment.value, color: segment.color, }); useTooltip && showTooltip(e, segment); } function handleMouseMove(e: React.MouseEvent, segment: ChartSegmentProps) { useTooltip && showTooltip(e, segment); } function handleMouseOut() { useTooltip && hideTooltip(); } function showTooltip(e: React.MouseEvent, segment: ChartSegmentProps) { refTooltip.current.innerHTML = `${segment.label}: ${segment.value}`; refTooltip.current.classList.add('is-visible'); const coordinates = getRelativeCoordinates(e, refContainer); refTooltip.current.style.left = coordinates.x + 10 + 'px'; refTooltip.current.style.top = coordinates.y + 10 + 'px'; } function hideTooltip() { refTooltip.current.classList.remove('is-visible'); } if (data.length > 0) { return (
{getChartData(data).map((s, i) => ( handleSegmentClick(e, s, useTooltip)} onMouseMove={(e) => handleMouseMove(e, s)} onMouseOut={() => handleMouseOut()}> ))}
); } else { return null; } }