| /* |
| * 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 List from '../../data/List'; |
| import {CandlestickItemLayout} from './candlestickLayout'; |
| import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem'; |
| import Model from '../../model/Model'; |
| |
| const SKIP_PROPS = ['color', 'borderColor'] as const; |
| |
| class CandlestickView extends ChartView { |
| |
| static readonly type = 'candlestick'; |
| readonly type = CandlestickView.type; |
| |
| private _isLargeDraw: boolean; |
| |
| private _data: List; |
| |
| render(seriesModel: CandlestickSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { |
| // If there is clipPath created in large mode. Remove it. |
| this.group.removeClipPath(); |
| |
| 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._isLargeDraw |
| ? this._incrementalRenderLarge(params, seriesModel) |
| : this._incrementalRenderNormal(params, seriesModel); |
| } |
| |
| _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); |
| } |
| |
| 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); |
| } |
| } |
| |
| _incrementalRenderLarge(params: StageHandlerProgressParams, seriesModel: CandlestickSeriesModel) { |
| createLarge(seriesModel, this.group, 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: List, 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, incremental?: boolean) { |
| const data = seriesModel.getData(); |
| const largePoints = data.getLayout('largePoints'); |
| |
| const elP = new LargeBoxPath({ |
| shape: {points: largePoints}, |
| __sign: 1 |
| }); |
| group.add(elP); |
| const elN = new LargeBoxPath({ |
| shape: {points: largePoints}, |
| __sign: -1 |
| }); |
| group.add(elN); |
| |
| setLargeStyle(1, elP, seriesModel, data); |
| setLargeStyle(-1, elN, seriesModel, data); |
| |
| if (incremental) { |
| elP.incremental = true; |
| elN.incremental = true; |
| } |
| } |
| |
| function setLargeStyle(sign: number, el: LargeBoxPath, seriesModel: CandlestickSeriesModel, data: List) { |
| // TODO put in visual? |
| const borderColor = seriesModel.get(['itemStyle', sign > 0 ? 'borderColor' : 'borderColor0']) |
| || seriesModel.get(['itemStyle', sign > 0 ? 'color' : 'color0']); |
| |
| // 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; |
| |