|  | /* | 
|  | * 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<NormalBoxPathShape> | 
|  | } | 
|  |  | 
|  | class NormalBoxPath extends Path<NormalBoxPathProps> { | 
|  |  | 
|  | 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<CandlestickDataItemOption>; | 
|  |  | 
|  | 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<number>; | 
|  | } | 
|  |  | 
|  | interface LargeBoxPathProps extends PathProps { | 
|  | shape?: Partial<LargeBoxPathShape> | 
|  | __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; |