This is an automated email from the ASF dual-hosted git repository. hanahmily pushed a commit to branch feature/5.0.0 in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git
commit 381f40bfb9499e32b0aaa0d3e64cdf71df78c148 Author: gaohongtao <[email protected]> AuthorDate: Mon Dec 18 16:01:43 2017 +0800 Finished dashboard demo --- src/main/frontend/public/alert.svg | 1 + src/main/frontend/public/app.svg | 1 + src/main/frontend/public/database.svg | 1 + src/main/frontend/public/redis.svg | 1 + src/main/frontend/public/service.svg | 1 + .../frontend/src/components/Charts/Bar/index.d.ts | 14 ++ .../frontend/src/components/Charts/Bar/index.js | 151 ++++++++++++ .../src/components/Charts/ChartCard/index.d.ts | 11 + .../src/components/Charts/ChartCard/index.js | 60 +++++ .../src/components/Charts/ChartCard/index.less | 74 ++++++ .../src/components/Charts/Field/index.d.ts | 7 + .../frontend/src/components/Charts/Field/index.js | 12 + .../src/components/Charts/Field/index.less | 16 ++ .../src/components/Charts/Gauge/index.d.ts | 10 + .../frontend/src/components/Charts/Gauge/index.js | 202 ++++++++++++++++ .../src/components/Charts/MiniArea/index.d.ts | 29 +++ .../src/components/Charts/MiniArea/index.js | 125 ++++++++++ .../src/components/Charts/MiniBar/index.d.ts | 11 + .../src/components/Charts/MiniBar/index.js | 87 +++++++ .../src/components/Charts/MiniProgress/index.d.ts | 12 + .../src/components/Charts/MiniProgress/index.js | 30 +++ .../src/components/Charts/MiniProgress/index.less | 35 +++ .../frontend/src/components/Charts/Pie/index.d.ts | 20 ++ .../frontend/src/components/Charts/Pie/index.js | 260 +++++++++++++++++++++ .../frontend/src/components/Charts/Pie/index.less | 94 ++++++++ .../src/components/Charts/Radar/index.d.ts | 14 ++ .../frontend/src/components/Charts/Radar/index.js | 189 +++++++++++++++ .../src/components/Charts/Radar/index.less | 46 ++++ .../src/components/Charts/TagCloud/index.d.ts | 10 + .../src/components/Charts/TagCloud/index.js | 170 ++++++++++++++ .../src/components/Charts/TagCloud/index.less | 6 + .../src/components/Charts/TimelineChart/index.d.ts | 15 ++ .../src/components/Charts/TimelineChart/index.js | 125 ++++++++++ .../src/components/Charts/TimelineChart/index.less | 3 + .../src/components/Charts/WaterWave/index.d.ts | 9 + .../src/components/Charts/WaterWave/index.js | 200 ++++++++++++++++ .../src/components/Charts/WaterWave/index.less | 28 +++ .../frontend/src/components/Charts/demo/bar.md | 26 +++ .../src/components/Charts/demo/chart-card.md | 65 ++++++ .../frontend/src/components/Charts/demo/gauge.md | 18 ++ .../src/components/Charts/demo/mini-area.md | 28 +++ .../src/components/Charts/demo/mini-bar.md | 28 +++ .../src/components/Charts/demo/mini-pie.md | 16 ++ .../src/components/Charts/demo/mini-progress.md | 12 + .../frontend/src/components/Charts/demo/mix.md | 83 +++++++ .../frontend/src/components/Charts/demo/pie.md | 47 ++++ .../frontend/src/components/Charts/demo/radar.md | 64 +++++ .../src/components/Charts/demo/tag-cloud.md | 25 ++ .../src/components/Charts/demo/timeline-chart.md | 27 +++ .../src/components/Charts/demo/waterwave.md | 20 ++ src/main/frontend/src/components/Charts/equal.js | 17 ++ src/main/frontend/src/components/Charts/index.d.ts | 17 ++ src/main/frontend/src/components/Charts/index.js | 31 +++ src/main/frontend/src/components/Charts/index.less | 19 ++ src/main/frontend/src/components/Charts/index.md | 132 +++++++++++ .../frontend/src/components/Trend/demo/basic.md | 17 ++ src/main/frontend/src/components/Trend/index.d.ts | 8 + src/main/frontend/src/components/Trend/index.js | 22 ++ src/main/frontend/src/components/Trend/index.less | 30 +++ src/main/frontend/src/components/Trend/index.md | 21 ++ src/main/frontend/src/layouts/BasicLayout.js | 3 - .../frontend/src/routes/Dashboard/Dashboard.js | 235 ++++++++++++++++++- .../frontend/src/routes/Dashboard/Dashboard.less | 5 + 63 files changed, 3092 insertions(+), 4 deletions(-) diff --git a/src/main/frontend/public/alert.svg b/src/main/frontend/public/alert.svg new file mode 100644 index 0000000..fc9270b --- /dev/null +++ b/src/main/frontend/public/alert.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M611.702 44.963l263.203 149.544c60.979 35.06 98.35 99.414 98.154 168.892v299.056c0.037 69.658-37.691 134.044-98.987 168.902l-262.37 149.36c-61.333 34.828-136.864 34.828-198.176 0l-263.61-149.55c-61.285-34.859-99.008-99.246-98 [...] \ No newline at end of file diff --git a/src/main/frontend/public/app.svg b/src/main/frontend/public/app.svg new file mode 100644 index 0000000..4116c21 --- /dev/null +++ b/src/main/frontend/public/app.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M357.885 49.289h285.276c173.87 0 316.906 143.086 316.906 317.854v285.276c0 173.831-143.086 316.925-316.906 316.925h-285.276c-174.749 0-317.854-143.086-317.854-316.925v-285.276c0-174.749 143.086-317.854 317.854-317.854z" fill= [...] \ No newline at end of file diff --git a/src/main/frontend/public/database.svg b/src/main/frontend/public/database.svg new file mode 100644 index 0000000..7fc1526 --- /dev/null +++ b/src/main/frontend/public/database.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M896.483256 191.467753v64.081481c0 70.584592-172.214247 128.160915-384.484791 128.160915-212.267475 0-384.483768-57.577347-384.483768-128.160915v-64.081481c0-70.583568 172.216293-128.160915 384.483768-128.160915 212.270545 0 [...] \ No newline at end of file diff --git a/src/main/frontend/public/redis.svg b/src/main/frontend/public/redis.svg new file mode 100644 index 0000000..e634317 --- /dev/null +++ b/src/main/frontend/public/redis.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M583.66976 285.16864l-229.83168 23.296a2.19136 2.19136 0 0 0-1.84832 1.78176 2.14016 2.14016 0 0 0 1.28 2.27328l179.12832 73.07776c0.28672 0.07168 0.49664 0.14336 0.77824 0.14336 0.77312 0 1.4848-0.42496 1.83808-1.13664l50.65 [...] \ No newline at end of file diff --git a/src/main/frontend/public/service.svg b/src/main/frontend/public/service.svg new file mode 100644 index 0000000..8d2daf7 --- /dev/null +++ b/src/main/frontend/public/service.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M151.3048309167421 870.0138201689984c199.20612239587413 199.20612239587422 522.1842157706417 199.20612239587433 721.3924594868594-0.002121320343348998 199.2068295026554-199.20682950265532 199.20682950265544-522.1849228774227 [...] \ No newline at end of file diff --git a/src/main/frontend/src/components/Charts/Bar/index.d.ts b/src/main/frontend/src/components/Charts/Bar/index.d.ts new file mode 100644 index 0000000..fd2d05d --- /dev/null +++ b/src/main/frontend/src/components/Charts/Bar/index.d.ts @@ -0,0 +1,14 @@ +import * as React from "react"; +export interface BarProps { + title: React.ReactNode; + color?: string; + margin?: [number, number, number, number]; + height: number; + data: Array<{ + x: string; + y: number; + }>; + autoLabel?: boolean; +} + +export default class Bar extends React.Component<BarProps, any> {} diff --git a/src/main/frontend/src/components/Charts/Bar/index.js b/src/main/frontend/src/components/Charts/Bar/index.js new file mode 100644 index 0000000..ef834d1 --- /dev/null +++ b/src/main/frontend/src/components/Charts/Bar/index.js @@ -0,0 +1,151 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import Debounce from 'lodash-decorators/debounce'; +import Bind from 'lodash-decorators/bind'; +import equal from '../equal'; +import styles from '../index.less'; + +class Bar extends PureComponent { + state = { + autoHideXLabels: false, + } + + componentDidMount() { + this.renderChart(this.props.data); + + window.addEventListener('resize', this.resize); + } + + componentWillReceiveProps(nextProps) { + if (!equal(this.props, nextProps)) { + this.renderChart(nextProps.data); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.resize); + if (this.chart) { + this.chart.destroy(); + } + this.resize.cancel(); + } + + @Bind() + @Debounce(200) + resize() { + if (!this.node) { + return; + } + const canvasWidth = this.node.parentNode.clientWidth; + const { data = [], autoLabel = true } = this.props; + if (!autoLabel) { + return; + } + const minWidth = data.length * 30; + const { autoHideXLabels } = this.state; + + if (canvasWidth <= minWidth) { + if (!autoHideXLabels) { + this.setState({ + autoHideXLabels: true, + }); + this.renderChart(data); + } + } else if (autoHideXLabels) { + this.setState({ + autoHideXLabels: false, + }); + this.renderChart(data); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { autoHideXLabels } = this.state; + const { + height = 0, + fit = true, + color = 'rgba(24, 144, 255, 0.85)', + margin = [32, 0, (autoHideXLabels ? 8 : 32), 40], + } = this.props; + + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const { Frame } = G2; + const frame = new Frame(data); + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height - 22, + legend: null, + plotCfg: { + margin, + }, + }); + + if (autoHideXLabels) { + chart.axis('x', { + title: false, + tickLine: false, + labels: false, + }); + } else { + chart.axis('x', { + title: false, + }); + } + chart.axis('y', { + title: false, + line: false, + tickLine: false, + }); + + chart.source(frame, { + x: { + type: 'cat', + }, + y: { + min: 0, + }, + }); + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + name: 'x', + }, + }); + chart.interval().position('x*y').color(color).style({ + fillOpacity: 1, + }); + chart.render(); + + this.chart = chart; + } + + render() { + const { height, title } = this.props; + + return ( + <div className={styles.chart} style={{ height }}> + <div> + { title && <h4>{title}</h4>} + <div ref={this.handleRef} /> + </div> + </div> + ); + } +} + +export default Bar; diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.d.ts b/src/main/frontend/src/components/Charts/ChartCard/index.d.ts new file mode 100644 index 0000000..21d2be3 --- /dev/null +++ b/src/main/frontend/src/components/Charts/ChartCard/index.d.ts @@ -0,0 +1,11 @@ +import * as React from "react"; +export interface ChartCardProps { + title: React.ReactNode; + action?: React.ReactNode; + total?: React.ReactNode | number; + footer?: React.ReactNode; + contentHeight?: number; + avatar?: React.ReactNode; +} + +export default class ChartCard extends React.Component<ChartCardProps, any> {} diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.js b/src/main/frontend/src/components/Charts/ChartCard/index.js new file mode 100644 index 0000000..a472682 --- /dev/null +++ b/src/main/frontend/src/components/Charts/ChartCard/index.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { Card, Spin } from 'antd'; +import classNames from 'classnames'; + +import styles from './index.less'; + +const ChartCard = ({ + loading = false, contentHeight, title, avatar, action, total, footer, children, ...rest +}) => { + const content = ( + <div className={styles.chartCard}> + <div + className={classNames(styles.chartTop, { [styles.chartTopMargin]: (!children && !footer) })} + > + <div className={styles.avatar}> + { + avatar + } + </div> + <div className={styles.metaWrap}> + <div className={styles.meta}> + <span className={styles.title}>{title}</span> + <span className={styles.action}>{action}</span> + </div> + { + // eslint-disable-next-line + (total !== undefined) && (<div className={styles.total} dangerouslySetInnerHTML={{ __html: total }} />) + } + </div> + </div> + { + children && ( + <div className={styles.content} style={{ height: contentHeight || 'auto' }}> + <div className={contentHeight && styles.contentFixed}> + {children} + </div> + </div> + ) + } + { + footer && ( + <div className={classNames(styles.footer, { [styles.footerMargin]: !children })}> + {footer} + </div> + ) + } + </div> + ); + + return ( + <Card + bodyStyle={{ padding: '20px 24px 8px 24px' }} + {...rest} + > + {<Spin spinning={loading}>{content}</Spin>} + </Card> + ); +}; + +export default ChartCard; diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.less b/src/main/frontend/src/components/Charts/ChartCard/index.less new file mode 100644 index 0000000..bdc573b --- /dev/null +++ b/src/main/frontend/src/components/Charts/ChartCard/index.less @@ -0,0 +1,74 @@ +@import "~antd/lib/style/themes/default.less"; + +.chartCard { + position: relative; + .chartTop { + position: relative; + overflow: hidden; + width: 100%; + } + .chartTopMargin { + margin-bottom: 12px; + } + .chartTopHasMargin { + margin-bottom: 20px; + } + .metaWrap { + float: left; + } + .avatar { + position: relative; + top: 4px; + float: left; + margin-right: 20px; + img { + border-radius: 100%; + } + } + .meta { + color: @text-color-secondary; + font-size: @font-size-base; + line-height: 22px; + height: 22px; + } + .action { + cursor: pointer; + position: absolute; + top: 0; + right: 0; + } + .total { + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + white-space: nowrap; + color: @heading-color; + margin-top: 4px; + margin-bottom: 0; + font-size: 30px; + line-height: 38px; + height: 38px; + } + .content { + margin-bottom: 12px; + position: relative; + width: 100%; + } + .contentFixed { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + } + .footer { + border-top: 1px solid @border-color-split; + padding-top: 9px; + margin-top: 8px; + & > * { + position: relative; + } + } + .footerMargin { + margin-top: 20px; + } +} diff --git a/src/main/frontend/src/components/Charts/Field/index.d.ts b/src/main/frontend/src/components/Charts/Field/index.d.ts new file mode 100644 index 0000000..7fa1328 --- /dev/null +++ b/src/main/frontend/src/components/Charts/Field/index.d.ts @@ -0,0 +1,7 @@ +import * as React from "react"; +export interface FieldProps { + label: React.ReactNode; + value: React.ReactNode; +} + +export default class Field extends React.Component<FieldProps, any> {} diff --git a/src/main/frontend/src/components/Charts/Field/index.js b/src/main/frontend/src/components/Charts/Field/index.js new file mode 100644 index 0000000..0f9ace2 --- /dev/null +++ b/src/main/frontend/src/components/Charts/Field/index.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import styles from './index.less'; + +const Field = ({ label, value, ...rest }) => ( + <div className={styles.field} {...rest}> + <span>{label}</span> + <span>{value}</span> + </div> +); + +export default Field; diff --git a/src/main/frontend/src/components/Charts/Field/index.less b/src/main/frontend/src/components/Charts/Field/index.less new file mode 100644 index 0000000..2848f9d --- /dev/null +++ b/src/main/frontend/src/components/Charts/Field/index.less @@ -0,0 +1,16 @@ +@import "~antd/lib/style/themes/default.less"; + +.field { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: 0; + span { + font-size: @font-size-base; + line-height: 22px; + } + span:last-child { + margin-left: 8px; + color: @heading-color; + } +} diff --git a/src/main/frontend/src/components/Charts/Gauge/index.d.ts b/src/main/frontend/src/components/Charts/Gauge/index.d.ts new file mode 100644 index 0000000..7f196ae --- /dev/null +++ b/src/main/frontend/src/components/Charts/Gauge/index.d.ts @@ -0,0 +1,10 @@ +import * as React from "react"; +export interface GaugeProps { + title: React.ReactNode; + color?: string; + height: number; + bgColor?: number; + percent: number; +} + +export default class Gauge extends React.Component<GaugeProps, any> {} diff --git a/src/main/frontend/src/components/Charts/Gauge/index.js b/src/main/frontend/src/components/Charts/Gauge/index.js new file mode 100644 index 0000000..cba4202 --- /dev/null +++ b/src/main/frontend/src/components/Charts/Gauge/index.js @@ -0,0 +1,202 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import equal from '../equal'; + +const { Shape } = G2; + +const primaryColor = '#2F9CFF'; +const backgroundColor = '#F0F2F5'; + +/* eslint no-underscore-dangle: 0 */ +class Gauge extends PureComponent { + componentDidMount() { + setTimeout(() => { + this.renderChart(); + }, 10); + } + + componentWillReceiveProps(nextProps) { + if (!equal(this.props, nextProps)) { + setTimeout(() => { + this.renderChart(nextProps); + }, 10); + } + } + + componentWillUnmount() { + if (this.chart) { + this.chart.destroy(); + } + } + + handleRef = (n) => { + this.node = n; + } + + initChart(nextProps) { + const { title, color = primaryColor } = nextProps || this.props; + + Shape.registShape('point', 'dashBoard', { + drawShape(cfg, group) { + const originPoint = cfg.points[0]; + const point = this.parsePoint({ x: originPoint.x, y: 0.4 }); + + const center = this.parsePoint({ + x: 0, + y: 0, + }); + + const shape = group.addShape('polygon', { + attrs: { + points: [ + [center.x, center.y], + [point.x + 8, point.y], + [point.x + 8, point.y - 2], + [center.x, center.y - 2], + ], + radius: 2, + lineWidth: 2, + arrow: false, + fill: color, + }, + }); + + group.addShape('Marker', { + attrs: { + symbol: 'circle', + lineWidth: 2, + fill: color, + radius: 8, + x: center.x, + y: center.y, + }, + }); + group.addShape('Marker', { + attrs: { + symbol: 'circle', + lineWidth: 2, + fill: '#fff', + radius: 5, + x: center.x, + y: center.y, + }, + }); + + const { origin } = cfg; + group.addShape('text', { + attrs: { + x: center.x, + y: center.y + 80, + text: `${origin._origin.value}%`, + textAlign: 'center', + fontSize: 24, + fill: 'rgba(0, 0, 0, 0.85)', + }, + }); + group.addShape('text', { + attrs: { + x: center.x, + y: center.y + 45, + text: title, + textAlign: 'center', + fontSize: 14, + fill: 'rgba(0, 0, 0, 0.43)', + }, + }); + + return shape; + }, + }); + } + + renderChart(nextProps) { + const { + height, color = primaryColor, bgColor = backgroundColor, title, percent, format, + } = nextProps || this.props; + const data = [{ name: title, value: percent }]; + + if (this.chart) { + this.chart.clear(); + } + if (this.node) { + this.node.innerHTML = ''; + } + + this.initChart(nextProps); + + const chart = new G2.Chart({ + container: this.node, + forceFit: true, + height, + animate: false, + plotCfg: { + margin: [10, 10, 30, 10], + }, + }); + + chart.source(data); + + chart.tooltip(false); + + chart.coord('gauge', { + startAngle: -1.2 * Math.PI, + endAngle: 0.20 * Math.PI, + }); + chart.col('value', { + type: 'linear', + nice: true, + min: 0, + max: 100, + tickCount: 6, + }); + chart.axis('value', { + subTick: false, + tickLine: { + stroke: color, + lineWidth: 2, + value: -14, + }, + labelOffset: -12, + formatter: format, + }); + chart.point().position('value').shape('dashBoard'); + draw(data); + + /* eslint no-shadow: 0 */ + function draw(data) { + const val = data[0].value; + const lineWidth = 12; + chart.guide().clear(); + + chart.guide().arc(() => { + return [0, 0.95]; + }, () => { + return [val, 0.95]; + }, { + stroke: color, + lineWidth, + }); + + chart.guide().arc(() => { + return [val, 0.95]; + }, (arg) => { + return [arg.max, 0.95]; + }, { + stroke: bgColor, + lineWidth, + }); + + chart.changeData(data); + } + + this.chart = chart; + } + + render() { + return ( + <div ref={this.handleRef} /> + ); + } +} + +export default Gauge; diff --git a/src/main/frontend/src/components/Charts/MiniArea/index.d.ts b/src/main/frontend/src/components/Charts/MiniArea/index.d.ts new file mode 100644 index 0000000..d2f67e4 --- /dev/null +++ b/src/main/frontend/src/components/Charts/MiniArea/index.d.ts @@ -0,0 +1,29 @@ +import * as React from "react"; + +// g2已经更新到3.0 +// 不带的写了 + +export interface Axis { + title: any; + line: any; + gridAlign: any; + labels: any; + tickLine: any; + grid: any; +} + +export interface MiniAreaProps { + color?: string; + height: number; + borderColor?: string; + line?: boolean; + animate?: boolean; + xAxis?: Axis; + yAxis?: Axis; + data: Array<{ + x: number; + y: number; + }>; +} + +export default class MiniArea extends React.Component<MiniAreaProps, any> {} diff --git a/src/main/frontend/src/components/Charts/MiniArea/index.js b/src/main/frontend/src/components/Charts/MiniArea/index.js new file mode 100644 index 0000000..65f0969 --- /dev/null +++ b/src/main/frontend/src/components/Charts/MiniArea/index.js @@ -0,0 +1,125 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import equal from '../equal'; +import styles from '../index.less'; + +class MiniArea extends PureComponent { + static defaultProps = { + borderColor: '#1890FF', + color: 'rgba(24, 144, 255, 0.2)', + }; + + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (!equal(this.props, nextProps)) { + this.renderChart(nextProps.data); + } + } + + componentWillUnmount() { + if (this.chart) { + this.chart.destroy(); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { + height = 0, fit = true, color, borderWidth = 2, line, xAxis, yAxis, animate = true, + } = this.props; + const borderColor = this.props.borderColor || color; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height + 54, + animate, + plotCfg: { + margin: [36, 5, 30, 5], + }, + legend: null, + }); + + if (!xAxis && !yAxis) { + chart.axis(false); + } + + if (xAxis) { + chart.axis('x', xAxis); + } else { + chart.axis('x', false); + } + + if (yAxis) { + chart.axis('y', yAxis); + } else { + chart.axis('y', false); + } + + const dataConfig = { + x: { + type: 'cat', + range: [0, 1], + ...xAxis, + }, + y: { + min: 0, + ...yAxis, + }, + }; + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + title: null, + name: 'x', + value: 'y', + }, + }); + + const view = chart.createView(); + view.source(data, dataConfig); + + view.area().position('x*y').color(color).shape('smooth') + .style({ fillOpacity: 1 }); + + if (line) { + const view2 = chart.createView(); + view2.source(data, dataConfig); + view2.line().position('x*y').color(borderColor).size(borderWidth) + .shape('smooth'); + view2.tooltip(false); + } + chart.render(); + + this.chart = chart; + } + + render() { + const { height } = this.props; + + return ( + <div className={styles.miniChart} style={{ height }}> + <div className={styles.chartContent}> + <div ref={this.handleRef} /> + </div> + </div> + ); + } +} + +export default MiniArea; diff --git a/src/main/frontend/src/components/Charts/MiniBar/index.d.ts b/src/main/frontend/src/components/Charts/MiniBar/index.d.ts new file mode 100644 index 0000000..09bd761 --- /dev/null +++ b/src/main/frontend/src/components/Charts/MiniBar/index.d.ts @@ -0,0 +1,11 @@ +import * as React from "react"; +export interface MiniBarProps { + color?: string; + height: number; + data: Array<{ + x: number; + y: number; + }>; +} + +export default class MiniBar extends React.Component<MiniBarProps, any> {} diff --git a/src/main/frontend/src/components/Charts/MiniBar/index.js b/src/main/frontend/src/components/Charts/MiniBar/index.js new file mode 100644 index 0000000..991571b --- /dev/null +++ b/src/main/frontend/src/components/Charts/MiniBar/index.js @@ -0,0 +1,87 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import equal from '../equal'; +import styles from '../index.less'; + +class MiniBar extends PureComponent { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (!equal(this.props, nextProps)) { + this.renderChart(nextProps.data); + } + } + + componentWillUnmount() { + if (this.chart) { + this.chart.destroy(); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 0, fit = true, color = '#1890FF' } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const { Frame } = G2; + const frame = new Frame(data); + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height + 54, + plotCfg: { + margin: [36, 5, 30, 5], + }, + legend: null, + }); + + chart.axis(false); + + chart.source(frame, { + x: { + type: 'cat', + }, + y: { + min: 0, + }, + }); + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + name: 'x', + }, + }); + chart.interval().position('x*y').color(color); + chart.render(); + + this.chart = chart; + } + + render() { + const { height } = this.props; + + return ( + <div className={styles.miniChart} style={{ height }}> + <div className={styles.chartContent}> + <div ref={this.handleRef} /> + </div> + </div> + ); + } +} + +export default MiniBar; diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts b/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts new file mode 100644 index 0000000..a80b935 --- /dev/null +++ b/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts @@ -0,0 +1,12 @@ +import * as React from "react"; +export interface MiniProgressProps { + target: number; + color?: string; + strokeWidth?: number; + percent?: number; +} + +export default class MiniProgress extends React.Component< + MiniProgressProps, + any +> {} diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.js b/src/main/frontend/src/components/Charts/MiniProgress/index.js new file mode 100644 index 0000000..08fe9b5 --- /dev/null +++ b/src/main/frontend/src/components/Charts/MiniProgress/index.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { Tooltip } from 'antd'; + +import styles from './index.less'; + +const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => ( + <div className={styles.miniProgress}> + <Tooltip title={`目标值: ${target}%`}> + <div + className={styles.target} + style={{ left: (target ? `${target}%` : null) }} + > + <span style={{ backgroundColor: (color || null) }} /> + <span style={{ backgroundColor: (color || null) }} /> + </div> + </Tooltip> + <div className={styles.progressWrap}> + <div + className={styles.progress} + style={{ + backgroundColor: (color || null), + width: (percent ? `${percent}%` : null), + height: (strokeWidth || null), + }} + /> + </div> + </div> +); + +export default MiniProgress; diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.less b/src/main/frontend/src/components/Charts/MiniProgress/index.less new file mode 100644 index 0000000..06823be --- /dev/null +++ b/src/main/frontend/src/components/Charts/MiniProgress/index.less @@ -0,0 +1,35 @@ +@import "~antd/lib/style/themes/default.less"; + +.miniProgress { + padding: 5px 0; + position: relative; + width: 100%; + .progressWrap { + background-color: @background-color-base; + position: relative; + } + .progress { + transition: all .4s cubic-bezier(.08, .82, .17, 1) 0s; + border-radius: 1px 0 0 1px; + background-color: @primary-color; + width: 0; + height: 100%; + } + .target { + position: absolute; + top: 0; + bottom: 0; + span { + border-radius: 100px; + position: absolute; + top: 0; + left: 0; + height: 4px; + width: 2px; + } + span:last-child { + top: auto; + bottom: 0; + } + } +} diff --git a/src/main/frontend/src/components/Charts/Pie/index.d.ts b/src/main/frontend/src/components/Charts/Pie/index.d.ts new file mode 100644 index 0000000..44a465d --- /dev/null +++ b/src/main/frontend/src/components/Charts/Pie/index.d.ts @@ -0,0 +1,20 @@ +import * as React from "react"; +export interface PieProps { + animate?: boolean; + color?: string; + height: number; + hasLegend?: boolean; + margin?: [number, number, number, number]; + percent?: number; + data?: Array<{ + x: string; + y: number; + }>; + total?: string; + title?: React.ReactNode; + tooltip?: boolean; + valueFormat?: (value: string) => string; + subTitle?: React.ReactNode; +} + +export default class Pie extends React.Component<PieProps, any> {} diff --git a/src/main/frontend/src/components/Charts/Pie/index.js b/src/main/frontend/src/components/Charts/Pie/index.js new file mode 100644 index 0000000..74cd229 --- /dev/null +++ b/src/main/frontend/src/components/Charts/Pie/index.js @@ -0,0 +1,260 @@ +import React, { Component } from 'react'; +import G2 from 'g2'; +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 equal from '../equal'; +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +class Pie extends Component { + state = { + legendData: [], + legendBlock: true, + }; + + componentDidMount() { + this.renderChart(); + this.resize(); + window.addEventListener('resize', this.resize); + } + + componentWillReceiveProps(nextProps) { + if (!equal(this.props, nextProps)) { + this.renderChart(nextProps.data); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.resize); + if (this.chart) { + this.chart.destroy(); + } + this.resize.cancel(); + } + + @Bind() + @Debounce(300) + resize() { + const { hasLegend } = this.props; + if (!hasLegend || !this.root) { + window.removeEventListener('resize', this.resize); + return; + } + if (this.root.parentNode.clientWidth <= 380) { + if (!this.state.legendBlock) { + this.setState({ + legendBlock: true, + }, () => { + this.renderChart(); + }); + } + } else if (this.state.legendBlock) { + this.setState({ + legendBlock: false, + }, () => { + this.renderChart(); + }); + } + } + + handleRef = (n) => { + this.node = n; + } + + handleRoot = (n) => { + this.root = n; + } + + handleLegendClick = (item, i) => { + const newItem = item; + newItem.checked = !newItem.checked; + + const { legendData } = this.state; + legendData[i] = newItem; + + if (this.chart) { + const filterItem = legendData.filter(l => l.checked).map(l => l.x); + this.chart.filter('x', filterItem); + this.chart.repaint(); + } + + this.setState({ + legendData, + }); + } + + renderChart(d) { + let data = d || this.props.data; + + const { + height = 0, + hasLegend, + fit = true, + margin = [12, 0, 12, 0], percent, color, + inner = 0.75, + animate = true, + colors, + lineWidth = 0, + } = this.props; + + const defaultColors = colors; + + let selected = this.props.selected || true; + let tooltip = this.props.tooltips || true; + + let formatColor; + if (percent) { + selected = false; + tooltip = false; + formatColor = (value) => { + if (value === '占比') { + return color || 'rgba(24, 144, 255, 0.85)'; + } else { + return '#F0F2F5'; + } + }; + + /* eslint no-param-reassign: */ + data = [ + { + x: '占比', + y: parseFloat(percent), + }, + { + x: '反比', + y: 100 - parseFloat(percent), + }, + ]; + } + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const { Stat } = G2; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height, + plotCfg: { + margin, + }, + animate, + }); + + if (!tooltip) { + chart.tooltip(false); + } else { + chart.tooltip({ + title: null, + }); + } + + chart.axis(false); + chart.legend(false); + + chart.source(data, { + x: { + type: 'cat', + range: [0, 1], + }, + y: { + min: 0, + }, + }); + + chart.coord('theta', { + inner, + }); + + chart + .intervalStack() + .position(Stat.summary.percent('y')) + .style({ lineWidth, stroke: '#fff' }) + .color('x', percent ? formatColor : defaultColors) + .selected(selected); + + chart.render(); + + this.chart = chart; + + let legendData = []; + if (hasLegend) { + const geom = chart.getGeoms()[0]; // 获取所有的图形 + const items = geom.getData(); // 获取图形对应的数据 + legendData = items.map((item) => { + /* eslint no-underscore-dangle:0 */ + const origin = item._origin; + origin.color = item.color; + origin.checked = true; + return origin; + }); + } + + this.setState({ + legendData, + }); + } + + render() { + const { valueFormat, subTitle, total, hasLegend, className, style } = this.props; + const { legendData, legendBlock } = this.state; + const pieClassName = classNames(styles.pie, className, { + [styles.hasLegend]: !!hasLegend, + [styles.legendBlock]: legendBlock, + }); + + return ( + <div ref={this.handleRoot} className={pieClassName} style={style}> + <ReactFitText maxFontSize={25}> + <div className={styles.chart}> + <div ref={this.handleRef} style={{ fontSize: 0 }} /> + { + (subTitle || total) && ( + <div className={styles.total}> + {subTitle && <h4 className="pie-sub-title">{subTitle}</h4>} + { + // eslint-disable-next-line + total && <div className="pie-stat" dangerouslySetInnerHTML={{ __html: total }} /> + } + </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}>{`${(item['..percent'] * 100).toFixed(2)}%`}</span> + <span + className={styles.value} + dangerouslySetInnerHTML={{ + __html: valueFormat ? valueFormat(item.y) : item.y, + }} + /> + </li> + )) + } + </ul> + ) + } + </div> + ); + } +} + +export default Pie; diff --git a/src/main/frontend/src/components/Charts/Pie/index.less b/src/main/frontend/src/components/Charts/Pie/index.less new file mode 100644 index 0000000..9478739 --- /dev/null +++ b/src/main/frontend/src/components/Charts/Pie/index.less @@ -0,0 +1,94 @@ +@import "~antd/lib/style/themes/default.less"; + +.pie { + position: relative; + .chart { + position: relative; + } + &.hasLegend .chart { + width: ~"calc(100% - 240px)"; + } + .legend { + position: absolute; + right: 0; + min-width: 200px; + top: 50%; + transform: translateY(-50%); + margin: 0 20px; + list-style: none; + padding: 0; + li { + cursor: pointer; + margin-bottom: 16px; + height: 22px; + line-height: 22px; + &:last-child { + margin-bottom: 0; + } + } + } + .dot { + border-radius: 8px; + display: inline-block; + margin-right: 8px; + position: relative; + top: -1px; + height: 8px; + width: 8px; + } + .line { + background-color: @border-color-split; + display: inline-block; + margin-right: 8px; + width: 1px; + height: 16px; + } + .legendTitle { + color: @text-color; + } + .percent { + color: @text-color-secondary; + } + .value { + position: absolute; + right: 0; + } + .title { + margin-bottom: 8px; + } + .total { + position: absolute; + left: 50%; + top: 50%; + text-align: center; + height: 62px; + transform: translate(-50%, -50%); + & > h4 { + color: @text-color-secondary; + font-size: 14px; + line-height: 22px; + height: 22px; + margin-bottom: 8px; + font-weight: normal; + } + & > p { + color: @heading-color; + display: block; + font-size: 1.2em; + height: 32px; + line-height: 32px; + white-space: nowrap; + } + } +} + +.legendBlock { + &.hasLegend .chart { + width: 100%; + margin: 0 0 32px 0; + } + .legend { + position: relative; + transform: none; + } +} diff --git a/src/main/frontend/src/components/Charts/Radar/index.d.ts b/src/main/frontend/src/components/Charts/Radar/index.d.ts new file mode 100644 index 0000000..fa85978 --- /dev/null +++ b/src/main/frontend/src/components/Charts/Radar/index.d.ts @@ -0,0 +1,14 @@ +import * as React from "react"; +export interface RadarProps { + title?: React.ReactNode; + height: number; + margin?: [number, number, number, number]; + hasLegend?: boolean; + data: Array<{ + name: string; + label: string; + value: string; + }>; +} + +export default class Radar extends React.Component<RadarProps, any> {} diff --git a/src/main/frontend/src/components/Charts/Radar/index.js b/src/main/frontend/src/components/Charts/Radar/index.js new file mode 100644 index 0000000..8f338bc --- /dev/null +++ b/src/main/frontend/src/components/Charts/Radar/index.js @@ -0,0 +1,189 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import { Row, Col } from 'antd'; +import equal from '../equal'; +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +class Radar extends PureComponent { + state = { + legendData: [], + } + + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (!equal(this.props, nextProps)) { + this.renderChart(nextProps.data); + } + } + + componentWillUnmount() { + if (this.chart) { + this.chart.destroy(); + } + } + + handleRef = (n) => { + this.node = n; + } + + handleLegendClick = (item, i) => { + const newItem = item; + newItem.checked = !newItem.checked; + + const { legendData } = this.state; + legendData[i] = newItem; + + if (this.chart) { + const filterItem = legendData.filter(l => l.checked).map(l => l.name); + this.chart.filter('name', filterItem); + this.chart.repaint(); + } + + this.setState({ + legendData, + }); + } + + renderChart(data) { + const { height = 0, + hasLegend = true, + fit = true, + tickCount = 4, + margin = [24, 30, 16, 30] } = this.props; + + const colors = [ + '#1890FF', '#FACC14', '#2FC25B', '#8543E0', '#F04864', '#13C2C2', '#fa8c16', '#a0d911', + ]; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height - (hasLegend ? 80 : 22), + plotCfg: { + margin, + }, + }); + + this.chart = chart; + + chart.source(data, { + value: { + min: 0, + tickCount, + }, + }); + + chart.coord('polar'); + chart.legend(false); + + chart.axis('label', { + line: null, + labelOffset: 8, + labels: { + label: { + fill: 'rgba(0, 0, 0, .65)', + }, + }, + grid: { + line: { + stroke: '#e9e9e9', + lineWidth: 1, + lineDash: [0, 0], + }, + }, + }); + + chart.axis('value', { + grid: { + type: 'polygon', + line: { + stroke: '#e9e9e9', + lineWidth: 1, + lineDash: [0, 0], + }, + }, + labels: { + label: { + fill: 'rgba(0, 0, 0, .65)', + }, + }, + }); + + chart.line().position('label*value').color('name', colors); + chart.point().position('label*value').color('name', colors).shape('circle') + .size(3); + + chart.render(); + + if (hasLegend) { + const geom = chart.getGeoms()[0]; // 获取所有的图形 + const items = geom.getData(); // 获取图形对应的数据 + const legendData = items.map((item) => { + /* eslint no-underscore-dangle:0 */ + const origin = item._origin; + const result = { + name: origin[0].name, + color: item.color, + checked: true, + value: origin.reduce((p, n) => p + n.value, 0), + }; + + return result; + }); + + this.setState({ + legendData, + }); + } + } + + render() { + const { height, title, hasLegend } = this.props; + const { legendData } = this.state; + + return ( + <div className={styles.radar} style={{ height }}> + <div> + {title && <h4>{title}</h4>} + <div ref={this.handleRef} /> + { + hasLegend && ( + <Row className={styles.legend}> + { + legendData.map((item, i) => ( + <Col + span={(24 / legendData.length)} + key={item.name} + onClick={() => this.handleLegendClick(item, i)} + > + <div className={styles.legendItem}> + <p> + <span className={styles.dot} style={{ backgroundColor: !item.checked ? '#aaa' : item.color }} /> + <span>{item.name}</span> + </p> + <h6>{item.value}</h6> + </div> + </Col> + )) + } + </Row> + ) + } + </div> + </div> + ); + } +} + +export default Radar; diff --git a/src/main/frontend/src/components/Charts/Radar/index.less b/src/main/frontend/src/components/Charts/Radar/index.less new file mode 100644 index 0000000..378db9c --- /dev/null +++ b/src/main/frontend/src/components/Charts/Radar/index.less @@ -0,0 +1,46 @@ +@import "~antd/lib/style/themes/default.less"; + +.radar { + .legend { + margin-top: 16px; + .legendItem { + position: relative; + text-align: center; + cursor: pointer; + color: @text-color-secondary; + line-height: 22px; + p { + margin: 0; + } + h6 { + color: @heading-color; + padding-left: 16px; + font-size: 24px; + line-height: 32px; + margin-top: 4px; + margin-bottom: 0; + } + &:after { + background-color: @border-color-split; + position: absolute; + top: 8px; + right: 0; + height: 40px; + width: 1px; + content: ''; + } + } + > :last-child .legendItem:after { + display: none; + } + .dot { + border-radius: 6px; + display: inline-block; + margin-right: 6px; + position: relative; + top: -1px; + height: 6px; + width: 6px; + } + } +} diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.d.ts b/src/main/frontend/src/components/Charts/TagCloud/index.d.ts new file mode 100644 index 0000000..e783213 --- /dev/null +++ b/src/main/frontend/src/components/Charts/TagCloud/index.d.ts @@ -0,0 +1,10 @@ +import * as React from "react"; +export interface TagCloudProps { + data: Array<{ + name: string; + value: number; + }>; + height: number; +} + +export default class TagCloud extends React.Component<TagCloudProps, any> {} diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.js b/src/main/frontend/src/components/Charts/TagCloud/index.js new file mode 100644 index 0000000..d3f0d70 --- /dev/null +++ b/src/main/frontend/src/components/Charts/TagCloud/index.js @@ -0,0 +1,170 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; +import G2 from 'g2'; +import Cloud from 'g-cloud'; +import Debounce from 'lodash-decorators/debounce'; +import Bind from 'lodash-decorators/bind'; +import styles from './index.less'; + +/* eslint no-underscore-dangle: 0 */ +/* eslint no-param-reassign: 0 */ + +const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png'; + +class TagCloud extends PureComponent { + componentDidMount() { + this.initTagCloud(); + this.renderChart(); + + window.addEventListener('resize', this.resize); + } + + componentWillReceiveProps(nextProps) { + if (this.props.data !== nextProps.data) { + this.renderChart(nextProps.data); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.resize); + this.renderChart.cancel(); + } + + resize = () => { + this.renderChart(); + } + + initTagCloud = () => { + const { Util, Shape } = G2; + + function getTextAttrs(cfg) { + const textAttrs = Util.mix(true, {}, { + fillOpacity: cfg.opacity, + fontSize: cfg.size, + rotate: cfg.origin._origin.rotate, + // rotate: cfg.origin._origin.rotate, + text: cfg.origin._origin.text, + textAlign: 'center', + fill: cfg.color, + textBaseline: 'Alphabetic', + }, cfg.style); + return textAttrs; + } + + // 给point注册一个词云的shape + Shape.registShape('point', 'cloud', { + drawShape(cfg, container) { + cfg.points = this.parsePoints(cfg.points); + const attrs = getTextAttrs(cfg); + const shape = container.addShape('text', { + attrs: Util.mix(attrs, { + x: cfg.points[0].x, + y: cfg.points[0].y, + }), + }); + return shape; + }, + }); + } + + saveRootRef = (node) => { + this.root = node; + } + + saveNodeRef = (node) => { + this.node = node; + } + + @Bind() + @Debounce(500) + renderChart(newData) { + const data = newData || this.props.data; + if (!data || data.length < 1) { + return; + } + + const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C']; + + const height = this.props.height * 4; + let width = 0; + if (this.root) { + width = this.root.offsetWidth * 4; + } + + data.sort((a, b) => b.value - a.value); + + const max = data[0].value; + const min = data[data.length - 1].value; + + // 构造一个词云布局对象 + const layout = new Cloud({ + words: data, + width, + height, + + rotate: () => 0, + + // 设定文字大小配置函数(默认为12-24px的随机大小) + size: words => (((words.value - min) / (max - min)) * 50) + 30, + + // 设定文字内容 + text: words => words.name, + }); + + layout.image(imgUrl, (imageCloud) => { + // clean + if (this.node) { + this.node.innerHTML = ''; + } + + // 执行词云布局函数,并在回调函数中调用G2对结果进行绘制 + imageCloud.exec((texts) => { + const chart = new G2.Chart({ + container: this.node, + width, + height, + plotCfg: { + margin: 0, + }, + }); + + chart.legend(false); + chart.axis(false); + chart.tooltip(false); + + chart.source(texts); + + // 将词云坐标系调整为G2的坐标系 + chart.coord().reflect(); + + chart + .point() + .position('x*y') + .color('text', colors) + .size('size', size => size) + .shape('cloud') + .style({ + fontStyle: texts[0].style, + fontFamily: texts[0].font, + fontWeight: texts[0].weight, + }); + + chart.render(); + }); + }); + } + + render() { + return ( + <div + className={classNames(styles.tagCloud, this.props.className)} + ref={this.saveRootRef} + style={{ width: '100%' }} + > + <div ref={this.saveNodeRef} style={{ height: this.props.height }} /> + </div> + ); + } +} + +export default TagCloud; diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.less b/src/main/frontend/src/components/Charts/TagCloud/index.less new file mode 100644 index 0000000..96b1006 --- /dev/null +++ b/src/main/frontend/src/components/Charts/TagCloud/index.less @@ -0,0 +1,6 @@ +.tagCloud { + canvas { + transform: scale(0.25); + transform-origin: 0 0; + } +} diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts b/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts new file mode 100644 index 0000000..5ea76a1 --- /dev/null +++ b/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts @@ -0,0 +1,15 @@ +import * as React from "react"; +export interface TimelineChartProps { + data: Array<{ + x: string; + y1: string; + y2: string; + }>; + titleMap: { y1: string; y2: string }; + height?: number; +} + +export default class TimelineChart extends React.Component< + TimelineChartProps, + any +> {} diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.js b/src/main/frontend/src/components/Charts/TimelineChart/index.js new file mode 100644 index 0000000..65048b8 --- /dev/null +++ b/src/main/frontend/src/components/Charts/TimelineChart/index.js @@ -0,0 +1,125 @@ +import React, { Component } from 'react'; +import G2 from 'g2'; +import Slider from 'g2-plugin-slider'; +import styles from './index.less'; + +class TimelineChart extends Component { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + componentWillUnmount() { + if (this.chart) { + this.chart.destroy(); + } + if (this.slider) { + this.slider.destroy(); + } + } + + sliderId = `timeline-chart-slider-${Math.random() * 1000}` + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 400, margin = [60, 20, 40, 40], titleMap, borderWidth = 2 } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + if (this.sliderId) { + document.getElementById(this.sliderId).innerHTML = ''; + } + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: true, + height, + plotCfg: { + margin, + }, + }); + + chart.axis('x', { + title: false, + }); + chart.axis('y1', { + title: false, + }); + chart.axis('y2', false); + + chart.legend({ + mode: false, + position: 'top', + }); + + let max; + if (data[0] && data[0].y1 && data[0].y2) { + max = Math.max(data.sort((a, b) => b.y1 - a.y1)[0].y1, + data.sort((a, b) => b.y2 - a.y2)[0].y2); + } + + chart.source(data, { + x: { + type: 'timeCat', + tickCount: 16, + mask: 'HH:MM', + range: [0, 1], + }, + y1: { + alias: titleMap.y1, + max, + min: 0, + }, + y2: { + alias: titleMap.y2, + max, + min: 0, + }, + }); + + chart.line().position('x*y1').color('#1890FF').size(borderWidth); + chart.line().position('x*y2').color('#2FC25B').size(borderWidth); + + this.chart = chart; + + /* eslint new-cap:0 */ + const slider = new Slider({ + domId: this.sliderId, + height: 26, + xDim: 'x', + yDim: 'y1', + charts: [chart], + }); + slider.render(); + + this.slider = slider; + } + + render() { + const { height, title } = this.props; + + return ( + <div className={styles.timelineChart} style={{ height }}> + <div> + { title && <h4>{title}</h4>} + <div ref={this.handleRef} /> + <div id={this.sliderId} /> + </div> + </div> + ); + } +} + +export default TimelineChart; diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.less b/src/main/frontend/src/components/Charts/TimelineChart/index.less new file mode 100644 index 0000000..1751975 --- /dev/null +++ b/src/main/frontend/src/components/Charts/TimelineChart/index.less @@ -0,0 +1,3 @@ +.timelineChart { + background: #fff; +} diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.d.ts b/src/main/frontend/src/components/Charts/WaterWave/index.d.ts new file mode 100644 index 0000000..0fefbea --- /dev/null +++ b/src/main/frontend/src/components/Charts/WaterWave/index.d.ts @@ -0,0 +1,9 @@ +import * as React from "react"; +export interface WaterWaveProps { + title: React.ReactNode; + color?: string; + height: number; + percent: number; +} + +export default class WaterWave extends React.Component<WaterWaveProps, any> {} diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.js b/src/main/frontend/src/components/Charts/WaterWave/index.js new file mode 100644 index 0000000..a9eece6 --- /dev/null +++ b/src/main/frontend/src/components/Charts/WaterWave/index.js @@ -0,0 +1,200 @@ +import React, { PureComponent } from 'react'; +import styles from './index.less'; + +/* eslint no-return-assign: 0 */ +// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90 + +class WaterWave extends PureComponent { + static defaultProps = { + height: 160, + } + state = { + radio: 1, + } + + componentDidMount() { + this.renderChart(); + this.resize(); + + window.addEventListener('resize', this.resize); + } + + componentWillUnmount() { + cancelAnimationFrame(this.timer); + if (this.node) { + this.node.innerHTML = ''; + } + window.removeEventListener('resize', this.resize); + } + + resize = () => { + const { height } = this.props; + const { offsetWidth } = this.root.parentNode; + this.setState({ + radio: offsetWidth < height ? offsetWidth / height : 1, + }); + } + + renderChart() { + const { percent, color = '#1890FF' } = this.props; + const data = percent / 100; + const self = this; + + if (!this.node || !data) { + return; + } + + const canvas = this.node; + const ctx = canvas.getContext('2d'); + + const canvasWidth = canvas.width; + const canvasHeight = canvas.height; + const radius = canvasWidth / 2; + const lineWidth = 2; + const cR = radius - (lineWidth); + + ctx.beginPath(); + ctx.lineWidth = lineWidth * 2; + + const axisLength = canvasWidth - (lineWidth); + const unit = axisLength / 8; + const range = 0.2; // 振幅 + let currRange = range; + const xOffset = lineWidth; + let sp = 0; // 周期偏移量 + let currData = 0; + const waveupsp = 0.005; // 水波上涨速度 + + let arcStack = []; + const bR = radius - (lineWidth); + const circleOffset = -(Math.PI / 2); + let circleLock = true; + + for (let i = circleOffset; i < circleOffset + (2 * Math.PI); i += 1 / (8 * Math.PI)) { + arcStack.push([ + radius + (bR * Math.cos(i)), + radius + (bR * Math.sin(i)), + ]); + } + + const cStartPoint = arcStack.shift(); + ctx.strokeStyle = color; + ctx.moveTo(cStartPoint[0], cStartPoint[1]); + + function drawSin() { + ctx.beginPath(); + ctx.save(); + + const sinStack = []; + for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) { + const x = sp + ((xOffset + i) / unit); + const y = Math.sin(x) * currRange; + const dx = i; + const dy = ((2 * cR * (1 - currData)) + (radius - cR)) - (unit * y); + + ctx.lineTo(dx, dy); + sinStack.push([dx, dy]); + } + + const startPoint = sinStack.shift(); + + ctx.lineTo(xOffset + axisLength, canvasHeight); + ctx.lineTo(xOffset, canvasHeight); + ctx.lineTo(startPoint[0], startPoint[1]); + + const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight); + gradient.addColorStop(0, '#ffffff'); + gradient.addColorStop(1, '#1890FF'); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.restore(); + } + + function render() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + if (circleLock) { + if (arcStack.length) { + const temp = arcStack.shift(); + ctx.lineTo(temp[0], temp[1]); + ctx.stroke(); + } else { + circleLock = false; + ctx.lineTo(cStartPoint[0], cStartPoint[1]); + ctx.stroke(); + arcStack = null; + + ctx.globalCompositeOperation = 'destination-over'; + ctx.beginPath(); + ctx.lineWidth = lineWidth; + ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1); + + ctx.beginPath(); + ctx.save(); + ctx.arc(radius, radius, radius - (3 * lineWidth), 0, 2 * Math.PI, 1); + + ctx.restore(); + ctx.clip(); + ctx.fillStyle = '#1890FF'; + } + } else { + if (data >= 0.85) { + if (currRange > range / 4) { + const t = range * 0.01; + currRange -= t; + } + } else if (data <= 0.1) { + if (currRange < range * 1.5) { + const t = range * 0.01; + currRange += t; + } + } else { + if (currRange <= range) { + const t = range * 0.01; + currRange += t; + } + if (currRange >= range) { + const t = range * 0.01; + currRange -= t; + } + } + if ((data - currData) > 0) { + currData += waveupsp; + } + if ((data - currData) < 0) { + currData -= waveupsp; + } + + sp += 0.07; + drawSin(); + } + self.timer = requestAnimationFrame(render); + } + + render(); + } + + render() { + const { radio } = this.state; + const { percent, title, height } = this.props; + return ( + <div className={styles.waterWave} ref={n => (this.root = n)} style={{ transform: `scale(${radio})` }}> + <div style={{ width: height, height, overflow: 'hidden' }}> + <canvas + className={styles.waterWaveCanvasWrapper} + ref={n => (this.node = n)} + width={height * 2} + height={height * 2} + /> + </div> + <div className={styles.text} style={{ width: height }}> + { + title && <span>{title}</span> + } + <h4>{percent}%</h4> + </div> + </div> + ); + } +} + +export default WaterWave; diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.less b/src/main/frontend/src/components/Charts/WaterWave/index.less new file mode 100644 index 0000000..d185ca3 --- /dev/null +++ b/src/main/frontend/src/components/Charts/WaterWave/index.less @@ -0,0 +1,28 @@ +@import "~antd/lib/style/themes/default.less"; + +.waterWave { + display: inline-block; + position: relative; + transform-origin: left; + .text { + position: absolute; + left: 0; + top: 32px; + text-align: center; + width: 100%; + span { + color: @text-color-secondary; + font-size: 14px; + line-height: 22px; + } + h4 { + color: @heading-color; + line-height: 32px; + font-size: 24px; + } + } + .waterWaveCanvasWrapper { + transform: scale(.5); + transform-origin: 0 0; + } +} diff --git a/src/main/frontend/src/components/Charts/demo/bar.md b/src/main/frontend/src/components/Charts/demo/bar.md new file mode 100644 index 0000000..955f44e --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/bar.md @@ -0,0 +1,26 @@ +--- +order: 4 +title: 柱状图 +--- + +通过设置 `x`,`y` 属性,可以快速的构建出一个漂亮的柱状图,各种纬度的关系则是通过自定义的数据展现。 + +````jsx +import { Bar } from 'ant-design-pro/lib/Charts'; + +const salesData = []; +for (let i = 0; i < 12; i += 1) { + salesData.push({ + x: `${i + 1}月`, + y: Math.floor(Math.random() * 1000) + 200, + }); +} + +ReactDOM.render( + <Bar + height={200} + title="销售额趋势" + data={salesData} + /> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/chart-card.md b/src/main/frontend/src/components/Charts/demo/chart-card.md new file mode 100644 index 0000000..5120479 --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/chart-card.md @@ -0,0 +1,65 @@ +--- +order: 1 +title: 图表卡片 +--- + +用于展示图表的卡片容器,可以方便的配合其它图表套件展示丰富信息。 + +````jsx +import { ChartCard, yuan, Field } from 'ant-design-pro/lib/Charts'; +import Trend from 'ant-design-pro/lib/Trend'; +import { Row, Col, Icon, Tooltip } from 'antd'; +import numeral from 'numeral'; + +ReactDOM.render( + <Row> + <Col span={24}> + <ChartCard + title="销售额" + action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>} + total={yuan(126560)} + footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />} + contentHeight={46} + > + <span> + 周同比 + <Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend> + </span> + <span style={{ marginLeft: 16 }}> + 日环比 + <Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend> + </span> + </ChartCard> + </Col> + <Col span={24} style={{ marginTop: 24 }}> + <ChartCard + title="移动指标" + avatar={ + <img + style={{ width: 56, height: 56 }} + src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" + alt="indicator" + /> + } + action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>} + total={yuan(126560)} + footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />} + /> + </Col> + <Col span={24} style={{ marginTop: 24 }}> + <ChartCard + title="移动指标" + avatar={( + <img + alt="indicator" + style={{ width: 56, height: 56 }} + src="https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png" + /> + )} + action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>} + total={yuan(126560)} + /> + </Col> + </Row> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/gauge.md b/src/main/frontend/src/components/Charts/demo/gauge.md new file mode 100644 index 0000000..f53465d --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/gauge.md @@ -0,0 +1,18 @@ +--- +order: 7 +title: 仪表盘 +--- + +仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。 + +````jsx +import { Gauge } from 'ant-design-pro/lib/Charts'; + +ReactDOM.render( + <Gauge + title="核销率" + height={164} + percent={87} + /> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/mini-area.md b/src/main/frontend/src/components/Charts/demo/mini-area.md new file mode 100644 index 0000000..2b9bfb4 --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/mini-area.md @@ -0,0 +1,28 @@ +--- +order: 2 +col: 2 +title: 迷你区域图 +--- + +````jsx +import { MiniArea } from 'ant-design-pro/lib/Charts'; +import moment from 'moment'; + +const visitData = []; +const beginDay = new Date().getTime(); +for (let i = 0; i < 20; i += 1) { + visitData.push({ + x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), + y: Math.floor(Math.random() * 100) + 10, + }); +} + +ReactDOM.render( + <MiniArea + line + color="#cceafe" + height={45} + data={visitData} + /> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/mini-bar.md b/src/main/frontend/src/components/Charts/demo/mini-bar.md new file mode 100644 index 0000000..fef301b --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/mini-bar.md @@ -0,0 +1,28 @@ +--- +order: 2 +col: 2 +title: 迷你柱状图 +--- + +迷你柱状图更适合展示简单的区间数据,简洁的表现方式可以很好的减少大数据量的视觉展现压力。 + +````jsx +import { MiniBar } from 'ant-design-pro/lib/Charts'; +import moment from 'moment'; + +const visitData = []; +const beginDay = new Date().getTime(); +for (let i = 0; i < 20; i += 1) { + visitData.push({ + x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), + y: Math.floor(Math.random() * 100) + 10, + }); +} + +ReactDOM.render( + <MiniBar + height={45} + data={visitData} + /> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/mini-pie.md b/src/main/frontend/src/components/Charts/demo/mini-pie.md new file mode 100644 index 0000000..9b1abf0 --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/mini-pie.md @@ -0,0 +1,16 @@ +--- +order: 6 +title: 迷你饼状图 +--- + +通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展 +现更多业务场景。 + +```jsx +import { Pie } from 'ant-design-pro/lib/Charts'; + +ReactDOM.render( + <Pie percent={28} subTitle="中式快餐" total="28%" height={140} />, + mountNode +); +``` diff --git a/src/main/frontend/src/components/Charts/demo/mini-progress.md b/src/main/frontend/src/components/Charts/demo/mini-progress.md new file mode 100644 index 0000000..6308a8f --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/mini-progress.md @@ -0,0 +1,12 @@ +--- +order: 3 +title: 迷你进度条 +--- + +````jsx +import { MiniProgress } from 'ant-design-pro/lib/Charts'; + +ReactDOM.render( + <MiniProgress percent={78} strokeWidth={8} target={80} /> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/mix.md b/src/main/frontend/src/components/Charts/demo/mix.md new file mode 100644 index 0000000..0c158e5 --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/mix.md @@ -0,0 +1,83 @@ +--- +order: 0 +title: 图表套件组合展示 +--- + +利用 Ant Design Pro 提供的图表套件,可以灵活组合符合设计规范的图表来满足复杂的业务需求。 + +````jsx +import { ChartCard, Field, MiniArea, MiniBar, MiniProgress } from 'ant-design-pro/lib/Charts'; +import Trend from 'ant-design-pro/lib/Trend'; +import NumberInfo from 'ant-design-pro/lib/NumberInfo'; +import { Row, Col, Icon, Tooltip } from 'antd'; +import numeral from 'numeral'; +import moment from 'moment'; + +const visitData = []; +const beginDay = new Date().getTime(); +for (let i = 0; i < 20; i += 1) { + visitData.push({ + x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), + y: Math.floor(Math.random() * 100) + 10, + }); +} + +ReactDOM.render( + <Row> + <Col span={24}> + <ChartCard + title="搜索用户数量" + contentHeight={134} + > + <NumberInfo + subTitle={<span>本周访问</span>} + total={numeral(12321).format('0,0')} + status="up" + subTotal={17.1} + /> + <MiniArea + line + height={45} + data={visitData} + /> + </ChartCard> + </Col> + <Col span={24} style={{ marginTop: 24 }}> + <ChartCard + title="访问量" + action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>} + total={numeral(8846).format('0,0')} + footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />} + contentHeight={46} + > + <MiniBar + height={46} + data={visitData} + /> + </ChartCard> + </Col> + <Col span={24} style={{ marginTop: 24 }}> + <ChartCard + title="线上购物转化率" + action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>} + total="78%" + footer={ + <div> + <span> + 周同比 + <Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend> + </span> + <span style={{ marginLeft: 16 }}> + 日环比 + <Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend> + </span> + </div> + } + contentHeight={46} + > + <MiniProgress percent={78} strokeWidth={8} target={80} /> + </ChartCard> + </Col> + </Row> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/pie.md b/src/main/frontend/src/components/Charts/demo/pie.md new file mode 100644 index 0000000..2929f2a --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/pie.md @@ -0,0 +1,47 @@ +--- +order: 5 +title: 饼状图 +--- + +````jsx +import { Pie, yuan } from 'ant-design-pro/lib/Charts'; + +const salesPieData = [ + { + x: '家用电器', + y: 4544, + }, + { + x: '食用酒水', + y: 3321, + }, + { + x: '个护健康', + y: 3113, + }, + { + x: '服饰箱包', + y: 2341, + }, + { + x: '母婴产品', + y: 1231, + }, + { + x: '其他', + y: 1231, + }, +]; + +ReactDOM.render( + <Pie + hasLegend + title="销售额" + subTitle="销售额" + total={yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))} + data={salesPieData} + valueFormat={val => yuan(val)} + height={294} + /> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/radar.md b/src/main/frontend/src/components/Charts/demo/radar.md new file mode 100644 index 0000000..584344a --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/radar.md @@ -0,0 +1,64 @@ +--- +order: 7 +title: 雷达图 +--- + +````jsx +import { Radar, ChartCard } from 'ant-design-pro/lib/Charts'; + +const radarOriginData = [ + { + name: '个人', + ref: 10, + koubei: 8, + output: 4, + contribute: 5, + hot: 7, + }, + { + name: '团队', + ref: 3, + koubei: 9, + output: 6, + contribute: 3, + hot: 1, + }, + { + name: '部门', + ref: 4, + koubei: 1, + output: 6, + contribute: 5, + hot: 7, + }, +]; +const radarData = []; +const radarTitleMap = { + ref: '引用', + koubei: '口碑', + output: '产量', + contribute: '贡献', + hot: '热度', +}; +radarOriginData.forEach((item) => { + Object.keys(item).forEach((key) => { + if (key !== 'name') { + radarData.push({ + name: item.name, + label: radarTitleMap[key], + value: item[key], + }); + } + }); +}); + +ReactDOM.render( + <ChartCard title="数据比例"> + <Radar + hasLegend + height={286} + data={radarData} + /> + </ChartCard> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/tag-cloud.md b/src/main/frontend/src/components/Charts/demo/tag-cloud.md new file mode 100644 index 0000000..c66f6fe --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/tag-cloud.md @@ -0,0 +1,25 @@ +--- +order: 9 +title: 标签云 +--- + +标签云是一套相关的标签以及与此相应的权重展示方式,一般典型的标签云有 30 至 150 个标签,而权重影响使用的字体大小或其他视觉效果。 + +````jsx +import { TagCloud } from 'ant-design-pro/lib/Charts'; + +const tags = []; +for (let i = 0; i < 50; i += 1) { + tags.push({ + name: `TagClout-Title-${i}`, + value: Math.floor((Math.random() * 50)) + 20, + }); +} + +ReactDOM.render( + <TagCloud + data={tags} + height={200} + /> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/timeline-chart.md b/src/main/frontend/src/components/Charts/demo/timeline-chart.md new file mode 100644 index 0000000..60773b5 --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/timeline-chart.md @@ -0,0 +1,27 @@ +--- +order: 9 +title: 带有时间轴的图表 +--- + +使用 `TimelineChart` 组件可以实现带有时间轴的柱状图展现,而其中的 `x` 属性,则是时间值的指向,默认最多支持同时展现两个指标,分别是 `y1` 和 `y2`。 + +````jsx +import { TimelineChart } from 'ant-design-pro/lib/Charts'; + +const chartData = []; +for (let i = 0; i < 20; i += 1) { + chartData.push({ + x: (new Date().getTime()) + (1000 * 60 * 30 * i), + y1: Math.floor(Math.random() * 100) + 1000, + y2: Math.floor(Math.random() * 100) + 10, + }); +} + +ReactDOM.render( + <TimelineChart + height={200} + data={chartData} + titleMap={{ y1: '客流量', y2: '支付笔数' }} + /> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/demo/waterwave.md b/src/main/frontend/src/components/Charts/demo/waterwave.md new file mode 100644 index 0000000..74d290f --- /dev/null +++ b/src/main/frontend/src/components/Charts/demo/waterwave.md @@ -0,0 +1,20 @@ +--- +order: 8 +title: 水波图 +--- + +水波图是一种比例的展示方式,可以更直观的展示关键值的占比。 + +````jsx +import { WaterWave } from 'ant-design-pro/lib/Charts'; + +ReactDOM.render( + <div style={{ textAlign: 'center' }}> + <WaterWave + height={161} + title="补贴资金剩余" + percent={34} + /> + </div> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Charts/equal.js b/src/main/frontend/src/components/Charts/equal.js new file mode 100644 index 0000000..ff3a4c7 --- /dev/null +++ b/src/main/frontend/src/components/Charts/equal.js @@ -0,0 +1,17 @@ +/* eslint eqeqeq: 0 */ + +function equal(old, target) { + let r = true; + for (const prop in old) { + if (typeof old[prop] === 'function' && typeof target[prop] === 'function') { + if (old[prop].toString() != target[prop].toString()) { + r = false; + } + } else if (old[prop] != target[prop]) { + r = false; + } + } + return r; +} + +export default equal; diff --git a/src/main/frontend/src/components/Charts/index.d.ts b/src/main/frontend/src/components/Charts/index.d.ts new file mode 100644 index 0000000..e47b947 --- /dev/null +++ b/src/main/frontend/src/components/Charts/index.d.ts @@ -0,0 +1,17 @@ +export { default as numeral } from "numeral"; +export { default as ChartCard } from "./ChartCard"; +export { default as Bar } from "./Bar"; +export { default as Pie } from "./Pie"; +export { default as Radar } from "./Radar"; +export { default as Gauge } from "./Gauge"; +export { default as MiniArea } from "./MiniArea"; +export { default as MiniBar } from "./MiniBar"; +export { default as MiniProgress } from "./MiniProgress"; +export { default as Field } from "./Field"; +export { default as WaterWave } from "./WaterWave"; +export { default as TagCloud } from "./TagCloud"; +export { default as TimelineChart } from "./TimelineChart"; + +declare const yuan: (value: number | string) => string; + +export { yuan }; diff --git a/src/main/frontend/src/components/Charts/index.js b/src/main/frontend/src/components/Charts/index.js new file mode 100644 index 0000000..cea9949 --- /dev/null +++ b/src/main/frontend/src/components/Charts/index.js @@ -0,0 +1,31 @@ +import numeral from 'numeral'; +import ChartCard from './ChartCard'; +import Bar from './Bar'; +import Pie from './Pie'; +import Radar from './Radar'; +import Gauge from './Gauge'; +import MiniArea from './MiniArea'; +import MiniBar from './MiniBar'; +import MiniProgress from './MiniProgress'; +import Field from './Field'; +import WaterWave from './WaterWave'; +import TagCloud from './TagCloud'; +import TimelineChart from './TimelineChart'; + +const yuan = val => `¥ ${numeral(val).format('0,0')}`; + +export default { + yuan, + Bar, + Pie, + Gauge, + Radar, + MiniBar, + MiniArea, + MiniProgress, + ChartCard, + Field, + WaterWave, + TagCloud, + TimelineChart, +}; diff --git a/src/main/frontend/src/components/Charts/index.less b/src/main/frontend/src/components/Charts/index.less new file mode 100644 index 0000000..52f97c4 --- /dev/null +++ b/src/main/frontend/src/components/Charts/index.less @@ -0,0 +1,19 @@ +.miniChart { + position: relative; + width: 100%; + .chartContent { + position: absolute; + bottom: -34px; + width: 100%; + & > div { + margin: 0 -5px; + overflow: hidden; + } + } + .chartLoading { + position: absolute; + top: 16px; + left: 50%; + margin-left: -7px; + } +} diff --git a/src/main/frontend/src/components/Charts/index.md b/src/main/frontend/src/components/Charts/index.md new file mode 100644 index 0000000..218f4eb --- /dev/null +++ b/src/main/frontend/src/components/Charts/index.md @@ -0,0 +1,132 @@ +--- +title: + en-US: Charts + zh-CN: Charts +subtitle: 图表 +order: 2 +cols: 2 +--- + +Ant Design Pro 提供的业务中常用的图表类型,都是基于 [G2](https://antv.alipay.com/g2/doc/index.html) 按照 Ant Design 图表规范封装,需要注意的是 Ant Design Pro 的图表组件以套件形式提供,可以任意组合实现复杂的业务需求。 + +因为结合了 Ant Design 的标准设计,本着极简的设计思想以及开箱即用的理念,简化了大量 API 配置,所以如果需要灵活定制图表,可以参考 Ant Design Pro 图表实现,自行基于 [G2](https://antv.alipay.com/g2/doc/index.html) 封装图表组件使用。 + +## API + +### ChartCard + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | 卡片标题 | ReactNode\|string | - | +| action | 卡片操作 | ReactNode | - | +| total | 数据总量 | ReactNode \| number | - | +| footer | 卡片底部 | ReactNode | - | +| contentHeight | 内容区域高度 | number | - | +| avatar | 右侧图标 | React.ReactNode | - | +### MiniBar + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| color | 图表颜色 | string | `#1890FF` | +| height | 图表高度 | number | - | +| data | 数据 | array<{x, y}> | - | + +### MiniArea + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| color | 图表颜色 | string | `rgba(24, 144, 255, 0.2)` | +| borderColor | 图表边颜色 | string | `#1890FF` | +| height | 图表高度 | number | - | +| line | 是否显示描边 | boolean | false | +| animate | 是否显示动画 | boolean | true | +| xAxis | [x 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - | +| yAxis | [y 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - | +| data | 数据 | array<{x, y}> | - | + +### MiniProgress + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| target | 目标比例 | number | - | +| color | 进度条颜色 | string | - | +| strokeWidth | 进度条高度 | number | - | +| percent | 进度比例 | number | - | + +### Bar + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | 图表标题 | ReactNode\|string | - | +| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` | +| margin | 图表内部间距 | array | \[32, 0, 32, 40\] | +| height | 图表高度 | number | - | +| data | 数据 | array<{x, y}> | - | +| autoLabel | 在宽度不足时,自动隐藏 x 轴的 label | boolean | `true` | + +### Pie + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| animate | 是否显示动画 | boolean | true | +| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` | +| height | 图表高度 | number | - | +| hasLegend | 是否显示 legend | boolean | `false` | +| margin | 图表内部间距 | array | \[24, 0, 24, 0\] | +| percent | 占比 | number | - | +| tooltip | 是否显示 tooltip | boolean | true | +| valueFormat | 显示值的格式化函数 | function | - | +| title | 图表标题 | ReactNode|string | - | +| subTitle | 图表子标题 | ReactNode|string | - | +| total | 图标中央的总数 | string | - | + +### Radar + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | 图表标题 | ReactNode\|string | - | +| height | 图表高度 | number | - | +| hasLegend | 是否显示 legend | boolean | `false` | +| margin | 图表内部间距 | array | \[24, 30, 16, 30\] | +| data | 图标数据 | array<{name,label,value}> | - | + +### Gauge + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | 图表标题 | ReactNode\|string | - | +| height | 图表高度 | number | - | +| color | 图表颜色 | string | `#2F9CFF` | +| bgColor | 图表背景颜色 | string | `#F0F2F5` | +| percent | 进度比例 | number | - | + +### WaterWave + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | 图表标题 | ReactNode\|string | - | +| height | 图表高度 | number | - | +| color | 图表颜色 | string | `#1890FF` | +| percent | 进度比例 | number | - | + +### TagCloud + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| data | 标题 | Array<name, value\> | - | +| height | 高度值 | number | - | + +### TimelineChart + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| data | 标题 | Array<x, y1, y2\> | - | +| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - | +| height | 高度值 | number | 400 | + +### Field + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| label | 标题 | ReactNode\|string | - | +| value | 值 | ReactNode\|string | - | diff --git a/src/main/frontend/src/components/Trend/demo/basic.md b/src/main/frontend/src/components/Trend/demo/basic.md new file mode 100644 index 0000000..82afcda --- /dev/null +++ b/src/main/frontend/src/components/Trend/demo/basic.md @@ -0,0 +1,17 @@ +--- +order: 0 +title: 演示 +--- + +在数值背后添加一个小图标来标识涨跌情况。 + +````jsx +import Trend from 'ant-design-pro/lib/Trend'; + +ReactDOM.render( + <div> + <Trend flag="up">12%</Trend> + <Trend flag="down" style={{ marginLeft: 8 }}>11%</Trend> + </div> +, mountNode); +```` diff --git a/src/main/frontend/src/components/Trend/index.d.ts b/src/main/frontend/src/components/Trend/index.d.ts new file mode 100644 index 0000000..698a49d --- /dev/null +++ b/src/main/frontend/src/components/Trend/index.d.ts @@ -0,0 +1,8 @@ +import * as React from "react"; + +export interface TrendProps { + colorful?: boolean; + flag: "up" | "down"; +} + +export default class Trend extends React.Component<TrendProps, any> {} diff --git a/src/main/frontend/src/components/Trend/index.js b/src/main/frontend/src/components/Trend/index.js new file mode 100644 index 0000000..2cbaad4 --- /dev/null +++ b/src/main/frontend/src/components/Trend/index.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { Icon } from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; + +const Trend = ({ colorful = true, flag, children, className, ...rest }) => { + const classString = classNames(styles.trendItem, { + [styles.trendItemGrey]: !colorful, + }, className); + return ( + <div + {...rest} + className={classString} + title={typeof children === 'string' ? children : ''} + > + <span className={styles.value}>{children}</span> + {flag && <span className={styles[flag]}><Icon type={`caret-${flag}`} /></span>} + </div> + ); +}; + +export default Trend; diff --git a/src/main/frontend/src/components/Trend/index.less b/src/main/frontend/src/components/Trend/index.less new file mode 100644 index 0000000..48695c9 --- /dev/null +++ b/src/main/frontend/src/components/Trend/index.less @@ -0,0 +1,30 @@ +@import "~antd/lib/style/themes/default.less"; + +.trendItem { + display: inline-block; + font-size: @font-size-base; + line-height: 22px; + + .up, + .down { + margin-left: 4px; + position: relative; + top: 1px; + i { + font-size: 12px; + transform: scale(0.83); + } + } + .up { + color: @red-6; + } + .down { + color: @green-6; + top: -1px; + } + + &.trendItemGrey .up, + &.trendItemGrey .down { + color: @text-color; + } +} diff --git a/src/main/frontend/src/components/Trend/index.md b/src/main/frontend/src/components/Trend/index.md new file mode 100644 index 0000000..683ed61 --- /dev/null +++ b/src/main/frontend/src/components/Trend/index.md @@ -0,0 +1,21 @@ +--- +title: + en-US: Trend + zh-CN: Trend +subtitle: 趋势标记 +cols: 1 +order: 14 +--- + +趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。 + +## API + +```html +<Trend flag="up">50%</Trend> +``` + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| colorful | 是否彩色标记 | Boolean | true | +| flag | 上升下降标识:`up|down` | string | - | diff --git a/src/main/frontend/src/layouts/BasicLayout.js b/src/main/frontend/src/layouts/BasicLayout.js index 6eab185..457b2cb 100644 --- a/src/main/frontend/src/layouts/BasicLayout.js +++ b/src/main/frontend/src/layouts/BasicLayout.js @@ -66,9 +66,6 @@ class BasicLayout extends React.PureComponent { return { location, breadcrumbNameMap }; } componentDidMount() { - this.props.dispatch({ - type: 'user/fetchCurrent', - }); } componentWillUnmount() { clearTimeout(this.resizeTimeout); diff --git a/src/main/frontend/src/routes/Dashboard/Dashboard.js b/src/main/frontend/src/routes/Dashboard/Dashboard.js index 353eca8..9a5dbae 100644 --- a/src/main/frontend/src/routes/Dashboard/Dashboard.js +++ b/src/main/frontend/src/routes/Dashboard/Dashboard.js @@ -1,13 +1,246 @@ import React, { PureComponent } from 'react'; import { connect } from 'dva'; +import { Row, Col, Icon, Tooltip, Card, Table } from 'antd'; +import moment from 'moment'; +import numeral from 'numeral'; +import { + ChartCard, Pie, MiniArea, MiniBar, MiniProgress, Field, +} from '../../components/Charts'; +import Trend from '../../components/Trend'; + +import styles from './Dashboard.less'; @connect(state => ({ dashboard: state.dashboard, })) export default class Dashboard extends PureComponent { render() { + const visitData = []; + const beginDay = new Date().getTime(); + + const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]; + for (let i = 0; i < fakeY.length; i += 1) { + visitData.push({ + x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), + y: fakeY[i], + }); + } + const databasePieData = [ + { + x: 'MySQL', + y: 10, + }, + { + x: 'Oracle', + y: 7, + }, + { + x: 'SQLServer', + y: 3, + }, + ]; + const tableColumns = [{ + title: 'Time', + dataIndex: 'time', + key: 'time', + }, { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, { + title: 'Duration', + dataIndex: 'duration', + key: 'duration', + }]; + + const slowServiceData = [{ + key: '1', + name: 'ServiceA', + time: '2017/12/11 19:22:32', + duration: '5000ms', + }, { + key: '1', + name: 'ServiceA', + time: '2017/12/11 19:22:32', + duration: '5000ms', + }, { + key: '1', + name: 'ServiceA', + time: '2017/12/11 19:22:32', + duration: '5000ms', + }, { + key: '1', + name: 'ServiceA', + time: '2017/12/11 19:22:32', + duration: '5000ms', + }, { + key: '1', + name: 'ServiceA', + time: '2017/12/11 19:22:32', + duration: '5000ms', + }]; + + const applicationThroughputColumns = [{ + title: 'Name', + dataIndex: 'name', + key: 'name', + }, { + title: 'Tps', + dataIndex: 'tps', + key: 'tps', + }]; + + const applicationThroughputData = [{ + key: '1', + name: 'App1', + tps: '500', + }, { + key: '1', + name: 'App1', + tps: '500', + }, { + key: '1', + name: 'App1', + tps: '500', + }, { + key: '1', + name: 'App1', + tps: '500', + }, { + key: '1', + name: 'App1', + tps: '500', + }]; + + const topColResponsiveProps = { + xs: 24, + sm: 12, + md: 12, + lg: 6, + xl: 6, + style: { marginBottom: 24 }, + }; + const middleColResponsiveProps = { + xs: 24, + sm: 24, + md: 24, + lg: 8, + xl: 8, + style: { marginBottom: 24, marginTop: 24 }, + }; return ( - <div>test</div> + <div> + <Row gutter={24}> + <Col {...topColResponsiveProps}> + <ChartCard + title="Total Application" + avatar={<img style={{ width: 56, height: 56 }} src="app.svg" alt="app" />} + action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>} + total={25} + /> + </Col> + <Col {...topColResponsiveProps}> + <ChartCard + title="Total Service" + avatar={<img style={{ width: 56, height: 56 }} src="service.svg" alt="service" />} + action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>} + total={525} + /> + </Col> + <Col {...topColResponsiveProps}> + <ChartCard + title="Total Database" + avatar={<img style={{ width: 56, height: 56 }} src="database.svg" alt="database" />} + action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>} + total={18} + /> + </Col> + <Col {...topColResponsiveProps}> + <ChartCard + title="Total Cache" + avatar={<img style={{ width: 56, height: 56 }} src="redis.svg" alt="redis" />} + action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>} + total={5} + /> + </Col> + </Row> + <Card + bordered={false} + bodyStyle={{ padding: 0 }} + > + <div Style="height: 400px">Topoloy</div> + </Card> + <Row gutter={24}> + <Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: 24 }}> + <ChartCard + title="Avg Application Alert" + avatar={<img style={{ width: 56, height: 56 }} src="alert.svg" alt="app" />} + action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>} + total="5%" + footer={<div><Field label="Max" value="10%" /> <Field label="Min" value="2%" /></div>} + > + <MiniArea + color="#D87093" + borderColor="#B22222" + line="true" + height={196} + data={visitData} + yAxis={{ + formatter(val) { + return `${val} %`; + }, + }} + /> + </ChartCard> + </Col> + </Row> + <Row gutter={24}> + <Col {...middleColResponsiveProps}> + <Card + bordered={false} + bodyStyle={{ padding: 0 }} + > + <Pie + hasLegend + title="Database" + subTitle="Total" + total={databasePieData.reduce((pre, now) => now.y + pre, 0)} + data={databasePieData} + height={300} + lineWidth={4} + /> + </Card> + </Col> + <Col {...middleColResponsiveProps}> + <Card + title="Slow Service" + bordered={false} + bodyStyle={{ padding: 0 }} + > + <Table + columns={tableColumns} + dataSource={slowServiceData} + pagination={{ + style: { marginBottom: 0 }, + pageSize: 5, + }} + /> + </Card> + </Col> + <Col {...middleColResponsiveProps}> + <Card + title="Application Throughput" + bordered={false} + bodyStyle={{ padding: 0 }} + > + <Table + columns={applicationThroughputColumns} + dataSource={applicationThroughputData} + /> + </Card> + </Col> + </Row> + </div> ); } } diff --git a/src/main/frontend/src/routes/Dashboard/Dashboard.less b/src/main/frontend/src/routes/Dashboard/Dashboard.less index e52dd30..8e13f67 100644 --- a/src/main/frontend/src/routes/Dashboard/Dashboard.less +++ b/src/main/frontend/src/routes/Dashboard/Dashboard.less @@ -21,3 +21,8 @@ height: auto; } } + +.trendText { + margin-left: 8px; + color: @heading-color; +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
