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/)
+
+| [
](http:// godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari | [
](http://godban.github.io/browsers-support-badges/)Opera | [
](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/)
+
+| [
](http:// godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari | [
](http://godban.github.io/browsers-support-badges/)Opera | [
](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);