blob: 3e5c4c933d23b576b3a03e0eefd18946d8abd814 [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 Path, {PathProps} from 'zrender/src/graphic/Path';
import Group from 'zrender/src/graphic/Group';
import {extend, map, defaults, each} from 'zrender/src/core/util';
import {
Rect,
Sector,
updateProps,
initProps,
removeElementWithFadeOut
} from '../../util/graphic';
import { getECData } from '../../util/innerStore';
import { enableHoverEmphasis, setStatesStylesFromModel } from '../../util/states';
import { setLabelStyle, getLabelStatesModels, setLabelValueAnimation } from '../../label/labelStyle';
import {throttle} from '../../util/throttle';
import {createClipPath} from '../helper/createClipPathFromCoordSys';
import Sausage from '../../util/shape/sausage';
import ChartView from '../../view/Chart';
import List, {DefaultDataVisual} from '../../data/List';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../core/ExtensionAPI';
import {
StageHandlerProgressParams,
ZRElementEvent,
ColorString,
OrdinalSortInfo,
Payload,
OrdinalNumber,
ParsedValue
} from '../../util/types';
import BarSeriesModel, {BarSeriesOption, BarDataItemOption} from './BarSeries';
import type Axis2D from '../../coord/cartesian/Axis2D';
import type Cartesian2D from '../../coord/cartesian/Cartesian2D';
import type Polar from '../../coord/polar/Polar';
import type Model from '../../model/Model';
import { isCoordinateSystemType } from '../../coord/CoordinateSystem';
import { getDefaultLabel, getDefaultInterpolatedLabel } from '../helper/labelHelper';
import OrdinalScale from '../../scale/Ordinal';
import SeriesModel from '../../model/Series';
import {AngleAxisModel, RadiusAxisModel} from '../../coord/polar/AxisModel';
import CartesianAxisModel from '../../coord/cartesian/AxisModel';
import {LayoutRect} from '../../util/layout';
import {EventCallback} from 'zrender/src/core/Eventful';
const BAR_BORDER_WIDTH_QUERY = ['itemStyle', 'borderWidth'] as const;
const BAR_BORDER_RADIUS_QUERY = ['itemStyle', 'borderRadius'] as const;
const _eventPos = [0, 0];
const mathMax = Math.max;
const mathMin = Math.min;
type CoordSysOfBar = BarSeriesModel['coordinateSystem'];
type RectShape = Rect['shape'];
type SectorShape = Sector['shape'];
type SectorLayout = SectorShape;
type RectLayout = RectShape;
type BarPossiblePath = Sector | Rect | Sausage;
type CartesianCoordArea = ReturnType<Cartesian2D['getArea']>;
type PolarCoordArea = ReturnType<Polar['getArea']>;
function getClipArea(coord: CoordSysOfBar, data: List) {
const coordSysClipArea = coord.getArea && coord.getArea();
if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
const baseAxis = coord.getBaseAxis();
// When boundaryGap is false or using time axis. bar may exceed the grid.
// We should not clip this part.
// See test/bar2.html
if (baseAxis.type !== 'category' || !baseAxis.onBand) {
const expandWidth = data.getLayout('bandWidth');
if (baseAxis.isHorizontal()) {
(coordSysClipArea as CartesianCoordArea).x -= expandWidth;
(coordSysClipArea as CartesianCoordArea).width += expandWidth * 2;
}
else {
(coordSysClipArea as CartesianCoordArea).y -= expandWidth;
(coordSysClipArea as CartesianCoordArea).height += expandWidth * 2;
}
}
}
return coordSysClipArea as PolarCoordArea | CartesianCoordArea;
}
class BarView extends ChartView {
static type = 'bar' as const;
type = BarView.type;
private _data: List;
private _isLargeDraw: boolean;
private _isFirstFrame: boolean; // First frame after series added
private _onRendered: EventCallback<unknown, unknown>;
private _backgroundGroup: Group;
private _backgroundEls: (Rect | Sector)[];
private _model: BarSeriesModel;
constructor() {
super();
this._isFirstFrame = true;
}
render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload) {
this._model = seriesModel;
this.removeOnRenderedListener(api);
this._updateDrawMode(seriesModel);
const coordinateSystemType = seriesModel.get('coordinateSystem');
if (coordinateSystemType === 'cartesian2d'
|| coordinateSystemType === 'polar'
) {
this._isLargeDraw
? this._renderLarge(seriesModel, ecModel, api)
: this._renderNormal(seriesModel, ecModel, api, payload);
}
else if (__DEV__) {
console.warn('Only cartesian2d and polar supported for bar.');
}
}
incrementalPrepareRender(seriesModel: BarSeriesModel): void {
this._clear();
this._updateDrawMode(seriesModel);
// incremental also need to clip, otherwise might be overlow.
// But must not set clip in each frame, otherwise all of the children will be marked redraw.
this._updateLargeClip(seriesModel);
}
incrementalRender(params: StageHandlerProgressParams, seriesModel: BarSeriesModel): void {
// Do not support progressive in normal mode.
this._incrementalRenderLarge(params, seriesModel);
}
private _updateDrawMode(seriesModel: BarSeriesModel): void {
const isLargeDraw = seriesModel.pipelineContext.large;
if (this._isLargeDraw == null || isLargeDraw !== this._isLargeDraw) {
this._isLargeDraw = isLargeDraw;
this._clear();
}
}
private _renderNormal(
seriesModel: BarSeriesModel,
ecModel: GlobalModel,
api: ExtensionAPI,
payload: Payload
): void {
const group = this.group;
const data = seriesModel.getData();
const oldData = this._data;
const coord = seriesModel.coordinateSystem;
const baseAxis = coord.getBaseAxis();
let isHorizontalOrRadial: boolean;
if (coord.type === 'cartesian2d') {
isHorizontalOrRadial = (baseAxis as Axis2D).isHorizontal();
}
else if (coord.type === 'polar') {
isHorizontalOrRadial = baseAxis.dim === 'angle';
}
const animationModel = seriesModel.isAnimationEnabled() ? seriesModel : null;
const axis2DModel = (baseAxis as Axis2D).model;
const realtimeSort = seriesModel.get('realtimeSort');
// If no data in the first frame, wait for data to initSort
if (realtimeSort && data.count()) {
if (this._isFirstFrame) {
this._initSort(data, isHorizontalOrRadial, baseAxis as Axis2D, api);
this._isFirstFrame = false;
return;
}
else {
this._onRendered = () => {
const orderMap = (idx: number) => {
const el = (data.getItemGraphicEl(idx) as Rect);
if (el) {
const shape = el.shape;
// If data is NaN, shape.xxx may be NaN, so use || 0 here in case
return (isHorizontalOrRadial ? shape.y + shape.height : shape.x + shape.width) || 0;
}
else {
return 0;
}
};
this._updateSort(data, orderMap, baseAxis as Axis2D, api);
};
api.getZr().on('rendered', this._onRendered as any);
}
}
const needsClip = seriesModel.get('clip', true) || realtimeSort;
const coordSysClipArea = getClipArea(coord, data);
// If there is clipPath created in large mode. Remove it.
group.removeClipPath();
// We don't use clipPath in normal mode because we needs a perfect animation
// And don't want the label are clipped.
const roundCap = seriesModel.get('roundCap', true);
const drawBackground = seriesModel.get('showBackground', true);
const backgroundModel = seriesModel.getModel('backgroundStyle');
const barBorderRadius = backgroundModel.get('borderRadius') || 0;
const bgEls: BarView['_backgroundEls'] = [];
const oldBgEls = this._backgroundEls;
const isInitSort = payload && payload.isInitSort;
const isChangeOrder = payload && payload.type === 'changeAxisOrder';
function createBackground(dataIndex: number) {
const bgLayout = getLayout[coord.type](data, dataIndex);
const bgEl = createBackgroundEl(coord, isHorizontalOrRadial, bgLayout);
bgEl.useStyle(backgroundModel.getItemStyle());
// Only cartesian2d support borderRadius.
if (coord.type === 'cartesian2d') {
(bgEl as Rect).setShape('r', barBorderRadius);
}
bgEls[dataIndex] = bgEl;
return bgEl;
};
data.diff(oldData)
.add(function (dataIndex) {
const itemModel = data.getItemModel<BarDataItemOption>(dataIndex);
const layout = getLayout[coord.type](data, dataIndex, itemModel);
if (drawBackground) {
createBackground(dataIndex);
}
// If dataZoom in filteMode: 'empty', the baseValue can be set as NaN in "axisProxy".
if (!data.hasValue(dataIndex)) {
return;
}
let isClipped = false;
if (needsClip) {
// Clip will modify the layout params.
// And return a boolean to determine if the shape are fully clipped.
isClipped = clip[coord.type](coordSysClipArea, layout);
}
const el = elementCreator[coord.type](
seriesModel,
data,
dataIndex,
layout,
isHorizontalOrRadial,
animationModel,
baseAxis.model,
false,
roundCap
);
updateStyle(
el, data, dataIndex, itemModel, layout,
seriesModel, isHorizontalOrRadial, coord.type === 'polar'
);
if (isInitSort) {
(el as Rect).attr({ shape: layout });
}
else if (realtimeSort) {
updateRealtimeAnimation(
seriesModel,
axis2DModel,
animationModel,
el as Rect,
layout as LayoutRect,
dataIndex,
isHorizontalOrRadial,
false,
false
);
}
else {
initProps(el, {shape: layout} as any, seriesModel, dataIndex);
}
data.setItemGraphicEl(dataIndex, el);
group.add(el);
el.ignore = isClipped;
})
.update(function (newIndex, oldIndex) {
const itemModel = data.getItemModel<BarDataItemOption>(newIndex);
const layout = getLayout[coord.type](data, newIndex, itemModel);
if (drawBackground) {
let bgEl: Rect | Sector;
if (oldBgEls.length === 0) {
bgEl = createBackground(oldIndex);
}
else {
bgEl = oldBgEls[oldIndex];
bgEl.useStyle(backgroundModel.getItemStyle());
// Only cartesian2d support borderRadius.
if (coord.type === 'cartesian2d') {
(bgEl as Rect).setShape('r', barBorderRadius);
}
bgEls[newIndex] = bgEl;
}
const bgLayout = getLayout[coord.type](data, newIndex);
const shape = createBackgroundShape(isHorizontalOrRadial, bgLayout, coord);
updateProps(bgEl, { shape: shape }, animationModel, newIndex);
}
let el = oldData.getItemGraphicEl(oldIndex) as BarPossiblePath;
if (!data.hasValue(newIndex)) {
group.remove(el);
el = null;
return;
}
let isClipped = false;
if (needsClip) {
isClipped = clip[coord.type](coordSysClipArea, layout);
if (isClipped) {
group.remove(el);
}
}
if (!el) {
el = elementCreator[coord.type](
seriesModel,
data,
newIndex,
layout,
isHorizontalOrRadial,
animationModel,
baseAxis.model,
!!el,
roundCap
);
}
// Not change anything if only order changed.
// Especially not change label.
if (!isChangeOrder) {
updateStyle(
el, data, newIndex, itemModel, layout,
seriesModel, isHorizontalOrRadial, coord.type === 'polar'
);
}
if (isInitSort) {
(el as Rect).attr({ shape: layout });
}
else if (realtimeSort) {
updateRealtimeAnimation(
seriesModel,
axis2DModel,
animationModel,
el as Rect,
layout as LayoutRect,
newIndex,
isHorizontalOrRadial,
true,
isChangeOrder
);
}
else {
updateProps(el, {
shape: layout
} as any, seriesModel, newIndex, null);
}
data.setItemGraphicEl(newIndex, el);
el.ignore = isClipped;
group.add(el);
})
.remove(function (dataIndex) {
const el = oldData.getItemGraphicEl(dataIndex) as Path;
el && removeElementWithFadeOut(el, seriesModel, dataIndex);
})
.execute();
const bgGroup = this._backgroundGroup || (this._backgroundGroup = new Group());
bgGroup.removeAll();
for (let i = 0; i < bgEls.length; ++i) {
bgGroup.add(bgEls[i]);
}
group.add(bgGroup);
this._backgroundEls = bgEls;
this._data = data;
}
private _renderLarge(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI): void {
this._clear();
createLarge(seriesModel, this.group);
this._updateLargeClip(seriesModel);
}
private _incrementalRenderLarge(params: StageHandlerProgressParams, seriesModel: BarSeriesModel): void {
this._removeBackground();
createLarge(seriesModel, this.group, true);
}
private _updateLargeClip(seriesModel: BarSeriesModel): void {
// Use clipPath in large mode.
const clipPath = seriesModel.get('clip', true)
? createClipPath(seriesModel.coordinateSystem, false, seriesModel)
: null;
if (clipPath) {
this.group.setClipPath(clipPath);
}
else {
this.group.removeClipPath();
}
}
_dataSort(
data: List<BarSeriesModel, DefaultDataVisual>,
idxMap: ((idx: number) => number)
): OrdinalSortInfo[] {
type SortValueInfo = {
mappedValue: number,
ordinalNumber: OrdinalNumber,
beforeSortIndex: number
};
const info: SortValueInfo[] = [];
data.each(idx => {
info.push({
mappedValue: idxMap(idx),
ordinalNumber: idx,
beforeSortIndex: null
});
});
info.sort((a, b) => {
return b.mappedValue - a.mappedValue;
});
// Update beforeSortIndex
for (let i = 0; i < info.length; ++i) {
info[info[i].ordinalNumber].beforeSortIndex = i;
}
return map(info, item => {
return {
ordinalNumber: item.ordinalNumber,
beforeSortIndex: item.beforeSortIndex
};
});
}
_isDataOrderChanged(
data: List<BarSeriesModel, DefaultDataVisual>,
orderMap: ((idx: number) => number),
oldOrder: OrdinalSortInfo[]
): boolean {
const oldCount = oldOrder ? oldOrder.length : 0;
if (oldCount !== data.count()) {
return true;
}
let lastValue = Number.MAX_VALUE;
for (let i = 0; i < oldOrder.length; ++i) {
const value = orderMap(oldOrder[i] && oldOrder[i].ordinalNumber);
if (value > lastValue) {
return true;
}
lastValue = value;
}
return false;
}
_updateSort(
data: List<BarSeriesModel, DefaultDataVisual>,
orderMap: ((idx: number) => number),
baseAxis: Axis2D,
api: ExtensionAPI
) {
const oldOrder = (baseAxis.scale as OrdinalScale).getCategorySortInfo();
const isOrderChanged = this._isDataOrderChanged(data, orderMap, oldOrder);
if (isOrderChanged) {
const newOrder = this._dataSort(data, orderMap);
const extent = baseAxis.scale.getExtent();
for (let i = extent[0]; i < extent[1]; ++i) {
/**
* Consider the case when A and B changed order, whose representing
* bars are both out of sight, we don't wish to trigger reorder action
* as long as the order in the view doesn't change.
*/
if (!oldOrder || !oldOrder[i] || oldOrder[i].ordinalNumber !== newOrder[i].ordinalNumber) {
this.removeOnRenderedListener(api);
const action = {
type: 'changeAxisOrder',
componentType: baseAxis.dim + 'Axis',
axisId: baseAxis.index,
sortInfo: newOrder
} as Payload;
api.dispatchAction(action);
break;
}
}
}
}
_initSort(
data: List<BarSeriesModel, DefaultDataVisual>,
isHorizontal: boolean,
baseAxis: Axis2D,
api: ExtensionAPI
) {
const action = {
type: 'changeAxisOrder',
componentType: baseAxis.dim + 'Axis',
isInitSort: true,
axisId: baseAxis.index,
sortInfo: this._dataSort(
data,
idx => parseFloat(data.get(isHorizontal ? 'y' : 'x', idx) as string) || 0
)
} as Payload;
api.dispatchAction(action);
}
remove(ecModel: GlobalModel, api: ExtensionAPI) {
this._clear(this._model);
this.removeOnRenderedListener(api);
}
dispose(ecModel: GlobalModel, api: ExtensionAPI) {
this.removeOnRenderedListener(api);
}
removeOnRenderedListener(api: ExtensionAPI) {
if (this._onRendered) {
api.getZr().off('rendered', this._onRendered);
this._onRendered = null;
}
}
private _clear(model?: SeriesModel): void {
const group = this.group;
const data = this._data;
if (model && model.isAnimationEnabled() && data && !this._isLargeDraw) {
this._removeBackground();
this._backgroundEls = [];
data.eachItemGraphicEl(function (el: Path) {
removeElementWithFadeOut(el, model, getECData(el).dataIndex);
});
}
else {
group.removeAll();
}
this._data = null;
this._isFirstFrame = true;
}
private _removeBackground(): void {
this.group.remove(this._backgroundGroup);
this._backgroundGroup = null;
}
}
interface Clipper {
(coordSysBoundingRect: PolarCoordArea | CartesianCoordArea, layout: RectLayout | SectorLayout): boolean
}
const clip: {
[key in 'cartesian2d' | 'polar']: Clipper
} = {
cartesian2d(coordSysBoundingRect: CartesianCoordArea, layout: Rect['shape']) {
const signWidth = layout.width < 0 ? -1 : 1;
const signHeight = layout.height < 0 ? -1 : 1;
// Needs positive width and height
if (signWidth < 0) {
layout.x += layout.width;
layout.width = -layout.width;
}
if (signHeight < 0) {
layout.y += layout.height;
layout.height = -layout.height;
}
const x = mathMax(layout.x, coordSysBoundingRect.x);
const x2 = mathMin(layout.x + layout.width, coordSysBoundingRect.x + coordSysBoundingRect.width);
const y = mathMax(layout.y, coordSysBoundingRect.y);
const y2 = mathMin(layout.y + layout.height, coordSysBoundingRect.y + coordSysBoundingRect.height);
layout.x = x;
layout.y = y;
layout.width = x2 - x;
layout.height = y2 - y;
const clipped = layout.width < 0 || layout.height < 0;
// Reverse back
if (signWidth < 0) {
layout.x += layout.width;
layout.width = -layout.width;
}
if (signHeight < 0) {
layout.y += layout.height;
layout.height = -layout.height;
}
return clipped;
},
polar(coordSysClipArea: PolarCoordArea, layout: Sector['shape']) {
const signR = layout.r0 <= layout.r ? 1 : -1;
// Make sure r is larger than r0
if (signR < 0) {
const tmp = layout.r;
layout.r = layout.r0;
layout.r0 = tmp;
}
const r = mathMin(layout.r, coordSysClipArea.r);
const r0 = mathMax(layout.r0, coordSysClipArea.r0);
layout.r = r;
layout.r0 = r0;
const clipped = r - r0 < 0;
// Reverse back
if (signR < 0) {
const tmp = layout.r;
layout.r = layout.r0;
layout.r0 = tmp;
}
return clipped;
}
};
interface ElementCreator {
(
seriesModel: BarSeriesModel, data: List, newIndex: number,
layout: RectLayout | SectorLayout, isHorizontalOrRadial: boolean,
animationModel: BarSeriesModel,
axisModel: CartesianAxisModel | AngleAxisModel | RadiusAxisModel,
isUpdate: boolean,
roundCap?: boolean
): BarPossiblePath
}
const elementCreator: {
[key in 'polar' | 'cartesian2d']: ElementCreator
} = {
cartesian2d(
seriesModel, data, newIndex, layout: RectLayout, isHorizontal,
animationModel, axisModel, isUpdate, roundCap
) {
const rect = new Rect({
shape: extend({}, layout),
z2: 1
});
(rect as any).__dataIndex = newIndex;
rect.name = 'item';
if (animationModel) {
const rectShape = rect.shape;
const animateProperty = isHorizontal ? 'height' : 'width' as 'width' | 'height';
rectShape[animateProperty] = 0;
}
return rect;
},
polar(
seriesModel, data, newIndex, layout: SectorLayout, isRadial: boolean,
animationModel, axisModel, isUpdate, roundCap
) {
// Keep the same logic with bar in catesion: use end value to control
// direction. Notice that if clockwise is true (by default), the sector
// will always draw clockwisely, no matter whether endAngle is greater
// or less than startAngle.
const clockwise = layout.startAngle < layout.endAngle;
const ShapeClass = (!isRadial && roundCap) ? Sausage : Sector;
const sector = new ShapeClass({
shape: defaults({clockwise: clockwise}, layout),
z2: 1
});
sector.name = 'item';
// Animation
if (animationModel) {
const sectorShape = sector.shape;
const animateProperty = isRadial ? 'r' : 'endAngle' as 'r' | 'endAngle';
const animateTarget = {} as SectorShape;
sectorShape[animateProperty] = isRadial ? 0 : layout.startAngle;
animateTarget[animateProperty] = layout[animateProperty];
(isUpdate ? updateProps : initProps)(sector, {
shape: animateTarget
// __value: typeof dataValue === 'string' ? parseInt(dataValue, 10) : dataValue
}, animationModel);
}
return sector;
}
};
function updateRealtimeAnimation(
seriesModel: BarSeriesModel,
axisModel: CartesianAxisModel,
animationModel: BarSeriesModel,
el: Rect,
layout: LayoutRect,
newIndex: number,
isHorizontal: boolean,
isUpdate: boolean,
isChangeOrder: boolean
) {
// Animation
if (animationModel || axisModel) {
let seriesTarget;
let axisTarget;
if (isHorizontal) {
axisTarget = {
x: layout.x,
width: layout.width
};
seriesTarget = {
y: layout.y,
height: layout.height
};
}
else {
axisTarget = {
y: layout.y,
height: layout.height
};
seriesTarget = {
x: layout.x,
width: layout.width
};
}
if (!isChangeOrder) {
// Keep the original growth animation if only axis order changed.
// Not start a new animation.
(isUpdate ? updateProps : initProps)(el, {
shape: seriesTarget
}, seriesModel, newIndex, null);
}
(isUpdate ? updateProps : initProps)(el, {
shape: axisTarget
}, axisModel, newIndex);
}
}
interface GetLayout {
(data: List, dataIndex: number, itemModel?: Model<BarDataItemOption>): RectLayout | SectorLayout
}
const getLayout: {
[key in 'cartesian2d' | 'polar']: GetLayout
} = {
// itemModel is only used to get borderWidth, which is not needed
// when calculating bar background layout.
cartesian2d(data, dataIndex, itemModel?): RectLayout {
const layout = data.getItemLayout(dataIndex) as RectLayout;
const fixedLineWidth = itemModel ? getLineWidth(itemModel, layout) : 0;
// fix layout with lineWidth
const signX = layout.width > 0 ? 1 : -1;
const signY = layout.height > 0 ? 1 : -1;
return {
x: layout.x + signX * fixedLineWidth / 2,
y: layout.y + signY * fixedLineWidth / 2,
width: layout.width - signX * fixedLineWidth,
height: layout.height - signY * fixedLineWidth
};
},
polar(data, dataIndex, itemModel?): SectorLayout {
const layout = data.getItemLayout(dataIndex);
return {
cx: layout.cx,
cy: layout.cy,
r0: layout.r0,
r: layout.r,
startAngle: layout.startAngle,
endAngle: layout.endAngle
} as SectorLayout;
}
};
function isZeroOnPolar(layout: SectorLayout) {
return layout.startAngle != null
&& layout.endAngle != null
&& layout.startAngle === layout.endAngle;
}
function updateStyle(
el: BarPossiblePath,
data: List, dataIndex: number,
itemModel: Model<BarDataItemOption>,
layout: RectLayout | SectorLayout,
seriesModel: BarSeriesModel,
isHorizontal: boolean,
isPolar: boolean
) {
const style = data.getItemVisual(dataIndex, 'style');
if (!isPolar) {
(el as Rect).setShape('r', itemModel.get(BAR_BORDER_RADIUS_QUERY) || 0);
}
el.useStyle(style);
const cursorStyle = itemModel.getShallow('cursor');
cursorStyle && (el as Path).attr('cursor', cursorStyle);
if (!isPolar) {
const labelPositionOutside = isHorizontal
? ((layout as RectLayout).height > 0 ? 'bottom' as const : 'top' as const)
: ((layout as RectLayout).width > 0 ? 'left' as const : 'right' as const);
const labelStatesModels = getLabelStatesModels(itemModel);
setLabelStyle(
el, labelStatesModels,
{
labelFetcher: seriesModel,
labelDataIndex: dataIndex,
defaultText: getDefaultLabel(seriesModel.getData(), dataIndex),
inheritColor: style.fill as ColorString,
defaultOpacity: style.opacity,
defaultOutsidePosition: labelPositionOutside
}
);
const label = el.getTextContent();
setLabelValueAnimation(
label,
labelStatesModels,
seriesModel.getRawValue(dataIndex) as ParsedValue,
(value: number) => getDefaultInterpolatedLabel(data, value)
);
}
const emphasisModel = itemModel.getModel(['emphasis']);
enableHoverEmphasis(el, emphasisModel.get('focus'), emphasisModel.get('blurScope'));
setStatesStylesFromModel(el, itemModel);
if (isZeroOnPolar(layout as SectorLayout)) {
el.style.fill = 'none';
el.style.stroke = 'none';
each(el.states, (state) => {
if (state.style) {
state.style.fill = state.style.stroke = 'none';
}
});
}
}
// In case width or height are too small.
function getLineWidth(
itemModel: Model<BarDataItemOption>,
rawLayout: RectLayout
) {
const lineWidth = itemModel.get(BAR_BORDER_WIDTH_QUERY) || 0;
// width or height may be NaN for empty data
const width = isNaN(rawLayout.width) ? Number.MAX_VALUE : Math.abs(rawLayout.width);
const height = isNaN(rawLayout.height) ? Number.MAX_VALUE : Math.abs(rawLayout.height);
return Math.min(lineWidth, width, height);
}
class LagePathShape {
points: ArrayLike<number>;
}
interface LargePathProps extends PathProps {
shape?: LagePathShape
}
class LargePath extends Path<LargePathProps> {
type = 'largeBar';
shape: LagePathShape;
;
__startPoint: number[];
__baseDimIdx: number;
__largeDataIndices: ArrayLike<number>;
__barWidth: number;
constructor(opts?: LargePathProps) {
super(opts);
}
getDefaultShape() {
return new LagePathShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: LagePathShape) {
// Drawing lines is more efficient than drawing
// a whole line or drawing rects.
const points = shape.points;
const startPoint = this.__startPoint;
const baseDimIdx = this.__baseDimIdx;
for (let i = 0; i < points.length; i += 2) {
startPoint[baseDimIdx] = points[i + baseDimIdx];
ctx.moveTo(startPoint[0], startPoint[1]);
ctx.lineTo(points[i], points[i + 1]);
}
}
}
function createLarge(
seriesModel: BarSeriesModel,
group: Group,
incremental?: boolean
) {
// TODO support polar
const data = seriesModel.getData();
const startPoint = [];
const baseDimIdx = data.getLayout('valueAxisHorizontal') ? 1 : 0;
startPoint[1 - baseDimIdx] = data.getLayout('valueAxisStart');
const largeDataIndices = data.getLayout('largeDataIndices');
const barWidth = data.getLayout('barWidth');
const backgroundModel = seriesModel.getModel('backgroundStyle');
const drawBackground = seriesModel.get('showBackground', true);
if (drawBackground) {
const points = data.getLayout('largeBackgroundPoints');
const backgroundStartPoint: number[] = [];
backgroundStartPoint[1 - baseDimIdx] = data.getLayout('backgroundStart');
const bgEl = new LargePath({
shape: {points: points},
incremental: !!incremental,
silent: true,
z2: 0
});
bgEl.__startPoint = backgroundStartPoint;
bgEl.__baseDimIdx = baseDimIdx;
bgEl.__largeDataIndices = largeDataIndices;
bgEl.__barWidth = barWidth;
setLargeBackgroundStyle(bgEl, backgroundModel, data);
group.add(bgEl);
}
const el = new LargePath({
shape: {points: data.getLayout('largePoints')},
incremental: !!incremental
});
el.__startPoint = startPoint;
el.__baseDimIdx = baseDimIdx;
el.__largeDataIndices = largeDataIndices;
el.__barWidth = barWidth;
group.add(el);
setLargeStyle(el, seriesModel, data);
// Enable tooltip and user mouse/touch event handlers.
getECData(el).seriesIndex = seriesModel.seriesIndex;
if (!seriesModel.get('silent')) {
el.on('mousedown', largePathUpdateDataIndex);
el.on('mousemove', largePathUpdateDataIndex);
}
}
// Use throttle to avoid frequently traverse to find dataIndex.
const largePathUpdateDataIndex = throttle(function (this: LargePath, event: ZRElementEvent) {
const largePath = this;
const dataIndex = largePathFindDataIndex(largePath, event.offsetX, event.offsetY);
getECData(largePath).dataIndex = dataIndex >= 0 ? dataIndex : null;
}, 30, false);
function largePathFindDataIndex(largePath: LargePath, x: number, y: number) {
const baseDimIdx = largePath.__baseDimIdx;
const valueDimIdx = 1 - baseDimIdx;
const points = largePath.shape.points;
const largeDataIndices = largePath.__largeDataIndices;
const barWidthHalf = Math.abs(largePath.__barWidth / 2);
const startValueVal = largePath.__startPoint[valueDimIdx];
_eventPos[0] = x;
_eventPos[1] = y;
const pointerBaseVal = _eventPos[baseDimIdx];
const pointerValueVal = _eventPos[1 - baseDimIdx];
const baseLowerBound = pointerBaseVal - barWidthHalf;
const baseUpperBound = pointerBaseVal + barWidthHalf;
for (let i = 0, len = points.length / 2; i < len; i++) {
const ii = i * 2;
const barBaseVal = points[ii + baseDimIdx];
const barValueVal = points[ii + valueDimIdx];
if (
barBaseVal >= baseLowerBound && barBaseVal <= baseUpperBound
&& (
startValueVal <= barValueVal
? (pointerValueVal >= startValueVal && pointerValueVal <= barValueVal)
: (pointerValueVal >= barValueVal && pointerValueVal <= startValueVal)
)
) {
return largeDataIndices[i];
}
}
return -1;
}
function setLargeStyle(
el: LargePath,
seriesModel: BarSeriesModel,
data: List
) {
const globalStyle = data.getVisual('style');
el.useStyle(extend({}, globalStyle));
// Use stroke instead of fill.
el.style.fill = null;
el.style.stroke = globalStyle.fill;
el.style.lineWidth = data.getLayout('barWidth');
}
function setLargeBackgroundStyle(
el: LargePath,
backgroundModel: Model<BarSeriesOption['backgroundStyle']>,
data: List
) {
const borderColor = backgroundModel.get('borderColor') || backgroundModel.get('color');
const itemStyle = backgroundModel.getItemStyle();
el.useStyle(itemStyle);
el.style.fill = null;
el.style.stroke = borderColor;
el.style.lineWidth = data.getLayout('barWidth') as number;
}
function createBackgroundShape(
isHorizontalOrRadial: boolean,
layout: SectorLayout | RectLayout,
coord: CoordSysOfBar
): SectorShape | RectShape {
if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
const rectShape = layout as RectShape;
const coordLayout = coord.getArea();
return {
x: isHorizontalOrRadial ? rectShape.x : coordLayout.x,
y: isHorizontalOrRadial ? coordLayout.y : rectShape.y,
width: isHorizontalOrRadial ? rectShape.width : coordLayout.width,
height: isHorizontalOrRadial ? coordLayout.height : rectShape.height
} as RectShape;
}
else {
const coordLayout = coord.getArea();
const sectorShape = layout as SectorShape;
return {
cx: coordLayout.cx,
cy: coordLayout.cy,
r0: isHorizontalOrRadial ? coordLayout.r0 : sectorShape.r0,
r: isHorizontalOrRadial ? coordLayout.r : sectorShape.r,
startAngle: isHorizontalOrRadial ? sectorShape.startAngle : 0,
endAngle: isHorizontalOrRadial ? sectorShape.endAngle : Math.PI * 2
} as SectorShape;
}
}
function createBackgroundEl(
coord: CoordSysOfBar,
isHorizontalOrRadial: boolean,
layout: SectorLayout | RectLayout
): Rect | Sector {
const ElementClz = coord.type === 'polar' ? Sector : Rect;
return new ElementClz({
shape: createBackgroundShape(isHorizontalOrRadial, layout, coord) as any,
silent: true,
z2: 0
});
}
export default BarView;