|
|
@@ -0,0 +1,308 @@
|
|
|
+import React, { Component } from 'react';
|
|
|
+import { Chart, Tooltip, Geom, Coord } from 'bizcharts';
|
|
|
+import { DataView } from '@antv/data-set';
|
|
|
+import { Divider } from 'antd';
|
|
|
+import classNames from 'classnames';
|
|
|
+import ReactFitText from 'react-fittext';
|
|
|
+import Debounce from 'lodash-decorators/debounce';
|
|
|
+import Bind from 'lodash-decorators/bind';
|
|
|
+import autoHeight from '../autoHeight';
|
|
|
+
|
|
|
+import styles from './index.less';
|
|
|
+export interface IPieProps {
|
|
|
+ animate?: boolean;
|
|
|
+ color?: string;
|
|
|
+ colors?: string[];
|
|
|
+ selected?: boolean;
|
|
|
+ height?: number;
|
|
|
+ margin?: [number, number, number, number];
|
|
|
+ hasLegend?: boolean;
|
|
|
+ padding?: [number, number, number, number];
|
|
|
+ percent?: number;
|
|
|
+ data?: Array<{
|
|
|
+ x: string | string;
|
|
|
+ y: number;
|
|
|
+ }>;
|
|
|
+ inner?: number;
|
|
|
+ lineWidth?: number;
|
|
|
+ forceFit?: boolean;
|
|
|
+ style?: React.CSSProperties;
|
|
|
+ className?: string;
|
|
|
+ total?: React.ReactNode | number | (() => React.ReactNode | number);
|
|
|
+ title?: React.ReactNode;
|
|
|
+ tooltip?: boolean;
|
|
|
+ valueFormat?: (value: string) => string | React.ReactNode;
|
|
|
+ subTitle?: React.ReactNode;
|
|
|
+}
|
|
|
+interface IPieState {
|
|
|
+ legendData: Array<{ checked: boolean; x: string; color: string; percent: number; y: string }>;
|
|
|
+ legendBlock: boolean;
|
|
|
+}
|
|
|
+class Pie extends Component<IPieProps, IPieState> {
|
|
|
+ state: IPieState = {
|
|
|
+ legendData: [],
|
|
|
+ legendBlock: false,
|
|
|
+ };
|
|
|
+
|
|
|
+ requestRef: number | undefined;
|
|
|
+ root!: HTMLDivElement;
|
|
|
+ chart: G2.Chart | undefined;
|
|
|
+
|
|
|
+ componentDidMount() {
|
|
|
+ window.addEventListener(
|
|
|
+ 'resize',
|
|
|
+ () => {
|
|
|
+ this.requestRef = requestAnimationFrame(() => this.resize());
|
|
|
+ },
|
|
|
+ { passive: true },
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ componentDidUpdate(preProps: IPieProps) {
|
|
|
+ const { data } = this.props;
|
|
|
+ if (data !== preProps.data) {
|
|
|
+ // because of charts data create when rendered
|
|
|
+ // so there is a trick for get rendered time
|
|
|
+ this.getLegendData();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ componentWillUnmount() {
|
|
|
+ if (this.requestRef) {
|
|
|
+ window.cancelAnimationFrame(this.requestRef);
|
|
|
+ }
|
|
|
+ window.removeEventListener('resize', this.resize);
|
|
|
+ if (this.resize) {
|
|
|
+ (this.resize as any).cancel();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getG2Instance = (chart: G2.Chart) => {
|
|
|
+ this.chart = chart;
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ this.getLegendData();
|
|
|
+ this.resize();
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // for custom lengend view
|
|
|
+ getLegendData = () => {
|
|
|
+ if (!this.chart) return;
|
|
|
+ const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
|
|
|
+ if (!geom) return;
|
|
|
+ const items = geom.get('dataArray') || []; // 获取图形对应的
|
|
|
+
|
|
|
+ const legendData = items.map((item: { color: any; _origin: any }[]) => {
|
|
|
+ /* eslint no-underscore-dangle:0 */
|
|
|
+ const origin = item[0]._origin;
|
|
|
+ origin.color = item[0].color;
|
|
|
+ origin.checked = true;
|
|
|
+ return origin;
|
|
|
+ });
|
|
|
+
|
|
|
+ this.setState({
|
|
|
+ legendData,
|
|
|
+ });
|
|
|
+ };
|
|
|
+ handleRoot = (n: HTMLDivElement) => {
|
|
|
+ this.root = n;
|
|
|
+ };
|
|
|
+
|
|
|
+ handleLegendClick = (item: any, i: string | number) => {
|
|
|
+ const newItem = item;
|
|
|
+ newItem.checked = !newItem.checked;
|
|
|
+
|
|
|
+ const { legendData } = this.state;
|
|
|
+ legendData[i] = newItem;
|
|
|
+
|
|
|
+ const filteredLegendData = legendData.filter(l => l.checked).map(l => l.x);
|
|
|
+
|
|
|
+ if (this.chart) {
|
|
|
+ this.chart.filter('x', val => filteredLegendData.indexOf(val + '') > -1);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.setState({
|
|
|
+ legendData,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // for window resize auto responsive legend
|
|
|
+ @Bind()
|
|
|
+ @Debounce(300)
|
|
|
+ resize() {
|
|
|
+ const { hasLegend } = this.props;
|
|
|
+ const { legendBlock } = this.state;
|
|
|
+ if (!hasLegend || !this.root) {
|
|
|
+ window.removeEventListener('resize', this.resize);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ this.root &&
|
|
|
+ this.root.parentNode &&
|
|
|
+ (this.root.parentNode as HTMLElement).clientWidth <= 380
|
|
|
+ ) {
|
|
|
+ if (!legendBlock) {
|
|
|
+ this.setState({
|
|
|
+ legendBlock: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else if (legendBlock) {
|
|
|
+ this.setState({
|
|
|
+ legendBlock: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ render() {
|
|
|
+ const {
|
|
|
+ valueFormat,
|
|
|
+ subTitle,
|
|
|
+ total,
|
|
|
+ hasLegend = false,
|
|
|
+ className,
|
|
|
+ style,
|
|
|
+ height = 0,
|
|
|
+ forceFit = true,
|
|
|
+ percent,
|
|
|
+ color,
|
|
|
+ inner = 0.75,
|
|
|
+ animate = true,
|
|
|
+ colors,
|
|
|
+ lineWidth = 1,
|
|
|
+ } = this.props;
|
|
|
+
|
|
|
+ const { legendData, legendBlock } = this.state;
|
|
|
+ const pieClassName = classNames(styles.pie, className, {
|
|
|
+ [styles.hasLegend]: !!hasLegend,
|
|
|
+ [styles.legendBlock]: legendBlock,
|
|
|
+ });
|
|
|
+
|
|
|
+ const {
|
|
|
+ data: propsData,
|
|
|
+ selected: propsSelected = true,
|
|
|
+ tooltip: propsTooltip = true,
|
|
|
+ } = this.props;
|
|
|
+
|
|
|
+ let data = propsData || [];
|
|
|
+ let selected = propsSelected;
|
|
|
+ let tooltip = propsTooltip;
|
|
|
+
|
|
|
+ const defaultColors = colors;
|
|
|
+ data = data || [];
|
|
|
+ selected = selected || true;
|
|
|
+ tooltip = tooltip || true;
|
|
|
+ let formatColor;
|
|
|
+
|
|
|
+ const scale = {
|
|
|
+ x: {
|
|
|
+ type: 'cat',
|
|
|
+ range: [0, 1],
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ min: 0,
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ if (percent || percent === 0) {
|
|
|
+ selected = false;
|
|
|
+ tooltip = false;
|
|
|
+ formatColor = (value: string) => {
|
|
|
+ if (value === '占比') {
|
|
|
+ return color || 'rgba(24, 144, 255, 0.85)';
|
|
|
+ }
|
|
|
+ return '#F0F2F5';
|
|
|
+ };
|
|
|
+
|
|
|
+ data = [
|
|
|
+ {
|
|
|
+ x: '占比',
|
|
|
+ y: parseFloat(percent + ''),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ x: '反比',
|
|
|
+ y: 100 - parseFloat(percent + ''),
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ const tooltipFormat: [string, (...args: any[]) => { name?: string; value: string }] = [
|
|
|
+ 'x*percent',
|
|
|
+ (x: string, p: number) => ({
|
|
|
+ name: x,
|
|
|
+ value: `${(p * 100).toFixed(2)}%`,
|
|
|
+ }),
|
|
|
+ ];
|
|
|
+
|
|
|
+ const padding = [12, 0, 12, 0] as [number, number, number, number];
|
|
|
+
|
|
|
+ const dv = new DataView();
|
|
|
+ dv.source(data).transform({
|
|
|
+ type: 'percent',
|
|
|
+ field: 'y',
|
|
|
+ dimension: 'x',
|
|
|
+ as: 'percent',
|
|
|
+ });
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div ref={this.handleRoot} className={pieClassName} style={style}>
|
|
|
+ <ReactFitText maxFontSize={25}>
|
|
|
+ <div className={styles.chart}>
|
|
|
+ <Chart
|
|
|
+ scale={scale}
|
|
|
+ height={height}
|
|
|
+ forceFit={forceFit}
|
|
|
+ data={dv}
|
|
|
+ padding={padding}
|
|
|
+ animate={animate}
|
|
|
+ onGetG2Instance={this.getG2Instance}
|
|
|
+ >
|
|
|
+ {!!tooltip && <Tooltip showTitle={false} />}
|
|
|
+ <Coord type="theta" innerRadius={inner} />
|
|
|
+ <Geom
|
|
|
+ style={{ lineWidth, stroke: '#fff' }}
|
|
|
+ tooltip={tooltip ? tooltipFormat : undefined}
|
|
|
+ type="intervalStack"
|
|
|
+ position="percent"
|
|
|
+ color={['x', percent || percent === 0 ? formatColor : defaultColors] as any}
|
|
|
+ selected={selected}
|
|
|
+ />
|
|
|
+ </Chart>
|
|
|
+
|
|
|
+ {(subTitle || total) && (
|
|
|
+ <div className={styles.total}>
|
|
|
+ {subTitle && <h4 className="pie-sub-title">{subTitle}</h4>}
|
|
|
+ {/* eslint-disable-next-line */}
|
|
|
+ {total && (
|
|
|
+ <div className="pie-stat">{typeof total === 'function' ? total() : total}</div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </ReactFitText>
|
|
|
+
|
|
|
+ {hasLegend && (
|
|
|
+ <ul className={styles.legend}>
|
|
|
+ {legendData.map((item, i) => (
|
|
|
+ <li key={item.x} onClick={() => this.handleLegendClick(item, i)}>
|
|
|
+ <span
|
|
|
+ className={styles.dot}
|
|
|
+ style={{
|
|
|
+ backgroundColor: !item.checked ? '#aaa' : item.color,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <span className={styles.legendTitle}>{item.x}</span>
|
|
|
+ <Divider type="vertical" />
|
|
|
+ <span className={styles.percent}>
|
|
|
+ {`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}
|
|
|
+ </span>
|
|
|
+ <span className={styles.value}>{valueFormat ? valueFormat(item.y) : item.y}</span>
|
|
|
+ </li>
|
|
|
+ ))}
|
|
|
+ </ul>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default autoHeight()(Pie);
|