/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import * as zrUtil from 'zrender/src/core/util'; import ChartView from '../../view/Chart'; import * as graphic from '../../util/graphic'; import { setStatesStylesFromModel } from '../../util/states'; import Path, { PathProps } from 'zrender/src/graphic/Path'; import {createClipPath} from '../helper/createClipPathFromCoordSys'; import CandlestickSeriesModel, { CandlestickDataItemOption } from './CandlestickSeries'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { StageHandlerProgressParams } from '../../util/types'; import SeriesData from '../../data/SeriesData'; import {CandlestickItemLayout} from './candlestickLayout'; import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem'; import Model from '../../model/Model'; import { saveOldStyle } from '../../animation/basicTransition'; import Element from 'zrender/src/Element'; const SKIP_PROPS = ['color', 'borderColor'] as const; class CandlestickView extends ChartView { static readonly type = 'candlestick'; readonly type = CandlestickView.type; private _isLargeDraw: boolean; private _data: SeriesData; private _progressiveEls: Element[]; render(seriesModel: CandlestickSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { // If there is clipPath created in large mode. Remove it. this.group.removeClipPath(); // Clear previously rendered progressive elements. this._progressiveEls = null; this._updateDrawMode(seriesModel); this._isLargeDraw ? this._renderLarge(seriesModel) : this._renderNormal(seriesModel); } incrementalPrepareRender(seriesModel: CandlestickSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { this._clear(); this._updateDrawMode(seriesModel); } incrementalRender( params: StageHandlerProgressParams, seriesModel: CandlestickSeriesModel, ecModel: GlobalModel, api: ExtensionAPI ) { this._progressiveEls = []; this._isLargeDraw ? this._incrementalRenderLarge(params, seriesModel) : this._incrementalRenderNormal(params, seriesModel); } eachRendered(cb: (el: Element) => boolean | void) { graphic.traverseElements(this._progressiveEls || this.group, cb); } _updateDrawMode(seriesModel: CandlestickSeriesModel) { const isLargeDraw = seriesModel.pipelineContext.large; if (this._isLargeDraw == null || isLargeDraw !== this._isLargeDraw) { this._isLargeDraw = isLargeDraw; this._clear(); } } _renderNormal(seriesModel: CandlestickSeriesModel) { const data = seriesModel.getData(); const oldData = this._data; const group = this.group; const isSimpleBox = data.getLayout('isSimpleBox'); const needsClip = seriesModel.get('clip', true); const coord = seriesModel.coordinateSystem; const clipArea = coord.getArea && coord.getArea(); // There is no old data only when first rendering or switching from // stream mode to normal mode, where previous elements should be removed. if (!this._data) { group.removeAll(); } data.diff(oldData) .add(function (newIdx) { if (data.hasValue(newIdx)) { const itemLayout = data.getItemLayout(newIdx) as CandlestickItemLayout; if (needsClip && isNormalBoxClipped(clipArea, itemLayout)) { return; } const el = createNormalBox(itemLayout, newIdx, true); graphic.initProps(el, {shape: {points: itemLayout.ends}}, seriesModel, newIdx); setBoxCommon(el, data, newIdx, isSimpleBox); group.add(el); data.setItemGraphicEl(newIdx, el); } }) .update(function (newIdx, oldIdx) { let el = oldData.getItemGraphicEl(oldIdx) as NormalBoxPath; // Empty data if (!data.hasValue(newIdx)) { group.remove(el); return; } const itemLayout = data.getItemLayout(newIdx) as CandlestickItemLayout; if (needsClip && isNormalBoxClipped(clipArea, itemLayout)) { group.remove(el); return; } if (!el) { el = createNormalBox(itemLayout, newIdx); } else { graphic.updateProps(el, { shape: { points: itemLayout.ends } }, seriesModel, newIdx); saveOldStyle(el); } setBoxCommon(el, data, newIdx, isSimpleBox); group.add(el); data.setItemGraphicEl(newIdx, el); }) .remove(function (oldIdx) { const el = oldData.getItemGraphicEl(oldIdx); el && group.remove(el); }) .execute(); this._data = data; } _renderLarge(seriesModel: CandlestickSeriesModel) { this._clear(); createLarge(seriesModel, this.group); const clipPath = seriesModel.get('clip', true) ? createClipPath(seriesModel.coordinateSystem, false, seriesModel) : null; if (clipPath) { this.group.setClipPath(clipPath); } else { this.group.removeClipPath(); } } _incrementalRenderNormal(params: StageHandlerProgressParams, seriesModel: CandlestickSeriesModel) { const data = seriesModel.getData(); const isSimpleBox = data.getLayout('isSimpleBox'); let dataIndex; while ((dataIndex = params.next()) != null) { const itemLayout = data.getItemLayout(dataIndex) as CandlestickItemLayout; const el = createNormalBox(itemLayout, dataIndex); setBoxCommon(el, data, dataIndex, isSimpleBox); el.incremental = true; this.group.add(el); this._progressiveEls.push(el); } } _incrementalRenderLarge(params: StageHandlerProgressParams, seriesModel: CandlestickSeriesModel) { createLarge(seriesModel, this.group, this._progressiveEls, true); } remove(ecModel: GlobalModel) { this._clear(); } _clear() { this.group.removeAll(); this._data = null; } } class NormalBoxPathShape { points: number[][]; } interface NormalBoxPathProps extends PathProps { shape?: Partial } class NormalBoxPath extends Path { readonly type = 'normalCandlestickBox'; shape: NormalBoxPathShape; __simpleBox: boolean; constructor(opts?: NormalBoxPathProps) { super(opts); } getDefaultShape() { return new NormalBoxPathShape(); } buildPath(ctx: CanvasRenderingContext2D, shape: NormalBoxPathShape) { const ends = shape.points; if (this.__simpleBox) { ctx.moveTo(ends[4][0], ends[4][1]); ctx.lineTo(ends[6][0], ends[6][1]); } else { ctx.moveTo(ends[0][0], ends[0][1]); ctx.lineTo(ends[1][0], ends[1][1]); ctx.lineTo(ends[2][0], ends[2][1]); ctx.lineTo(ends[3][0], ends[3][1]); ctx.closePath(); ctx.moveTo(ends[4][0], ends[4][1]); ctx.lineTo(ends[5][0], ends[5][1]); ctx.moveTo(ends[6][0], ends[6][1]); ctx.lineTo(ends[7][0], ends[7][1]); } } } function createNormalBox(itemLayout: CandlestickItemLayout, dataIndex: number, isInit?: boolean) { const ends = itemLayout.ends; return new NormalBoxPath({ shape: { points: isInit ? transInit(ends, itemLayout) : ends }, z2: 100 }); } function isNormalBoxClipped(clipArea: CoordinateSystemClipArea, itemLayout: CandlestickItemLayout) { let clipped = true; for (let i = 0; i < itemLayout.ends.length; i++) { // If any point are in the region. if (clipArea.contain(itemLayout.ends[i][0], itemLayout.ends[i][1])) { clipped = false; break; } } return clipped; } function setBoxCommon(el: NormalBoxPath, data: SeriesData, dataIndex: number, isSimpleBox?: boolean) { const itemModel = data.getItemModel(dataIndex) as Model; el.useStyle(data.getItemVisual(dataIndex, 'style')); el.style.strokeNoScale = true; el.__simpleBox = isSimpleBox; setStatesStylesFromModel(el, itemModel); } function transInit(points: number[][], itemLayout: CandlestickItemLayout) { return zrUtil.map(points, function (point) { point = point.slice(); point[1] = itemLayout.initBaseline; return point; }); } class LargeBoxPathShape { points: ArrayLike; } interface LargeBoxPathProps extends PathProps { shape?: Partial __sign?: number } class LargeBoxPath extends Path { readonly type = 'largeCandlestickBox'; shape: LargeBoxPathShape; __sign: number; constructor(opts?: LargeBoxPathProps) { super(opts); } getDefaultShape() { return new LargeBoxPathShape(); } buildPath(ctx: CanvasRenderingContext2D, shape: LargeBoxPathShape) { // Drawing lines is more efficient than drawing // a whole line or drawing rects. const points = shape.points; for (let i = 0; i < points.length;) { if (this.__sign === points[i++]) { const x = points[i++]; ctx.moveTo(x, points[i++]); ctx.lineTo(x, points[i++]); } else { i += 3; } } } } function createLarge( seriesModel: CandlestickSeriesModel, group: graphic.Group, progressiveEls?: Element[], incremental?: boolean ) { const data = seriesModel.getData(); const largePoints = data.getLayout('largePoints'); const elP = new LargeBoxPath({ shape: {points: largePoints}, __sign: 1, ignoreCoarsePointer: true }); group.add(elP); const elN = new LargeBoxPath({ shape: {points: largePoints}, __sign: -1, ignoreCoarsePointer: true }); group.add(elN); const elDoji = new LargeBoxPath({ shape: {points: largePoints}, __sign: 0, ignoreCoarsePointer: true }); group.add(elDoji); setLargeStyle(1, elP, seriesModel, data); setLargeStyle(-1, elN, seriesModel, data); setLargeStyle(0, elDoji, seriesModel, data); if (incremental) { elP.incremental = true; elN.incremental = true; } if (progressiveEls) { progressiveEls.push(elP, elN); } } function setLargeStyle(sign: number, el: LargeBoxPath, seriesModel: CandlestickSeriesModel, data: SeriesData) { // TODO put in visual? let borderColor = seriesModel.get(['itemStyle', sign > 0 ? 'borderColor' : 'borderColor0']) // Use color for border color by default. || seriesModel.get(['itemStyle', sign > 0 ? 'color' : 'color0']); if (sign === 0) { borderColor = seriesModel.get(['itemStyle', 'borderColorDoji']); } // Color must be excluded. // Because symbol provide setColor individually to set fill and stroke const itemStyle = seriesModel.getModel('itemStyle').getItemStyle(SKIP_PROPS); el.useStyle(itemStyle); el.style.fill = null; el.style.stroke = borderColor; } export default CandlestickView;