blob: 3a8e971d78955090fb8f130f7d6ed49c456ff465 [file] [log] [blame]
/*
* 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';
import { saveOldStyle } from '../../animation/basicTrasition';
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);
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);
}
}
_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;