diff --git a/__tests__/unit/waterfall-spec.ts b/__tests__/unit/waterfall-spec.ts new file mode 100644 index 0000000000..32300a21c0 --- /dev/null +++ b/__tests__/unit/waterfall-spec.ts @@ -0,0 +1,170 @@ +import { Waterfall } from '../../src'; +import { Shape } from '@antv/g'; +import * as _ from '@antv/util'; + +describe('waterfall plot', () => { + const data = [ + { type: '日用品', money: 300 }, + { type: '伙食费', money: 900 }, + { type: '交通费', money: 200 }, + { type: '水电费', money: 300 }, + { type: '房租', money: 1200 }, + { type: '商场消费', money: 1000 }, + { type: '应酬交际', money: -2000 }, + ]; + + const plotOptions = { + title: { + visible: true, + text: '每月收支情况(瀑布图)', + }, + forceFit: true, + data, + padding: 'auto', + data, + xField: 'type', + yField: 'money', + meta: { + type: { + alias: '类别', + }, + money: { + alias: '金额', + }, + }, + }; + + const canvasDiv = document.createElement('div'); + canvasDiv.style.width = '600px'; + canvasDiv.style.height = '600px'; + canvasDiv.style.left = '30px'; + canvasDiv.style.top = '30px'; + canvasDiv.id = 'canvas1'; + document.body.appendChild(canvasDiv); + + const waterfallPlot = new Waterfall(canvasDiv, plotOptions); + const waterfallLayer = waterfallPlot.getLayer(); + + it('normal', () => { + waterfallPlot.render(); + const shapes = waterfallLayer.view.get('elements')[0].getShapes(); + expect(shapes.length).toBe(data.length * 2 + 1); + const lines = shapes.filter((s) => s.name === 'leader-line'); + expect(lines.length).toBe(data.length); + }); + + it('custom color, string', () => { + waterfallPlot.updateConfig({ + color: 'rgba(0, 255, 255, 0.2)', + }); + waterfallPlot.render(); + const shapes = waterfallLayer.view + .get('elements')[0] + .getShapes() + .filter((s) => s.name === 'interval'); + expect(_.every(shapes, (s: Shape) => s.attr('fill') === 'rgba(0, 255, 255, 0.2)')).toBe(true); + }); + + it('custom color, object', () => { + waterfallPlot.updateConfig({ + color: { + rising: 'red', + falling: 'green', + total: '#ddd', + }, + }); + waterfallPlot.render(); + const shapes = waterfallLayer.view + .get('elements')[0] + .getShapes() + .filter((s) => s.name === 'interval'); + expect(shapes[0].attr('fill')).toBe('red'); + expect(shapes[6].attr('fill')).toBe('green'); + expect(_.last(shapes).attr('fill')).toBe('#ddd'); + }); + + it('use callback to custom color', () => { + waterfallPlot.updateConfig({ + color: (type, value, values, index) => { + if (index === data.length) { + return '#ddd'; + } else if (value > 0) { + return 'rgba(255, 0, 0, 0.45)'; + } + return 'rgba(255, 255, 0, 0.45)'; + }, + }); + waterfallPlot.render(); + const shapes = waterfallLayer.view + .get('elements')[0] + .getShapes() + .filter((s) => s.name === 'interval'); + expect(shapes[0].attr('fill')).toBe('rgba(255, 0, 0, 0.45)'); + expect(shapes[6].attr('fill')).toBe('rgba(255, 255, 0, 0.45)'); + expect(_.last(shapes).attr('fill')).toBe('#ddd'); + }); + + it('not show total', () => { + waterfallPlot.updateConfig({ + showTotal: { + visible: false, + }, + }); + waterfallPlot.render(); + const shapes = waterfallLayer.view.get('elements')[0].getShapes(); + expect(shapes.length).toBe(data.length * 2 - 1); + const lines = shapes.filter((s) => s.name === 'leader-line'); + expect(lines.length).toBe(data.length - 1); + }); + + it('diff-label', () => { + waterfallPlot.updateConfig({ + diffLabel: { + visible: true, + style: { + fill: 'red', + }, + formatter: (text, item, index) => { + if (text.startsWith('+')) { + return `涨 ${text.substr(1)}`; + } else if (text.startsWith('-')) { + return `跌 ${text.substr(1)}`; + } + return text; + }, + }, + }); + waterfallPlot.render(); + const shapes = waterfallLayer.view.get('elements')[0].getShapes(); + // @ts-ignore + const diffLabel = waterfallLayer.diffLabel; + const labelShapes = diffLabel.container.get('children')[0].get('children'); + const intervals = shapes.filter((s) => s.name === 'interval'); + /** auto hide label that is overflowed */ + expect(labelShapes.length).toBe(intervals.length); + expect( + _.every( + intervals, + (shape: Shape, idx) => labelShapes[idx].attr('y') === (shape.getBBox().minY + shape.getBBox().maxY) / 2 + ) + ).toBe(true); + expect(labelShapes[0].attr('text')).toBe('涨 300'); + expect(labelShapes[0].attr('fill')).toBe('red'); + }); + + it('hidden diff-label', () => { + waterfallPlot.updateConfig({ + diffLabel: { + visible: false, + }, + }); + waterfallPlot.render(); + // @ts-ignore + const diffLabel = waterfallLayer.diffLabel; + expect(diffLabel).toBe(null); + }); + + afterAll(() => { + // waterfallPlot.destroy(); + }); +}); diff --git a/examples/column/waterfall/API.en.md b/examples/column/waterfall/API.en.md new file mode 100644 index 0000000000..8de89fa021 --- /dev/null +++ b/examples/column/waterfall/API.en.md @@ -0,0 +1,13 @@ +--- +title: API +--- + +API. + +- Modern browsers and Internet Explorer 9+ (with [polyfills](https:// ant.design/docs/react/getting-started#Compatibility)) +- Server-side Rendering +- [Electron](http:// electron.atom.io/) + +| [IE / Edge](http:// godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | [Electron](http://godban.github.io/browsers-support-badges/)
Electron | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| IE9, IE10, IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions | diff --git a/examples/column/waterfall/API.zh.md b/examples/column/waterfall/API.zh.md new file mode 100644 index 0000000000..a3bf78cbaa --- /dev/null +++ b/examples/column/waterfall/API.zh.md @@ -0,0 +1,13 @@ +--- +title: API +--- + +暂无。 + +- Modern browsers and Internet Explorer 9+ (with [polyfills](https:// ant.design/docs/react/getting-started#Compatibility)) +- Server-side Rendering +- [Electron](http:// electron.atom.io/) + +| [IE / Edge](http:// godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | [Electron](http://godban.github.io/browsers-support-badges/)
Electron | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| IE9, IE10, IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions | diff --git a/examples/column/waterfall/demo/basic.js b/examples/column/waterfall/demo/basic.js new file mode 100644 index 0000000000..060db3baa7 --- /dev/null +++ b/examples/column/waterfall/demo/basic.js @@ -0,0 +1,34 @@ +import { Waterfall } from '@antv/g2plot'; + +const data = [ + { type: '日用品', money: 120 }, + { type: '伙食费', money: 900 }, + { type: '交通费', money: 200 }, + { type: '水电费', money: 300 }, + { type: '房租', money: 1200 }, + { type: '商场消费', money: 1000 }, + { type: '应酬红包', money: -2000 }, +]; + +const waterfallPlot = new Waterfall(document.getElementById('container'), { + title: { + visible: true, + text: '每月收支情况(瀑布图)', + }, + forceFit: true, + data, + padding: 'auto', + data, + xField: 'type', + yField: 'money', + meta: { + type: { + alias: '类别', + }, + money: { + alias: '金额', + }, + }, +}); + +waterfallPlot.render(); diff --git a/examples/column/waterfall/demo/meta.json b/examples/column/waterfall/demo/meta.json new file mode 100644 index 0000000000..766d48b8fb --- /dev/null +++ b/examples/column/waterfall/demo/meta.json @@ -0,0 +1,13 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "basic.js", + "title": "基础瀑布图", + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*gTX1T6UddcYAAAAAAAAAAABkARQnAQ" + } + ] +} diff --git a/examples/column/waterfall/design.en.md b/examples/column/waterfall/design.en.md new file mode 100644 index 0000000000..6d7c247589 --- /dev/null +++ b/examples/column/waterfall/design.en.md @@ -0,0 +1,5 @@ +--- +title: 设计规范 +--- + +设计规范 diff --git a/examples/column/waterfall/design.zh.md b/examples/column/waterfall/design.zh.md new file mode 100644 index 0000000000..6d7c247589 --- /dev/null +++ b/examples/column/waterfall/design.zh.md @@ -0,0 +1,5 @@ +--- +title: 设计规范 +--- + +设计规范 diff --git a/examples/column/waterfall/index.en.md b/examples/column/waterfall/index.en.md new file mode 100644 index 0000000000..ea586e694e --- /dev/null +++ b/examples/column/waterfall/index.en.md @@ -0,0 +1,6 @@ +--- +title: Waterfall Chart +order: 5 +--- + +Description about this component. diff --git a/examples/column/waterfall/index.zh.md b/examples/column/waterfall/index.zh.md new file mode 100644 index 0000000000..d25c11f4dc --- /dev/null +++ b/examples/column/waterfall/index.zh.md @@ -0,0 +1,5 @@ +--- +title: 瀑布图 +order: 5 +--- + diff --git a/src/plots/index.ts b/src/plots/index.ts index 68287784fe..36b3a896e6 100644 --- a/src/plots/index.ts +++ b/src/plots/index.ts @@ -20,3 +20,4 @@ export { default as Area, AreaConfig } from './area'; export { default as StackArea, StackAreaConfig } from './stack-area'; export { default as PercentageStackArea, PercentageStackAreaConfig } from './percentage-stack-area'; export { default as Heatmap, HeatmapConfig } from './heatmap'; +export { default as Waterfall, WaterfallConfig } from './waterfall'; diff --git a/src/plots/waterfall/component/label/diff-label.ts b/src/plots/waterfall/component/label/diff-label.ts new file mode 100644 index 0000000000..ca88b3cff6 --- /dev/null +++ b/src/plots/waterfall/component/label/diff-label.ts @@ -0,0 +1,108 @@ +import { Group, BBox } from '@antv/g'; +import { View } from '@antv/g2'; +import * as _ from '@antv/util'; +import { VALUE_FIELD, IS_TOTAL } from '../../layer'; + +export interface DiffLabelcfg { + view: View; + fields: string[]; + formatter: (text: string, item: object, idx: number) => string; + style?: { + fill?: string; + stroke?: string; + strokeOpacity?: number; + [k: string]: any; + }; +} + +function getDefaultCfg() { + return { + fill: '#fff', + fontSize: 12, + lineHeight: 12, + stroke: 'rgba(0, 0, 0, 0.45)', + }; +} + +export default class DiffLabel { + private view: View; + private fields: string[]; + private container: Group; + private formatter: (text: string, item: object, idx: number) => string; + private textAttrs: object = {}; + + constructor(cfg: DiffLabelcfg) { + this.view = cfg.view; + this.fields = cfg.fields; + this.formatter = cfg.formatter; + this.textAttrs = _.mix(getDefaultCfg(), cfg.style); + + this._init(); + } + + /** 绘制辅助labels */ + public draw() { + if (!this.view || this.view.destroyed) { + return; + } + const data = _.clone(this.view.get('data')); + this.container = this.view.get('frontgroundGroup').addGroup(); + const shapes = this.view + .get('elements')[0] + .getShapes() + .filter((s) => s.name === 'interval'); + const labelsGroup = new Group(); + + _.each(shapes, (shape, idx) => { + if (!shape.get('origin')) return; + const _origin = shape.get('origin')._origin; + const shapeBox: BBox = shape.getBBox(); + const values = _origin[VALUE_FIELD]; + let diff = values; + if (_.isArray(values)) { + diff = values[1] - values[0]; + } + diff = diff > 0 ? `+${diff}` : diff; + /** is total, total do not need `+` sign */ + if (_origin[IS_TOTAL]) { + diff = values[0] - values[1]; + } + let formattedText = diff; + if (this.formatter) { + const color = shapes[idx].attr('fill'); + formattedText = this.formatter(`${diff}`, { _origin: data[idx], color }, idx); + } + const text = labelsGroup.addShape('text', { + attrs: { + text: formattedText, + textBaseline: 'middle', + textAlign: 'center', + x: (shapeBox.minX + shapeBox.maxX) / 2, + y: (shapeBox.minY + shapeBox.maxY) / 2, + ...this.textAttrs, + }, + }); + if (text.getBBox().height > shapeBox.height) { + text.set('visible', false); + } + }); + this.container.add(labelsGroup); + this.view.get('canvas').draw(); + } + + public clear() { + if (this.container) { + this.container.clear(); + } + } + + private _init() { + this.view.on('beforerender', () => { + this.clear(); + }); + + this.view.on('afterrender', () => { + this.draw(); + }); + } +} diff --git a/src/plots/waterfall/component/label/waterfall-label.ts b/src/plots/waterfall/component/label/waterfall-label.ts new file mode 100644 index 0000000000..f9b4619321 --- /dev/null +++ b/src/plots/waterfall/component/label/waterfall-label.ts @@ -0,0 +1,31 @@ +import { registerElementLabels, ElementLabels } from '@antv/g2'; +import * as _ from '@antv/util'; +import { ColumnLabels } from '../../../column/component/label/column-label'; +import { VALUE_FIELD } from '../../layer'; + +class WaterfallLabels extends ColumnLabels { + public adjustPosition(label, shape, item) { + const MARGIN = 2; + const shapeBox = shape.getBBox(); + const origin = label.get('origin'); + const yField = item.fields[0]; + const values = origin[VALUE_FIELD]; + const diff = origin[yField]; + const value = _.isArray(values) ? values[1] : values; + let yPos = (shapeBox.minY + shapeBox.maxY) / 2; + let textBaseline = 'bottom'; + + if (diff < 0) { + yPos = shapeBox.maxY + MARGIN; + textBaseline = 'top'; + } else { + yPos = shapeBox.minY - MARGIN; + } + + label.attr('y', yPos); + label.attr('text', value); + label.attr('textBaseline', textBaseline); + } +} + +registerElementLabels('waterfall', WaterfallLabels); diff --git a/src/plots/waterfall/event.ts b/src/plots/waterfall/event.ts new file mode 100644 index 0000000000..77e49d21e3 --- /dev/null +++ b/src/plots/waterfall/event.ts @@ -0,0 +1,4 @@ +/** + * @file events of waterfall chart is equal to column chart + */ +export { EVENT_MAP, onEvent } from '../column/event'; diff --git a/src/plots/waterfall/geometry/shape/waterfall.ts b/src/plots/waterfall/geometry/shape/waterfall.ts new file mode 100644 index 0000000000..ad7563bba2 --- /dev/null +++ b/src/plots/waterfall/geometry/shape/waterfall.ts @@ -0,0 +1,76 @@ +import { Global, registerShape } from '@antv/g2'; +import * as _ from '@antv/util'; + +function getRectPath(points) { + const path = []; + for (let i = 0; i < points.length; i++) { + const point = points[i]; + if (point) { + const action = i === 0 ? 'M' : 'L'; + path.push([action, point.x, point.y]); + } + } + const first = points[0]; + path.push(['L', first.x, first.y]); + path.push(['Z']); + return path; +} + +const ShapeUtil = { + addFillAttrs(attrs, cfg) { + if (cfg.color) { + attrs.fill = cfg.color; + } + if (_.isNumber(cfg.opacity)) { + attrs.opacity = attrs.fillOpacity = cfg.opacity; + } + }, +}; + +function getFillAttrs(cfg) { + const defaultAttrs = Global.theme.shape.interval; + const attrs = _.mix({}, defaultAttrs, cfg.style); + ShapeUtil.addFillAttrs(attrs, cfg); + if (cfg.color) { + attrs.stroke = attrs.stroke || cfg.color; + } + return attrs; +} + +// @ts-ignore +registerShape('interval', 'waterfall', { + draw(cfg, container: any) { + const fillAttrs = getFillAttrs(cfg); + let rectPath = getRectPath(cfg.points); + rectPath = this.parsePath(rectPath); + // 1. 区域 + const interval = container.addShape('path', { + attrs: _.mix(fillAttrs, { + path: rectPath, + }), + }); + const leaderLine = _.get(cfg.style, 'leaderLine'); + if (leaderLine && leaderLine.visible) { + const lineStyle = leaderLine.style || {}; + // 2. 虚线连线 + if (cfg.nextPoints) { + let linkPath = [ + ['M', cfg.points[2].x, cfg.points[2].y], + ['L', cfg.nextPoints[0].x, cfg.nextPoints[0].y], + ]; + linkPath = this.parsePath(linkPath); + const path = container.addShape('path', { + attrs: { + path: linkPath, + stroke: '#d3d3d3', + lineDash: [4, 2], + lineWidth: 1, + ...lineStyle, + }, + }); + path.name = 'leader-line'; + } + } + return interval; + }, +}); diff --git a/src/plots/waterfall/index.ts b/src/plots/waterfall/index.ts new file mode 100644 index 0000000000..28e742220f --- /dev/null +++ b/src/plots/waterfall/index.ts @@ -0,0 +1,15 @@ +import * as _ from '@antv/util'; +import BasePlot, { PlotConfig } from '../../base/plot'; +import WaterfallLayer, { WaterfallViewConfig } from './layer'; + +export interface WaterfallConfig extends WaterfallViewConfig, PlotConfig {} + +export default class Waterfall extends BasePlot { + public static getDefaultOptions: typeof WaterfallLayer.getDefaultOptions = WaterfallLayer.getDefaultOptions; + + public createLayers(props) { + const layerProps = _.deepMix({}, props); + layerProps.type = 'waterfall'; + super.createLayers(layerProps); + } +} diff --git a/src/plots/waterfall/layer.ts b/src/plots/waterfall/layer.ts new file mode 100644 index 0000000000..192d0a2f29 --- /dev/null +++ b/src/plots/waterfall/layer.ts @@ -0,0 +1,281 @@ +import * as _ from '@antv/util'; +import { registerPlotType } from '../../base/global'; +import './geometry/shape/waterfall'; +import { LayerConfig } from '../../base/layer'; +import { ElementOption, IStyleConfig, DataItem, Label } from '../../interface/config'; +import ViewLayer, { ViewConfig } from '../../base/view-layer'; +import { extractScale } from '../../util/scale'; +import { DataPointType } from '@antv/g2/lib/interface'; +import { AttributeCfg } from '@antv/attr'; +import { getComponent } from '../../components/factory'; +import * as EventParser from './event'; +import './component/label/waterfall-label'; +import DiffLabel, { DiffLabelcfg } from './component/label/diff-label'; + +interface WaterfallStyle {} + +const G2_GEOM_MAP = { + waterfall: 'interval', +}; + +const PLOT_GEOM_MAP = { + interval: 'waterfall', +}; + +export const VALUE_FIELD = '$$value$$'; +export const IS_TOTAL = '$$total$$'; +const INDEX_FIELD = '$$index$$'; + +export interface WaterfallViewConfig extends ViewConfig { + showTotal?: { + visible: boolean; + label: string; + }; + /** 差值label */ + diffLabel?: { + visible: boolean; + style?: DiffLabelcfg['style']; + formatter?: DiffLabelcfg['formatter']; + }; + leaderLine?: { + visible: boolean; + style?: { + stroke?: string; + lineWidth?: number; + lineDash?: number[]; + }; + }; + color?: + | string + | { rising: string; falling: string; total?: string } + | ((type: string, value: number | null, values: number | number[], index: number) => string); + waterfallStyle?: WaterfallStyle | ((...args: any[]) => WaterfallStyle); +} + +export interface WaterfallLayerConfig extends WaterfallViewConfig, LayerConfig {} + +export default class WaterfallLayer extends ViewLayer { + public waterfall; + public type: string = 'watarfall'; + public diffLabel; + + public static getDefaultOptions(): Partial { + return _.deepMix({}, super.getDefaultOptions(), { + legend: { + visible: false, + position: 'bottom', + }, + label: { + visible: true, + adjustPosition: true, + }, + /** 差值 label */ + diffLabel: { + visible: true, + }, + /** 迁移线 */ + leaderLine: { + visible: true, + }, + /** 显示总计 */ + showTotal: { + visible: true, + label: '总计值', + }, + waterfallStyle: { + /** 默认无描边 */ + lineWidth: 0, + }, + tooltip: { + visible: true, + shared: true, + crosshairs: { + type: 'rect', + }, + }, + }); + } + + public afterInit() { + super.afterInit(); + const options = this.options; + if (options.diffLabel && options.diffLabel.visible) { + this.diffLabel = new DiffLabel({ + view: this.view, + fields: [options.xField, options.yField, VALUE_FIELD], + formatter: options.diffLabel.formatter, + style: options.diffLabel.style, + }); + } else if (this.diffLabel) { + this.diffLabel.clear(); + this.diffLabel = null; + } + } + + public afterRender() { + super.afterRender(); + const options = this.options; + this.view.on('tooltip:change', (e) => { + const { items } = e; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const _origin = _.get(item.point, '_origin', {}); + // 改变 tooltip 显示的name和value + item.name = _origin[options.xField]; + item.value = _origin[options.yField]; + if (!item.value && _origin[IS_TOTAL]) { + const values = _origin[VALUE_FIELD]; + item.value = values[0] - values[1]; + } + e.items[i] = item; + } + }); + } + + protected geometryParser(dim, type) { + if (dim === 'g2') { + return G2_GEOM_MAP[type]; + } + return PLOT_GEOM_MAP[type]; + } + + protected addGeometry() { + const options = this.options; + const waterfall: ElementOption = { + type: 'interval', + position: { + fields: [options.xField, VALUE_FIELD], + }, + shape: { + values: ['waterfall'], + }, + }; + if (options.label) { + waterfall.label = this.extractLabel(); + } + waterfall.style = this._parseStyle(); + waterfall.color = this._parseColor(); + this.waterfall = waterfall; + this.setConfig('element', waterfall); + } + + protected processData(originData?: DataItem[]) { + const plotData = []; + const xField = this.options.xField; + const yField = this.options.yField; + _.map(originData, (dataItem, idx: number) => { + let value: any = dataItem[yField]; + if (idx > 0) { + const prevValue = plotData[idx - 1][VALUE_FIELD]; + if (_.isArray(prevValue)) { + value = [prevValue[1], dataItem[yField] + prevValue[1]]; + } else { + value = [prevValue, dataItem[yField] + prevValue]; + } + } + plotData.push({ + ...dataItem, + [VALUE_FIELD]: value, + [INDEX_FIELD]: idx, + }); + }); + if (this.options.showTotal && this.options.showTotal.visible) { + const values = _.map(originData, (o) => o[yField]); + const totalValue = _.reduce(values, (p: number, n: number) => p + n, 0); + plotData.push({ + [xField]: this.options.showTotal.label, + [yField]: null, + [VALUE_FIELD]: [totalValue, 0], + [INDEX_FIELD]: plotData.length, + [IS_TOTAL]: true, + }); + } + return plotData; + } + + protected scale() { + const { options } = this; + const scales = {}; + /** 配置x-scale */ + scales[options.xField] = { type: 'cat' }; + if (_.has(options, 'xAxis')) { + extractScale(scales[options.xField], options.xAxis); + } + /** 配置y-scale */ + scales[options.yField] = {}; + if (_.has(options, 'yAxis')) { + extractScale(scales[options.yField], options.yAxis); + } + this.setConfig('scales', scales); + super.scale(); + } + + protected coord() {} + + protected parseEvents(eventParser) { + super.parseEvents(EventParser); + } + + protected extractLabel() { + const options = this.options; + const label = _.deepMix({}, options.label as Label); + if (label.visible === false) { + return false; + } + const labelConfig = getComponent('label', { + plot: this, + labelType: 'waterfall', + fields: [options.yField], + ...label, + }); + return labelConfig; + } + + /** 牵引线的样式注入到style中 */ + private _parseStyle(): IStyleConfig { + const style = this.options.waterfallStyle; + const leaderLine = this.options.leaderLine; + const config: DataPointType = {}; + if (_.isFunction(style)) { + config.callback = (...args) => { + return Object.assign({}, style(...args), { leaderLine }); + }; + } else { + config.cfg = { ...style, leaderLine }; + } + + return config; + } + + private _parseColor(): AttributeCfg { + const options = this.options; + const { xField, yField } = this.options; + const config: any = { + fields: [xField, yField, VALUE_FIELD, INDEX_FIELD], + }; + if (_.isFunction(options.color)) { + config.callback = options.color; + } else { + let risingColor = '#f4664a'; + let fallingColor = '#30bf78'; + let totalColor = 'rgba(0, 0, 0, 0.25)'; + if (_.isString(options.color)) { + risingColor = fallingColor = totalColor = options.color; + } else if (_.isObject(options.color)) { + const { rising, falling, total } = options.color; + risingColor = rising; + fallingColor = falling; + totalColor = total; + } + config.callback = (type, value, values: number | number[], index: number) => { + if (index === this.options.data.length) { + return totalColor || (values[0] >= 0 ? risingColor : fallingColor); + } + return (_.isArray(values) ? values[1] - values[0] : values) >= 0 ? risingColor : fallingColor; + }; + } + return config as AttributeCfg; + } +} + +registerPlotType('waterfall', WaterfallLayer);