diff --git a/src/action/changeAxisOrder.ts b/src/action/changeAxisOrder.ts new file mode 100644 index 0000000000..5d49d8f2b2 --- /dev/null +++ b/src/action/changeAxisOrder.ts @@ -0,0 +1,47 @@ +/* +* 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. +*/ + +// @ts-nocheck + +import * as echarts from '../echarts'; +import * as zrUtil from 'zrender/src/core/util'; + + +/** + * @payload + * @property {string} [componentType=series] + * @property {number} [dx] + * @property {number} [dy] + * @property {number} [zoom] + * @property {number} [originX] + * @property {number} [originY] + */ +echarts.registerAction({ + type: 'changeAxisOrder', + event: 'changeAxisOrder', + update: 'update' +}, function (payload, ecModel) { + const componentType = payload.componentType || 'series'; + + ecModel.eachComponent( + { mainType: componentType, query: payload }, + function (componentModel) { + } + ); +}); diff --git a/src/chart/bar.ts b/src/chart/bar.ts index db4bbef3bf..1370124c25 100644 --- a/src/chart/bar.ts +++ b/src/chart/bar.ts @@ -24,6 +24,7 @@ import {layout, largeLayout} from '../layout/barGrid'; import '../coord/cartesian/Grid'; import './bar/BarSeries'; import './bar/BarView'; +import '../action/changeAxisOrder'; // In case developer forget to include grid component import '../component/gridSimple'; diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts index 299a46fbd7..336c2562cd 100644 --- a/src/chart/bar/BarView.ts +++ b/src/chart/bar/BarView.ts @@ -27,18 +27,21 @@ import { initProps, enableHoverEmphasis, setLabelStyle, - clearStates + clearStates, + updateLabel, + initLabel } from '../../util/graphic'; import Path, { PathProps } from 'zrender/src/graphic/Path'; +import * as numberUtil from '../../util/number'; import Group from 'zrender/src/graphic/Group'; import {throttle} from '../../util/throttle'; import {createClipPath} from '../helper/createClipPathFromCoordSys'; import Sausage from '../../util/shape/sausage'; import ChartView from '../../view/Chart'; -import List from '../../data/List'; +import List, {DefaultDataVisual} from '../../data/List'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; -import { StageHandlerProgressParams, ZRElementEvent, ColorString } from '../../util/types'; +import { StageHandlerProgressParams, ZRElementEvent, ColorString, OrdinalSortInfo, Payload, OrdinalNumber, OrdinalRawValue, DisplayState, ParsedValue } from '../../util/types'; import BarSeriesModel, { BarSeriesOption, BarDataItemOption } from './BarSeries'; import type Axis2D from '../../coord/cartesian/Axis2D'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; @@ -46,6 +49,9 @@ import type { RectLike } from 'zrender/src/core/BoundingRect'; import type Model from '../../model/Model'; import { isCoordinateSystemType } from '../../coord/CoordinateSystem'; import { getDefaultLabel } from '../helper/labelHelper'; +import OrdinalScale from '../../scale/Ordinal'; +import AngleAxis from '../../coord/polar/AngleAxis'; +import RadiusAxis from '../../coord/polar/RadiusAxis'; const BAR_BORDER_WIDTH_QUERY = ['itemStyle', 'borderWidth'] as const; const BAR_BORDER_RADIUS_QUERY = ['itemStyle', 'borderRadius'] as const; @@ -100,17 +106,18 @@ class BarView extends ChartView { private _backgroundEls: (Rect | Sector)[]; - render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI): void { + render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload) { this._updateDrawMode(seriesModel); const coordinateSystemType = seriesModel.get('coordinateSystem'); + const isReorder = payload && payload.type === 'changeAxisOrder'; if (coordinateSystemType === 'cartesian2d' || coordinateSystemType === 'polar' ) { this._isLargeDraw ? this._renderLarge(seriesModel, ecModel, api) - : this._renderNormal(seriesModel, ecModel, api); + : this._renderNormal(seriesModel, ecModel, api, isReorder); } else if (__DEV__) { console.warn('Only cartesian2d and polar supported for bar.'); @@ -138,23 +145,38 @@ class BarView extends ChartView { } } - private _renderNormal(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI): void { + private _renderNormal( + seriesModel: BarSeriesModel, + ecModel: GlobalModel, + api: ExtensionAPI, + isReorder: boolean + ): void { + const that = this; const group = this.group; const data = seriesModel.getData(); const oldData = this._data; const coord = seriesModel.coordinateSystem; const baseAxis = coord.getBaseAxis(); + let valueAxis: Axis2D | RadiusAxis | AngleAxis; let isHorizontalOrRadial: boolean; if (coord.type === 'cartesian2d') { isHorizontalOrRadial = (baseAxis as Axis2D).isHorizontal(); + valueAxis = coord.getOtherAxis(baseAxis as Axis2D); } else if (coord.type === 'polar') { isHorizontalOrRadial = baseAxis.dim === 'angle'; + valueAxis = coord.getOtherAxis(baseAxis as (AngleAxis | RadiusAxis)); } const animationModel = seriesModel.isAnimationEnabled() ? seriesModel : null; + const axisAnimationModel = baseAxis.model; + + const axis2DModel = (baseAxis as Axis2D).model; + const axisSort = coord.type === 'cartesian2d' && axis2DModel.get('sort') + && axis2DModel.get('sortSeriesIndex') === seriesModel.seriesIndex; + const realtimeSort = axisSort && axis2DModel.get('realtimeSort'); const needsClip = seriesModel.get('clip', true); const coordSysClipArea = getClipArea(coord, data); @@ -163,6 +185,8 @@ class BarView extends ChartView { // We don't use clipPath in normal mode because we needs a perfect animation // And don't want the label are clipped. + const labelModel = seriesModel.getModel('label'); + const roundCap = seriesModel.get('roundCap', true); const drawBackground = seriesModel.get('showBackground', true); @@ -172,6 +196,50 @@ class BarView extends ChartView { const bgEls: BarView['_backgroundEls'] = []; const oldBgEls = this._backgroundEls; + let hasDuringForOneData = false; + let getDuring: () => (() => void) = () => { + return null; + }; + if (coord.type === 'cartesian2d') { + const oldOrder = (baseAxis.scale as OrdinalScale).getCategorySortInfo(); + const orderMap = (idx: number) => { + return data.get(valueAxis.dim, idx) as number; + }; + + if (realtimeSort) { + // Sort in animation during + const isOrderChanged = this._isDataOrderChanged(data, orderMap, oldOrder); + if (isOrderChanged) { + getDuring = () => { + if (!hasDuringForOneData) { + hasDuringForOneData = true; + return () => { + const orderMap = (idx: number) => { + const el = (data.getItemGraphicEl(idx) as Rect); + if (el) { + const shape = el.shape; + return isHorizontalOrRadial ? shape.y + shape.height : shape.x + shape.width; + } + else { + return 0; + } + }; + that._updateSort(data, orderMap, baseAxis as Axis2D, api); + }; + } + else { + return () => null; + } + }; + } + } + else if (axisSort) { + // Sort now in the first frame + this._updateSort(data, orderMap, baseAxis as Axis2D, api); + } + } + + data.diff(oldData) .add(function (dataIndex) { const itemModel = data.getItemModel(dataIndex); @@ -204,7 +272,7 @@ class BarView extends ChartView { } const el = elementCreator[coord.type]( - dataIndex, layout, isHorizontalOrRadial, animationModel, false, roundCap + seriesModel, data, dataIndex, layout, isHorizontalOrRadial, animationModel, false, getDuring(), roundCap ); data.setItemGraphicEl(dataIndex, el); group.add(el); @@ -250,13 +318,52 @@ class BarView extends ChartView { if (el) { clearStates(el); - updateProps(el as Path, { - shape: layout - }, animationModel, newIndex); + + if (coord.type === 'cartesian2d' + && baseAxis.type === 'category' && (baseAxis as Axis2D).model.get('sort') + ) { + const rect = layout as RectShape; + let seriesShape, axisShape; + if (baseAxis.dim === 'x') { + axisShape = { + x: rect.x, + width: rect.width + }; + seriesShape = { + y: rect.y, + height: rect.height + }; + } + else { + axisShape = { + y: rect.y, + height: rect.height + }; + seriesShape = { + x: rect.x, + width: rect.width + }; + } + + if (!isReorder) { + updateProps(el as Path, { shape: seriesShape }, animationModel, newIndex, null, getDuring()); + } + updateProps(el as Path, { shape: axisShape }, axisAnimationModel, newIndex, null); + } + else { + updateProps(el as Path, { + shape: layout + }, animationModel, newIndex, null); + } + + const defaultTextGetter = (values: ParsedValue | ParsedValue[]) => { + return getDefaultLabel(seriesModel.getData(), newIndex, values); + }; + updateLabel(el, data, newIndex, labelModel, seriesModel, animationModel, defaultTextGetter); } else { el = elementCreator[coord.type]( - newIndex, layout, isHorizontalOrRadial, animationModel, true, roundCap + seriesModel, data, newIndex, layout, isHorizontalOrRadial, animationModel, true, getDuring(), roundCap ); } @@ -316,7 +423,85 @@ class BarView extends ChartView { } } - remove(ecModel?: GlobalModel): void { + _dataSort( + data: List, + map: ((idx: number) => number) + ): OrdinalSortInfo[] { + type SortValueInfo = { + mappedValue: number, + ordinalNumber: OrdinalNumber, + beforeSortIndex: number + }; + const info: SortValueInfo[] = []; + data.each(idx => { + info.push({ + mappedValue: map(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 zrUtil.map(info, item => { + return { + ordinalNumber: item.ordinalNumber, + beforeSortIndex: item.beforeSortIndex + }; + }); + } + + _isDataOrderChanged( + data: List, + 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].ordinalNumber); + if (value > lastValue) { + return true; + } + lastValue = value; + } + return false; + } + + _updateSort( + data: List, + orderMap: ((idx: number) => number), + baseAxis: Axis2D, + api: ExtensionAPI + ) { + const oldOrder = (baseAxis.scale as OrdinalScale).getCategorySortInfo(); + const isOrderChanged = this._isDataOrderChanged(data, orderMap, oldOrder); + if (isOrderChanged) { + // re-sort and update in axis + const sortInfo = this._dataSort(data, orderMap); + baseAxis.setCategorySortInfo(sortInfo); + + const action = { + type: 'changeAxisOrder', + componentType: baseAxis.dim + 'Axis', + axisId: baseAxis.index + } as Payload; + api.dispatchAction(action); + } + } + + remove(ecModel?: GlobalModel) { this._clear(ecModel); } @@ -399,8 +584,10 @@ const clip: { interface ElementCreator { ( - dataIndex: number, layout: RectLayout | SectorLayout, isHorizontalOrRadial: boolean, - animationModel: BarSeriesModel, isUpdate: boolean, roundCap?: boolean + seriesModel: BarSeriesModel, data: List, newIndex: number, + layout: RectLayout | SectorLayout, isHorizontalOrRadial: boolean, + animationModel: BarSeriesModel, isUpdate: boolean, during: () => void, + roundCap?: boolean ): BarPossiblePath } @@ -409,8 +596,8 @@ const elementCreator: { } = { cartesian2d( - dataIndex, layout: RectLayout, isHorizontal, - animationModel, isUpdate + seriesModel, data, newIndex, layout: RectLayout, isHorizontal, + animationModel, isUpdate, during ) { const rect = new Rect({ shape: zrUtil.extend({}, layout), @@ -427,17 +614,25 @@ const elementCreator: { const animateTarget = {} as RectShape; rectShape[animateProperty] = 0; animateTarget[animateProperty] = layout[animateProperty]; + (isUpdate ? updateProps : initProps)(rect, { shape: animateTarget - }, animationModel, dataIndex); + }, animationModel, newIndex, null, during); + + const defaultTextGetter = (values: ParsedValue | ParsedValue[]) => { + return getDefaultLabel(seriesModel.getData(), newIndex, values); + }; + + const labelModel = seriesModel.getModel('label'); + (isUpdate ? updateLabel : initLabel)(rect, data, newIndex, labelModel, seriesModel, animationModel, defaultTextGetter); } return rect; }, polar( - dataIndex: number, layout: SectorLayout, isRadial: boolean, - animationModel, isUpdate, roundCap + seriesModel, data, newIndex, layout: SectorLayout, isRadial: boolean, + animationModel, isUpdate, during, 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 @@ -462,8 +657,9 @@ const elementCreator: { sectorShape[animateProperty] = isRadial ? 0 : layout.startAngle; animateTarget[animateProperty] = layout[animateProperty]; (isUpdate ? updateProps : initProps)(sector, { - shape: animateTarget - }, animationModel, dataIndex); + shape: animateTarget, + // __value: typeof dataValue === 'string' ? parseInt(dataValue, 10) : dataValue + }, animationModel); } return sector; diff --git a/src/chart/helper/labelHelper.ts b/src/chart/helper/labelHelper.ts index 25a0960cb1..2fd2282000 100644 --- a/src/chart/helper/labelHelper.ts +++ b/src/chart/helper/labelHelper.ts @@ -20,22 +20,31 @@ import {retrieveRawValue} from '../../data/helper/dataProvider'; import List from '../../data/List'; +import {ParsedValue} from '../../util/types'; /** * @return label string. Not null/undefined */ -export function getDefaultLabel(data: List, dataIndex: number): string { +export function getDefaultLabel( + data: List, + dataIndex: number, + interpolatedValues?: ParsedValue | ParsedValue[] +): string { const labelDims = data.mapDimensionsAll('defaultedLabel'); const len = labelDims.length; // Simple optimization (in lots of cases, label dims length is 1) if (len === 1) { - return retrieveRawValue(data, dataIndex, labelDims[0]); + return interpolatedValues == null + ? retrieveRawValue(data, dataIndex, labelDims[0]) + : interpolatedValues; } else if (len) { const vals = []; for (let i = 0; i < labelDims.length; i++) { - const val = retrieveRawValue(data, dataIndex, labelDims[i]); + const val = interpolatedValues == null + ? retrieveRawValue(data, dataIndex, labelDims[i]) + : interpolatedValues; vals.push(val); } return vals.join(' '); diff --git a/src/coord/Axis.ts b/src/coord/Axis.ts index 0870273240..698e322d01 100644 --- a/src/coord/Axis.ts +++ b/src/coord/Axis.ts @@ -178,7 +178,7 @@ class Axis { const ticksCoords = map(ticks, function (tickValue) { return { coord: this.dataToCoord(tickValue), - tickValue: tickValue + tickValue: this.scale instanceof OrdinalScale ? this.scale.getCategoryIndex(tickValue) : tickValue }; }, this); diff --git a/src/coord/cartesian/Axis2D.ts b/src/coord/cartesian/Axis2D.ts index 5ad717526b..ca82100095 100644 --- a/src/coord/cartesian/Axis2D.ts +++ b/src/coord/cartesian/Axis2D.ts @@ -18,11 +18,12 @@ */ import Axis from '../Axis'; -import { DimensionName } from '../../util/types'; +import { DimensionName, OrdinalSortInfo } from '../../util/types'; import Scale from '../../scale/Scale'; import CartesianAxisModel, { CartesianAxisPosition } from './AxisModel'; import Grid from './Grid'; import { OptionAxisType } from '../axisCommonTypes'; +import OrdinalScale from '../../scale/Ordinal'; interface Axis2D { @@ -110,6 +111,18 @@ class Axis2D extends Axis { return this.coordToData(this.toLocalCoord(point[this.dim === 'x' ? 0 : 1]), clamp); } + /** + * Set ordinalSortInfo + * @param info new OrdinalSortInfo + */ + setCategorySortInfo(info: OrdinalSortInfo[]): boolean { + if (this.type !== 'category') { + return false; + } + + this.model.option.categorySortInfo = info; + (this.scale as OrdinalScale).setCategorySortInfo(info); + } } diff --git a/src/coord/cartesian/AxisModel.ts b/src/coord/cartesian/AxisModel.ts index 3b6db87b8f..1d4a6d0b5e 100644 --- a/src/coord/cartesian/AxisModel.ts +++ b/src/coord/cartesian/AxisModel.ts @@ -25,6 +25,7 @@ import Axis2D from './Axis2D'; import { AxisBaseOption } from '../axisCommonTypes'; import GridModel from './GridModel'; import { AxisBaseModel } from '../AxisBaseModel'; +import {OrdinalSortInfo} from '../../util/types'; export type CartesianAxisPosition = 'top' | 'bottom' | 'left' | 'right'; @@ -35,6 +36,10 @@ interface CartesianAxisOption extends AxisBaseOption { position?: CartesianAxisPosition; // Offset is for multiple axis on the same position. offset?: number; + sort?: boolean; + realtimeSort?: boolean; + sortSeriesIndex?: number; + categorySortInfo?: OrdinalSortInfo[]; } class CartesianAxisModel extends ComponentModel @@ -61,7 +66,11 @@ zrUtil.mixin(CartesianAxisModel, AxisModelCommonMixin); const extraOption: CartesianAxisOption = { // gridIndex: 0, // gridId: '', - offset: 0 + offset: 0, + sort: false, + realtimeSort: false, + sortSeriesIndex: null, + categorySortInfo: [] }; axisModelCreator('x', CartesianAxisModel, extraOption); diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts index 4a0b54bb1d..6c803f9d03 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -47,6 +47,7 @@ import { Dictionary } from 'zrender/src/core/types'; import {CoordinateSystemMaster} from '../CoordinateSystem'; import { ScaleDataValue } from '../../util/types'; import List from '../../data/List'; +import OrdinalScale from '../../scale/Ordinal'; import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; @@ -407,10 +408,19 @@ class Grid implements CoordinateSystemMaster { * Update cartesian properties from series. */ private _updateScale(ecModel: GlobalModel, gridModel: GridModel): void { + const sortedDataValue: number[] = []; + const sortedDataIndex: number[] = []; + let hasCategoryIndices = false; + // Reset scale each(this._axesList, function (axis) { axis.scale.setExtent(Infinity, -Infinity); + if (axis.type === 'category') { + const categorySortInfo = axis.model.get('categorySortInfo'); + (axis.scale as OrdinalScale).setCategorySortInfo(categorySortInfo); + } }); + ecModel.eachSeries(function (seriesModel) { if (isCartesian2DSeries(seriesModel)) { const axesModelMap = findAxisModels(seriesModel); diff --git a/src/model/mixin/dataFormat.ts b/src/model/mixin/dataFormat.ts index eca285f0e6..45d205b546 100644 --- a/src/model/mixin/dataFormat.ts +++ b/src/model/mixin/dataFormat.ts @@ -17,11 +17,12 @@ * under the License. */ +import * as zrUtil from 'zrender/src/core/util'; +import Element from 'zrender/src/Element'; import {retrieveRawValue} from '../../data/helper/dataProvider'; import {formatTpl} from '../../util/format'; -import { DataHost, DisplayState, TooltipRenderMode, CallbackDataParams, ColorString, ZRColor } from '../../util/types'; +import { DataHost, DisplayState, TooltipRenderMode, CallbackDataParams, ColorString, ZRColor, OptionDataValue, ParsedValue } from '../../util/types'; import GlobalModel from '../Global'; -import Element from 'zrender/src/Element'; const DIMENSION_LABEL_REG = /\{@(.+?)\}/g; @@ -33,6 +34,7 @@ interface DataFormatMixin extends DataHost { componentIndex: number; id: string; name: string; + animatedValue: OptionDataValue[]; } class DataFormatMixin { @@ -43,7 +45,7 @@ class DataFormatMixin { getDataParams( dataIndex: number, dataType?: string, - el?: Element // May be used in override. + el?: Element, // May be used in override. ): CallbackDataParams { const data = this.getData(dataType); @@ -96,13 +98,20 @@ class DataFormatMixin { status?: DisplayState, dataType?: string, dimIndex?: number, - labelProp?: string + labelProp?: string, + // interpolateValues?: ParsedValue | ParsedValue[] + extendParams?: Partial ): string { status = status || 'normal'; const data = this.getData(dataType); const itemModel = data.getItemModel(dataIndex); - const params = this.getDataParams(dataIndex, dataType); + const params = this.getDataParams(dataIndex, dataType, null); + + if (extendParams) { + zrUtil.extend(params, extendParams); + } + if (dimIndex != null && (params.value instanceof Array)) { params.value = params.value[dimIndex]; } diff --git a/src/scale/Ordinal.ts b/src/scale/Ordinal.ts index f7057b7d36..7a9d2b38f2 100644 --- a/src/scale/Ordinal.ts +++ b/src/scale/Ordinal.ts @@ -28,7 +28,7 @@ import Scale from './Scale'; import OrdinalMeta from '../data/OrdinalMeta'; import List from '../data/List'; import * as scaleHelper from './helper'; -import { OrdinalRawValue, OrdinalNumber, DimensionLoose } from '../util/types'; +import { OrdinalRawValue, OrdinalNumber, DimensionLoose, OrdinalSortInfo } from '../util/types'; import { AxisBaseOption } from '../coord/axisCommonTypes'; import { isArray } from 'zrender/src/core/util'; @@ -39,6 +39,7 @@ class OrdinalScale extends Scale { readonly type = 'ordinal'; private _ordinalMeta: OrdinalMeta; + private _categorySortInfo: OrdinalSortInfo[]; constructor(setting?: { @@ -54,6 +55,7 @@ class OrdinalScale extends Scale { ordinalMeta = new OrdinalMeta({categories: ordinalMeta}); } this._ordinalMeta = ordinalMeta; + this._categorySortInfo = []; this._extent = this.getSetting('extent') || [0, ordinalMeta.categories.length - 1]; } @@ -74,10 +76,12 @@ class OrdinalScale extends Scale { * Normalize given rank or name to linear [0, 1] */ normalize(val: OrdinalRawValue | OrdinalNumber): number { - return scaleHelper.normalize(this.parse(val), this._extent); + val = this.getCategoryIndex(this.parse(val)); + return scaleHelper.normalize(val, this._extent); } scale(val: number): OrdinalNumber { + val = this.getCategoryIndex(val); return Math.round(scaleHelper.scale(val, this._extent)); } @@ -99,6 +103,23 @@ class OrdinalScale extends Scale { return; } + setCategorySortInfo(info: OrdinalSortInfo[]): void { + this._categorySortInfo = info; + } + + getCategorySortInfo(): OrdinalSortInfo[] { + return this._categorySortInfo; + } + + getCategoryIndex(n: OrdinalNumber): OrdinalNumber { + if (this._categorySortInfo.length) { + return this._categorySortInfo[n].beforeSortIndex; + } + else { + return n; + } + } + /** * Get item on rank n */ diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts index 7cedc8ac54..4075785626 100644 --- a/src/scale/Scale.ts +++ b/src/scale/Scale.ts @@ -21,7 +21,7 @@ import * as clazzUtil from '../util/clazz'; import { Dictionary } from 'zrender/src/core/types'; import List from '../data/List'; -import { DimensionName, ScaleDataValue, OptionDataValue } from '../util/types'; +import { DimensionName, ScaleDataValue, OptionDataValue, DimensionLoose } from '../util/types'; import { ScaleRawExtentInfo } from '../coord/scaleRawExtentInfo'; @@ -85,7 +85,7 @@ abstract class Scale { /** * Set extent from data */ - unionExtentFromData(data: List, dim: DimensionName): void { + unionExtentFromData(data: List, dim: DimensionName | DimensionLoose): void { this.unionExtent(data.getApproximateExtent(dim)); } diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 412581276d..d6c30e7f50 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -46,7 +46,7 @@ import LRU from 'zrender/src/core/LRU'; import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable'; import { PatternObject } from 'zrender/src/graphic/Pattern'; import { GradientObject } from 'zrender/src/graphic/Gradient'; -import Element, { ElementEvent, ElementTextConfig } from 'zrender/src/Element'; +import Element, { ElementEvent, ElementTextConfig, ElementProps } from 'zrender/src/Element'; import Model from '../model/Model'; import { AnimationOptionMixin, @@ -58,7 +58,10 @@ import { ColorString, DataModel, ECEventData, - ZRStyleProps + ZRStyleProps, + SeriesOption, + ParsedValue, + CallbackDataParams } from './types'; import GlobalModel from '../model/Global'; import { makeInner } from './model'; @@ -72,6 +75,11 @@ import { map, defaults } from 'zrender/src/core/util'; +import * as numberUtil from './number'; +import SeriesModel from '../model/Series'; +import {OnframeCallback, interpolateNumber} from 'zrender/src/animation/Animator'; +import List from '../data/List'; +import DataFormatMixin from '../model/mixin/dataFormat'; const mathMax = Math.max; @@ -614,7 +622,8 @@ interface SetLabelStyleOpt extends TextCommonParams { state: DisplayState, dataType: string, labelDimIndex: number, - labelProp: string + labelProp: string, + extendParams?: Partial ) => string }, labelDataIndex?: LDI, @@ -623,6 +632,33 @@ interface SetLabelStyleOpt extends TextCommonParams { } +function getLabelText(opt?: SetLabelStyleOpt, interpolateValues?: ParsedValue | ParsedValue[]) { + const labelFetcher = opt.labelFetcher; + const labelDataIndex = opt.labelDataIndex; + const labelDimIndex = opt.labelDimIndex; + const labelProp = opt.labelProp; + + let baseText; + if (labelFetcher) { + baseText = labelFetcher.getFormattedLabel(labelDataIndex, 'normal', null, labelDimIndex, labelProp, { + value: interpolateValues + }); + } + if (baseText == null) { + baseText = isFunction(opt.defaultText) ? opt.defaultText(labelDataIndex, opt) : opt.defaultText; + } + const emphasisStyleText = retrieve2( + labelFetcher + ? labelFetcher.getFormattedLabel(labelDataIndex, 'emphasis', null, labelDimIndex, labelProp) + : null, + baseText + ); + return { + normal: baseText, + emphasis: emphasisStyleText + }; +} + /** * Set normal styles and emphasis styles about text on target element * If target is a ZRText. It will create a new style object. @@ -652,26 +688,6 @@ export function setLabelStyle( // label should be displayed, where text is fetched by `normal.formatter` or `opt.defaultText`. let richText = isSetOnText ? targetEl as ZRText : null; if (showNormal || showEmphasis) { - const labelFetcher = opt.labelFetcher; - const labelDataIndex = opt.labelDataIndex; - const labelDimIndex = opt.labelDimIndex; - const labelProp = opt.labelProp; - - let baseText; - if (labelFetcher) { - baseText = labelFetcher.getFormattedLabel(labelDataIndex, 'normal', null, labelDimIndex, labelProp); - } - if (baseText == null) { - baseText = isFunction(opt.defaultText) ? opt.defaultText(labelDataIndex, opt) : opt.defaultText; - } - const normalStyleText = baseText; - const emphasisStyleText = retrieve2( - labelFetcher - ? labelFetcher.getFormattedLabel(labelDataIndex, 'emphasis', null, labelDimIndex, labelProp) - : null, - baseText - ); - if (!isSetOnText) { // Reuse the previous richText = targetEl.getTextContent(); @@ -728,8 +744,9 @@ export function setLabelStyle( // auto slient is those cases. richText.silent = !!normalModel.getShallow('silent'); - normalStyle.text = normalStyleText; - emphasisState.style.text = emphasisStyleText; + const labelText = getLabelText(opt); + normalStyle.text = labelText.normal; + emphasisState.style.text = labelText.emphasis; // Keep x and y if (richText.style.x != null) { @@ -1056,9 +1073,11 @@ function animateOrSetProps( getAnimationDelayParams?: (el: Element, dataIndex: number) => AnimationDelayCallbackParam }, dataIndex?: number | (() => void), - cb?: () => void + cb?: () => void, + during?: (percent: number) => void ) { if (typeof dataIndex === 'function') { + during = cb; cb = dataIndex; dataIndex = null; } @@ -1095,7 +1114,8 @@ function animateOrSetProps( delay: animationDelay || 0, easing: animationEasing, done: cb, - force: !!cb + force: !!cb || !!during, + during: during }) : (el.stopAnimation(), el.attr(props), cb && cb()); } @@ -1128,9 +1148,10 @@ function updateProps( // TODO: TYPE AnimatableModel animatableModel?: Model, dataIndex?: number | (() => void), - cb?: () => void + cb?: () => void, + during?: () => void ) { - animateOrSetProps(true, el, props, animatableModel, dataIndex, cb); + animateOrSetProps(true, el, props, animatableModel, dataIndex, cb, during); } export {updateProps}; @@ -1148,9 +1169,107 @@ export function initProps( props: Props, animatableModel?: Model, dataIndex?: number | (() => void), - cb?: () => void + cb?: () => void, + during?: () => void +) { + animateOrSetProps(false, el, props, animatableModel, dataIndex, cb, during); +} + +function animateOrSetLabel( + isUpdate: boolean, + el: Element, + data: List, + dataIndex: number, + labelModel: Model, + seriesModel: SeriesModel, + animatableModel?: Model, + defaultTextGetter?: (value: ParsedValue[] | ParsedValue) => string +) { + const element = el as Element & { __value: ParsedValue[] | ParsedValue }; + const valueAnimationEnabled = labelModel && labelModel.get('valueAnimation'); + if (valueAnimationEnabled) { + const precisionOption = labelModel.get('precision'); + let precision: number = precisionOption === 'auto' ? 0 : precisionOption; + + let interpolateValues: (number | string)[] | (number | string); + const rawValues = seriesModel.getRawValue(dataIndex); + let isRawValueNumber = false; + if (typeof rawValues === 'number') { + isRawValueNumber = true; + interpolateValues = rawValues; + } + else { + interpolateValues = []; + for (let i = 0; i < (rawValues as []).length; ++i) { + const info = data.getDimensionInfo(i); + if (info.type !== 'ordinal') { + interpolateValues.push((rawValues as [])[i]); + } + } + } + + const during = (percent: number) => { + let interpolated; + if (isRawValueNumber) { + const value = interpolateNumber(0, interpolateValues as number, percent); + interpolated = numberUtil.round(value, precision); + } + else { + interpolated = []; + for (let i = 0, j = 0; i < (rawValues as []).length; ++i) { + const info = data.getDimensionInfo(i); + // Don't interpolate ordinal dims + if (info.type === 'ordinal') { + interpolated[i] = (rawValues as [])[i]; + } + else { + const value = interpolateNumber(0, (interpolateValues as number[])[i], percent); + interpolated[i] = numberUtil.round(value), precision; + ++j; + } + } + } + const text = el.getTextContent(); + if (text) { + const labelText = getLabelText({ + labelDataIndex: dataIndex, + labelFetcher: seriesModel, + defaultText: defaultTextGetter + ? defaultTextGetter(interpolated) + : interpolated + '' + }, interpolated); + text.style.text = labelText.normal; + text.dirty(); + } + }; + + const props: ElementProps = {}; + animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, null, during); + } +} + +export function updateLabel( + el: Element, + data: List, + dataIndex: number, + labelModel: Model, + seriesModel: SeriesModel, + animatableModel?: Model, + defaultTextGetter?: (value: ParsedValue[] | ParsedValue) => string +) { + animateOrSetLabel(true, el, data, dataIndex, labelModel, seriesModel, animatableModel, defaultTextGetter); +} + +export function initLabel( + el: Element, + data: List, + dataIndex: number, + labelModel: Model, + seriesModel: SeriesModel, + animatableModel?: Model, + defaultTextGetter?: (value: ParsedValue[] | ParsedValue) => string ) { - animateOrSetProps(false, el, props, animatableModel, dataIndex, cb); + animateOrSetLabel(false, el, data, dataIndex, labelModel, seriesModel, animatableModel, defaultTextGetter); } /** diff --git a/src/util/types.ts b/src/util/types.ts index ebd78cf920..e0077bbde2 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -242,6 +242,10 @@ export type TooltipRenderMode = 'html' | 'richText'; // Check `convertDataValue` for more details. export type OrdinalRawValue = string | number; export type OrdinalNumber = number; // The number mapped from each OrdinalRawValue. +export type OrdinalSortInfo = { + ordinalNumber: OrdinalNumber, + beforeSortIndex: number +}; export type ParsedValueNumeric = number | OrdinalNumber; export type ParsedValue = ParsedValueNumeric | OrdinalRawValue; // FIXME:TS better name? @@ -762,6 +766,8 @@ export interface LabelOption extends TextCommonOption { overflow?: TextStyleProps['overflow'] silent?: boolean + precision?: number | 'auto' + valueAnimation?: boolean // TODO: TYPE not all label support formatter // formatter?: string | ((params: CallbackDataParams) => string) diff --git a/test/bar-race.html b/test/bar-race.html new file mode 100644 index 0000000000..1ffcaa2d2f --- /dev/null +++ b/test/bar-race.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +