| /* | |
| * 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; |