diff --git a/package.json b/package.json index 1256581087..2e3c1debaa 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ }, "dependencies": { "tslib": "2.3.0", - "zrender": "6.0.0" + "zrender": "npm:zrender-nightly@^6.0.1-dev.20251114" }, "devDependencies": { "@babel/code-frame": "7.10.4", diff --git a/src/coord/axisTickLabelBuilder.ts b/src/coord/axisTickLabelBuilder.ts index a6166d3d62..e7924da776 100644 --- a/src/coord/axisTickLabelBuilder.ts +++ b/src/coord/axisTickLabelBuilder.ts @@ -1,29 +1,29 @@ /* -* 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. -*/ + * 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 * as textContain from 'zrender/src/contain/text'; -import {makeInner} from '../util/model'; +import { makeInner } from '../util/model'; import { - makeLabelFormatter, - getOptionCategoryInterval, - shouldShowAllLabels + makeLabelFormatter, + getOptionCategoryInterval, + shouldShowAllLabels, } from './axisHelper'; import Axis from './Axis'; import Model from '../model/Model'; @@ -34,262 +34,343 @@ import type Axis2D from './cartesian/Axis2D'; import { NullUndefined, ScaleTick, VisualAxisBreak } from '../util/types'; import { ScaleGetTicksOpt } from '../scale/Scale'; - type AxisLabelInfoDetermined = { - formattedLabel: string, - rawLabel: string, - tickValue: number, - time: ScaleTick['time'] | NullUndefined, - break: VisualAxisBreak | NullUndefined, + formattedLabel: string; + rawLabel: string; + tickValue: number; + time: ScaleTick['time'] | NullUndefined; + break: VisualAxisBreak | NullUndefined; }; type AxisCache = { - list: {key: TKey; value: TVal;}[] + list: { key: TKey; value: TVal }[]; }; type AxisCategoryTickLabelCacheKey = - CategoryAxisBaseOption[TTickLabel]['interval']; + CategoryAxisBaseOption[TTickLabel]['interval']; interface AxisCategoryLabelCreated { - labels: AxisLabelInfoDetermined[] - labelCategoryInterval: number + labels: AxisLabelInfoDetermined[]; + labelCategoryInterval: number; } interface AxisCategoryTickCreated { - ticks: number[] - tickCategoryInterval?: number + ticks: number[]; + tickCategoryInterval?: number; } type AxisModelInnerStore = { - lastAutoInterval: number - lastTickCount: number - axisExtent0: number - axisExtent1: number + lastAutoInterval: number; + lastTickCount: number; + axisExtent0: number; + axisExtent1: number; }; const modelInner = makeInner(); type AxisInnerStoreCacheProp = 'axisTick' | 'axisLabel'; type AxisInnerStore = { - axisTick: AxisCache, AxisCategoryTickCreated> - axisLabel: AxisCache, AxisCategoryLabelCreated> - autoInterval: number + axisTick: AxisCache< + AxisCategoryTickLabelCacheKey<'axisTick'>, + AxisCategoryTickCreated + >; + axisLabel: AxisCache< + AxisCategoryTickLabelCacheKey<'axisLabel'>, + AxisCategoryLabelCreated + >; + autoInterval: number; }; const axisInner = makeInner(); export const AxisTickLabelComputingKind = { - estimate: 1, - determine: 2, + estimate: 1, + determine: 2, } as const; export type AxisTickLabelComputingKind = - (typeof AxisTickLabelComputingKind)[keyof typeof AxisTickLabelComputingKind]; + (typeof AxisTickLabelComputingKind)[keyof typeof AxisTickLabelComputingKind]; export interface AxisLabelsComputingContext { - // PENDING: ugly impl, refactor for better code structure? - out: { - // - If `noPxChangeTryDetermine` is not empty, it indicates that the result is not reusable - // when axis pixel extent or axis origin point changed. - // Generally the result is reusable if the result values are calculated with no dependency - // on pixel info, such as `axis.dataToCoord` or `axis.extent`. - // - If ensuring no px changed, calling `noPxChangeTryDetermine` to attempt to convert the - // "estimate" result to "determine" result. - // If return any falsy, cannot make that conversion and need recompute. - noPxChangeTryDetermine: (() => boolean)[] - } - // Must never be NullUndefined - kind: AxisTickLabelComputingKind + // PENDING: ugly impl, refactor for better code structure? + out: { + // - If `noPxChangeTryDetermine` is not empty, it indicates that the result is not reusable + // when axis pixel extent or axis origin point changed. + // Generally the result is reusable if the result values are calculated with no dependency + // on pixel info, such as `axis.dataToCoord` or `axis.extent`. + // - If ensuring no px changed, calling `noPxChangeTryDetermine` to attempt to convert the + // "estimate" result to "determine" result. + // If return any falsy, cannot make that conversion and need recompute. + noPxChangeTryDetermine: (() => boolean)[]; + }; + // Must never be NullUndefined + kind: AxisTickLabelComputingKind; } -export function createAxisLabelsComputingContext(kind: AxisTickLabelComputingKind): AxisLabelsComputingContext { - return { - out: { - noPxChangeTryDetermine: [] - }, - kind, - }; +export function createAxisLabelsComputingContext( + kind: AxisTickLabelComputingKind +): AxisLabelsComputingContext { + return { + out: { + noPxChangeTryDetermine: [], + }, + kind, + }; } - function tickValuesToNumbers(axis: Axis, values: (number | string | Date)[]) { - const nums = zrUtil.map(values, val => axis.scale.parse(val)); - if (axis.type === 'time' && nums.length > 0) { - // Time axis needs duplicate first/last tick (see TimeScale.getTicks()) - // The first and last tick/label don't get drawn - nums.sort(); - nums.unshift(nums[0]); - nums.push(nums[nums.length - 1]); - } - return nums; + const nums = zrUtil.map(values, (val) => axis.scale.parse(val)); + if (axis.type === 'time' && nums.length > 0) { + // Time axis needs duplicate first/last tick (see TimeScale.getTicks()) + // The first and last tick/label don't get drawn + nums.sort(); + nums.unshift(nums[0]); + nums.push(nums[nums.length - 1]); + } + return nums; } -export function createAxisLabels(axis: Axis, ctx: AxisLabelsComputingContext): { - labels: AxisLabelInfoDetermined[] +export function createAxisLabels( + axis: Axis, + ctx: AxisLabelsComputingContext +): { + labels: AxisLabelInfoDetermined[]; } { - const custom = axis.getLabelModel().get('customValues'); - if (custom) { - const labelFormatter = makeLabelFormatter(axis); - const extent = axis.scale.getExtent(); - const tickNumbers = tickValuesToNumbers(axis, custom); - const ticks = zrUtil.filter(tickNumbers, val => val >= extent[0] && val <= extent[1]); + const custom = axis.getLabelModel().get('customValues'); + if (custom) { + const labelFormatter = makeLabelFormatter(axis); + const extent = axis.scale.getExtent(); + const tickNumbers = tickValuesToNumbers(axis, custom); + const ticks = zrUtil.filter( + tickNumbers, + (val) => val >= extent[0] && val <= extent[1] + ); + return { + labels: zrUtil.map(ticks, (numval) => { + const tick = { value: numval }; + const index = ticks.indexOf(numval); + return { - labels: zrUtil.map(ticks, numval => { - const tick = {value: numval}; - const index = ticks.indexOf(numval); - - return { - formattedLabel: labelFormatter(tick, index), - rawLabel: axis.scale.getLabel(tick), - tickValue: numval, - time: undefined as ScaleTick['time'] | NullUndefined, - break: undefined as VisualAxisBreak | NullUndefined, - }; - }), + formattedLabel: labelFormatter(tick, index), + rawLabel: axis.scale.getLabel(tick), + tickValue: numval, + time: undefined as ScaleTick['time'] | NullUndefined, + break: undefined as VisualAxisBreak | NullUndefined, }; - } - // Only ordinal scale support tick interval - return axis.type === 'category' - ? makeCategoryLabels(axis, ctx) - : makeRealNumberLabels(axis); + }), + }; + } + // Only ordinal scale support tick interval + return axis.type === 'category' + ? makeCategoryLabels(axis, ctx) + : makeRealNumberLabels(axis); } /** * @param tickModel For example, can be axisTick, splitLine, splitArea. */ export function createAxisTicks( - axis: Axis, - tickModel: AxisBaseModel, - opt?: Pick + axis: Axis, + tickModel: AxisBaseModel, + opt?: Pick ): { - ticks: number[], - tickCategoryInterval?: number + ticks: number[]; + tickCategoryInterval?: number; } { - const custom = axis.getTickModel().get('customValues'); - if (custom) { - const extent = axis.scale.getExtent(); - const tickNumbers = tickValuesToNumbers(axis, custom); - return { - ticks: zrUtil.filter(tickNumbers, val => val >= extent[0] && val <= extent[1]) - }; - } - // Only ordinal scale support tick interval - return axis.type === 'category' - ? makeCategoryTicks(axis, tickModel) - : {ticks: zrUtil.map(axis.scale.getTicks(opt), tick => tick.value)}; + const custom = axis.getTickModel().get('customValues'); + if (custom) { + const extent = axis.scale.getExtent(); + const tickNumbers = tickValuesToNumbers(axis, custom); + return { + ticks: zrUtil.filter( + tickNumbers, + (val) => val >= extent[0] && val <= extent[1] + ), + }; + } + // Only ordinal scale support tick interval + return axis.type === 'category' + ? makeCategoryTicks(axis, tickModel) + : { ticks: zrUtil.map(axis.scale.getTicks(opt), (tick) => tick.value) }; } -function makeCategoryLabels(axis: Axis, ctx: AxisLabelsComputingContext): ReturnType { - const labelModel = axis.getLabelModel(); - const result = makeCategoryLabelsActually(axis, labelModel, ctx); +function makeCategoryLabels( + axis: Axis, + ctx: AxisLabelsComputingContext +): ReturnType { + const labelModel = axis.getLabelModel(); + const result = makeCategoryLabelsActually(axis, labelModel, ctx); - return (!labelModel.get('show') || axis.scale.isBlank()) - ? {labels: []} - : result; + return !labelModel.get('show') || axis.scale.isBlank() + ? { labels: [] } + : result; } function makeCategoryLabelsActually( - axis: Axis, - labelModel: Model, - ctx: AxisLabelsComputingContext + axis: Axis, + labelModel: Model, + ctx: AxisLabelsComputingContext ): { - labels: AxisLabelInfoDetermined[] - labelCategoryInterval: number + labels: AxisLabelInfoDetermined[]; + labelCategoryInterval: number; } { - const labelsCache = ensureCategoryLabelCache(axis); - const optionLabelInterval = getOptionCategoryInterval(labelModel); - const isEstimate = ctx.kind === AxisTickLabelComputingKind.estimate; - - // In AxisTickLabelComputingKind.estimate, the result likely varies during a single - // pass of ec main process,due to the change of axisExtent, and will not be shared with - // splitLine. Therefore no cache is used. - if (!isEstimate) { - // PENDING: check necessary? - const result = axisCacheGet(labelsCache, optionLabelInterval); - if (result) { - return result; - } + const labelsCache = ensureCategoryLabelCache(axis); + const optionLabelInterval = getOptionCategoryInterval(labelModel); + const isEstimate = ctx.kind === AxisTickLabelComputingKind.estimate; + + // In AxisTickLabelComputingKind.estimate, the result likely varies during a single + // pass of ec main process,due to the change of axisExtent, and will not be shared with + // splitLine. Therefore no cache is used. + if (!isEstimate) { + // PENDING: check necessary? + const result = axisCacheGet(labelsCache, optionLabelInterval); + if (result) { + return result; } + } + + let labels; + let numericLabelInterval; + + if (zrUtil.isFunction(optionLabelInterval)) { + labels = makeLabelsByCustomizedCategoryInterval(axis, optionLabelInterval); + } + else { + numericLabelInterval = + optionLabelInterval === 'auto' + ? makeAutoCategoryInterval(axis, ctx) + : optionLabelInterval; + labels = makeLabelsByNumericCategoryInterval(axis, numericLabelInterval); + } + + const result = { labels, labelCategoryInterval: numericLabelInterval }; + if (!isEstimate) { + axisCacheSet(labelsCache, optionLabelInterval, result); + } + else { + ctx.out.noPxChangeTryDetermine.push(function () { + axisCacheSet(labelsCache, optionLabelInterval, result); + return true; + }); + } + return result; +} - let labels; - let numericLabelInterval; - - if (zrUtil.isFunction(optionLabelInterval)) { - labels = makeLabelsByCustomizedCategoryInterval(axis, optionLabelInterval); - } - else { - numericLabelInterval = optionLabelInterval === 'auto' - ? makeAutoCategoryInterval(axis, ctx) : optionLabelInterval; - labels = makeLabelsByNumericCategoryInterval(axis, numericLabelInterval); - } +function makeCategoryTicks(axis: Axis, tickModel: AxisBaseModel) { + const ticksCache = ensureCategoryTickCache(axis); + const optionTickInterval = getOptionCategoryInterval(tickModel); + const result = axisCacheGet(ticksCache, optionTickInterval); - const result = {labels, labelCategoryInterval: numericLabelInterval}; - if (!isEstimate) { - axisCacheSet(labelsCache, optionLabelInterval, result); - } - else { - ctx.out.noPxChangeTryDetermine.push(function () { - axisCacheSet(labelsCache, optionLabelInterval, result); - return true; - }); - } + if (result) { return result; + } + + let ticks: number[]; + let tickCategoryInterval; + + // Optimize for the case that large category data and no label displayed, + // we should not return all ticks. + if (!tickModel.get('show') || axis.scale.isBlank()) { + ticks = []; + } + + if (zrUtil.isFunction(optionTickInterval)) { + ticks = makeLabelsByCustomizedCategoryInterval( + axis, + optionTickInterval, + true + ); + } + // Always use label interval by default despite label show. Consider this + // scenario, Use multiple grid with the xAxis sync, and only one xAxis shows + // labels. `splitLine` and `axisTick` should be consistent in this case. + else if (optionTickInterval === 'auto') { + const labelsResult = makeCategoryLabelsActually( + axis, + axis.getLabelModel(), + createAxisLabelsComputingContext(AxisTickLabelComputingKind.determine) + ); + tickCategoryInterval = labelsResult.labelCategoryInterval; + ticks = zrUtil.map(labelsResult.labels, function (labelItem) { + return labelItem.tickValue; + }); + } + else { + tickCategoryInterval = optionTickInterval; + ticks = makeLabelsByNumericCategoryInterval( + axis, + tickCategoryInterval, + true + ); + } + + // Cache to avoid calling interval function repeatedly. + return axisCacheSet(ticksCache, optionTickInterval, { + ticks: ticks, + tickCategoryInterval: tickCategoryInterval, + }); } -function makeCategoryTicks(axis: Axis, tickModel: AxisBaseModel) { - const ticksCache = ensureCategoryTickCache(axis); - const optionTickInterval = getOptionCategoryInterval(tickModel); - const result = axisCacheGet(ticksCache, optionTickInterval); - - if (result) { - return result; - } +function makeRealNumberLabels(axis: Axis): ReturnType { + const labelModel = axis.getLabelModel(); + const showAllLabel = shouldShowAllLabels(axis); + const includeMinLabel = labelModel.get('showMinLabel') || showAllLabel; + const includeMaxLabel = labelModel.get('showMaxLabel') || showAllLabel; - let ticks: number[]; - let tickCategoryInterval; + const pruneByBreak = + includeMinLabel || includeMaxLabel ? 'preserve_extent_bound' : 'auto'; + const ticks = axis.scale.getTicks({ pruneByBreak: pruneByBreak }); - // Optimize for the case that large category data and no label displayed, - // we should not return all ticks. - if (!tickModel.get('show') || axis.scale.isBlank()) { - ticks = []; - } + const labelFormatter = makeLabelFormatter(axis); - if (zrUtil.isFunction(optionTickInterval)) { - ticks = makeLabelsByCustomizedCategoryInterval(axis, optionTickInterval, true); - } - // Always use label interval by default despite label show. Consider this - // scenario, Use multiple grid with the xAxis sync, and only one xAxis shows - // labels. `splitLine` and `axisTick` should be consistent in this case. - else if (optionTickInterval === 'auto') { - const labelsResult = makeCategoryLabelsActually( - axis, axis.getLabelModel(), createAxisLabelsComputingContext(AxisTickLabelComputingKind.determine) - ); - tickCategoryInterval = labelsResult.labelCategoryInterval; - ticks = zrUtil.map(labelsResult.labels, function (labelItem) { - return labelItem.tickValue; - }); + const labels = zrUtil.map(ticks, function (tick, idx) { + return { + formattedLabel: labelFormatter(tick, idx), + rawLabel: axis.scale.getLabel(tick), + tickValue: tick.value, + time: tick.time, + break: tick.break, + }; + }); + + const extent = axis.scale.getExtent(); + if (includeMinLabel && ticks.length > 0) { + const firstTick = ticks[0]; + const isFirstTickBreak = firstTick.break != null; + const notAtExtentStart = Math.abs(firstTick.value - extent[0]) > 1e-10; + + if (notAtExtentStart || isFirstTickBreak) { + const minTick = { value: extent[0] }; + const timeInfo = + firstTick.time && !isFirstTickBreak ? firstTick.time : undefined; + labels.unshift({ + formattedLabel: labelFormatter(minTick, 0), + rawLabel: axis.scale.getLabel(minTick), + tickValue: extent[0], + time: timeInfo, + break: undefined, + }); } - else { - tickCategoryInterval = optionTickInterval; - ticks = makeLabelsByNumericCategoryInterval(axis, tickCategoryInterval, true); + } + + if (includeMaxLabel && ticks.length > 0) { + const lastTick = ticks[ticks.length - 1]; + const isLastTickBreak = lastTick.break != null; + const notAtExtentEnd = Math.abs(lastTick.value - extent[1]) > 1e-10; + + if (notAtExtentEnd || isLastTickBreak) { + const maxTick = { value: extent[1] }; + const timeInfo = + lastTick.time && !isLastTickBreak ? lastTick.time : undefined; + labels.push({ + formattedLabel: labelFormatter(maxTick, labels.length), + rawLabel: axis.scale.getLabel(maxTick), + tickValue: extent[1], + time: timeInfo, + break: undefined, + }); } + } - // Cache to avoid calling interval function repeatedly. - return axisCacheSet(ticksCache, optionTickInterval, { - ticks: ticks, tickCategoryInterval: tickCategoryInterval - }); -} - -function makeRealNumberLabels(axis: Axis): ReturnType { - const ticks = axis.scale.getTicks(); - const labelFormatter = makeLabelFormatter(axis); - return { - labels: zrUtil.map(ticks, function (tick, idx) { - return { - formattedLabel: labelFormatter(tick, idx), - rawLabel: axis.scale.getLabel(tick), - tickValue: tick.value, - time: tick.time, - break: tick.break, - }; - }) - }; + return { + labels: labels, + }; } // Large category data calculation is performance sensitive, and ticks and label probably will @@ -303,41 +384,53 @@ const ensureCategoryLabelCache = initAxisCacheMethod('axisLabel'); * cache size always is small, and currently no JS Map object key polyfill, we use a simple * array cache instead of plain object hash. */ -function initAxisCacheMethod(prop: TCacheProp) { - return function ensureCache(axis: Axis): AxisInnerStore[TCacheProp] { - return axisInner(axis)[prop] || (axisInner(axis)[prop] = {list: []}); - }; +function initAxisCacheMethod( + prop: TCacheProp +) { + return function ensureCache(axis: Axis): AxisInnerStore[TCacheProp] { + return axisInner(axis)[prop] || (axisInner(axis)[prop] = { list: [] }); + }; } -function axisCacheGet(cache: AxisCache, key: TKey): TVal | NullUndefined { - for (let i = 0; i < cache.list.length; i++) { - if (cache.list[i].key === key) { - return cache.list[i].value; - } +function axisCacheGet( + cache: AxisCache, + key: TKey +): TVal | NullUndefined { + for (let i = 0; i < cache.list.length; i++) { + if (cache.list[i].key === key) { + return cache.list[i].value; } + } } -function axisCacheSet(cache: AxisCache, key: TKey, value: TVal): TVal { - cache.list.push({key: key, value: value}); - return value; +function axisCacheSet( + cache: AxisCache, + key: TKey, + value: TVal +): TVal { + cache.list.push({ key: key, value: value }); + return value; } -function makeAutoCategoryInterval(axis: Axis, ctx: AxisLabelsComputingContext): number { - if (ctx.kind === AxisTickLabelComputingKind.estimate) { - // Currently axisTick is not involved in estimate kind, and the result likely varies during a - // single pass of ec main process, due to the change of axisExtent. Therefore no cache is used. - const result = axis.calculateCategoryInterval(ctx); - ctx.out.noPxChangeTryDetermine.push(function () { - axisInner(axis).autoInterval = result; - return true; - }); - return result; - } - // Both tick and label uses this result, cacah it to avoid recompute. - const result = axisInner(axis).autoInterval; - return result != null - ? result - : (axisInner(axis).autoInterval = axis.calculateCategoryInterval(ctx)); +function makeAutoCategoryInterval( + axis: Axis, + ctx: AxisLabelsComputingContext +): number { + if (ctx.kind === AxisTickLabelComputingKind.estimate) { + // Currently axisTick is not involved in estimate kind, and the result likely varies during a + // single pass of ec main process, due to the change of axisExtent. Therefore no cache is used. + const result = axis.calculateCategoryInterval(ctx); + ctx.out.noPxChangeTryDetermine.push(function () { + axisInner(axis).autoInterval = result; + return true; + }); + return result; + } + // Both tick and label uses this result, cacah it to avoid recompute. + const result = axisInner(axis).autoInterval; + return result != null + ? result + : (axisInner(axis).autoInterval = axis.calculateCategoryInterval(ctx)); } /** @@ -346,204 +439,234 @@ function makeAutoCategoryInterval(axis: Axis, ctx: AxisLabelsComputingContext): * To get precise result, at least one of `getRotate` and `isHorizontal` * should be implemented in axis. */ -export function calculateCategoryInterval(axis: Axis, ctx: AxisLabelsComputingContext) { - const kind = ctx.kind; - - const params = fetchAutoCategoryIntervalCalculationParams(axis); - const labelFormatter = makeLabelFormatter(axis); - const rotation = (params.axisRotate - params.labelRotate) / 180 * Math.PI; - - const ordinalScale = axis.scale as OrdinalScale; - const ordinalExtent = ordinalScale.getExtent(); - // Providing this method is for optimization: - // avoid generating a long array by `getTicks` - // in large category data case. - const tickCount = ordinalScale.count(); - - if (ordinalExtent[1] - ordinalExtent[0] < 1) { - return 0; - } - - let step = 1; - // Simple optimization. Arbitrary value. - const maxCount = 40; - if (tickCount > maxCount) { - step = Math.max(1, Math.floor(tickCount / maxCount)); - } - let tickValue = ordinalExtent[0]; - const unitSpan = axis.dataToCoord(tickValue + 1) - axis.dataToCoord(tickValue); - const unitW = Math.abs(unitSpan * Math.cos(rotation)); - const unitH = Math.abs(unitSpan * Math.sin(rotation)); - - let maxW = 0; - let maxH = 0; - - // Caution: Performance sensitive for large category data. - // Consider dataZoom, we should make appropriate step to avoid O(n) loop. - for (; tickValue <= ordinalExtent[1]; tickValue += step) { - let width = 0; - let height = 0; - - // Not precise, do not consider align and vertical align - // and each distance from axis line yet. - const rect = textContain.getBoundingRect( - labelFormatter({ value: tickValue }), params.font, 'center', 'top' - ); - // Magic number - width = rect.width * 1.3; - height = rect.height * 1.3; - - // Min size, void long loop. - maxW = Math.max(maxW, width, 7); - maxH = Math.max(maxH, height, 7); - } - - let dw = maxW / unitW; - let dh = maxH / unitH; - // 0/0 is NaN, 1/0 is Infinity. - isNaN(dw) && (dw = Infinity); - isNaN(dh) && (dh = Infinity); - const interval = Math.max(0, Math.floor(Math.min(dw, dh))); - - if (kind === AxisTickLabelComputingKind.estimate) { - // In estimate kind, the inteval likely varies, thus do not erase the cache. - ctx.out.noPxChangeTryDetermine.push( - zrUtil.bind(calculateCategoryIntervalTryDetermine, null, axis, interval, tickCount) - ); - return interval; - } - - const lastInterval = calculateCategoryIntervalDealCache(axis, interval, tickCount); - return lastInterval != null ? lastInterval : interval; +export function calculateCategoryInterval( + axis: Axis, + ctx: AxisLabelsComputingContext +) { + const kind = ctx.kind; + + const params = fetchAutoCategoryIntervalCalculationParams(axis); + const labelFormatter = makeLabelFormatter(axis); + const rotation = ((params.axisRotate - params.labelRotate) / 180) * Math.PI; + + const ordinalScale = axis.scale as OrdinalScale; + const ordinalExtent = ordinalScale.getExtent(); + // Providing this method is for optimization: + // avoid generating a long array by `getTicks` + // in large category data case. + const tickCount = ordinalScale.count(); + + if (ordinalExtent[1] - ordinalExtent[0] < 1) { + return 0; + } + + let step = 1; + // Simple optimization. Arbitrary value. + const maxCount = 40; + if (tickCount > maxCount) { + step = Math.max(1, Math.floor(tickCount / maxCount)); + } + let tickValue = ordinalExtent[0]; + const unitSpan = + axis.dataToCoord(tickValue + 1) - axis.dataToCoord(tickValue); + const unitW = Math.abs(unitSpan * Math.cos(rotation)); + const unitH = Math.abs(unitSpan * Math.sin(rotation)); + + let maxW = 0; + let maxH = 0; + + // Caution: Performance sensitive for large category data. + // Consider dataZoom, we should make appropriate step to avoid O(n) loop. + for (; tickValue <= ordinalExtent[1]; tickValue += step) { + let width = 0; + let height = 0; + + // Not precise, do not consider align and vertical align + // and each distance from axis line yet. + const rect = textContain.getBoundingRect( + labelFormatter({ value: tickValue }), + params.font, + 'center', + 'top' + ); + // Magic number + width = rect.width * 1.3; + height = rect.height * 1.3; + + // Min size, void long loop. + maxW = Math.max(maxW, width, 7); + maxH = Math.max(maxH, height, 7); + } + + let dw = maxW / unitW; + let dh = maxH / unitH; + // 0/0 is NaN, 1/0 is Infinity. + isNaN(dw) && (dw = Infinity); + isNaN(dh) && (dh = Infinity); + const interval = Math.max(0, Math.floor(Math.min(dw, dh))); + + if (kind === AxisTickLabelComputingKind.estimate) { + // In estimate kind, the inteval likely varies, thus do not erase the cache. + ctx.out.noPxChangeTryDetermine.push( + zrUtil.bind( + calculateCategoryIntervalTryDetermine, + null, + axis, + interval, + tickCount + ) + ); + return interval; + } + + const lastInterval = calculateCategoryIntervalDealCache( + axis, + interval, + tickCount + ); + return lastInterval != null ? lastInterval : interval; } function calculateCategoryIntervalTryDetermine( - axis: Axis, interval: number, tickCount: number + axis: Axis, + interval: number, + tickCount: number ): boolean { - return calculateCategoryIntervalDealCache(axis, interval, tickCount) == null; + return calculateCategoryIntervalDealCache(axis, interval, tickCount) == null; } // Return the lastInterval if need to use it, otherwise return NullUndefined and save cache. function calculateCategoryIntervalDealCache( - axis: Axis, interval: number, tickCount: number + axis: Axis, + interval: number, + tickCount: number ): number | NullUndefined { - const cache = modelInner(axis.model); - const axisExtent = axis.getExtent(); - const lastAutoInterval = cache.lastAutoInterval; - const lastTickCount = cache.lastTickCount; - // Use cache to keep interval stable while moving zoom window, - // otherwise the calculated interval might jitter when the zoom - // window size is close to the interval-changing size. - // For example, if all of the axis labels are `a, b, c, d, e, f, g`. - // The jitter will cause that sometimes the displayed labels are - // `a, d, g` (interval: 2) sometimes `a, c, e`(interval: 1). - if (lastAutoInterval != null - && lastTickCount != null - && Math.abs(lastAutoInterval - interval) <= 1 - && Math.abs(lastTickCount - tickCount) <= 1 - // Always choose the bigger one, otherwise the critical - // point is not the same when zooming in or zooming out. - && lastAutoInterval > interval - // If the axis change is caused by chart resize, the cache should not - // be used. Otherwise some hidden labels might not be shown again. - && cache.axisExtent0 === axisExtent[0] - && cache.axisExtent1 === axisExtent[1] - ) { - return lastAutoInterval; - } - // Only update cache if cache not used, otherwise the - // changing of interval is too insensitive. - else { - cache.lastTickCount = tickCount; - cache.lastAutoInterval = interval; - cache.axisExtent0 = axisExtent[0]; - cache.axisExtent1 = axisExtent[1]; - } + const cache = modelInner(axis.model); + const axisExtent = axis.getExtent(); + const lastAutoInterval = cache.lastAutoInterval; + const lastTickCount = cache.lastTickCount; + // Use cache to keep interval stable while moving zoom window, + // otherwise the calculated interval might jitter when the zoom + // window size is close to the interval-changing size. + // For example, if all of the axis labels are `a, b, c, d, e, f, g`. + // The jitter will cause that sometimes the displayed labels are + // `a, d, g` (interval: 2) sometimes `a, c, e`(interval: 1). + if ( + lastAutoInterval != null + && lastTickCount != null + && Math.abs(lastAutoInterval - interval) <= 1 + && Math.abs(lastTickCount - tickCount) <= 1 + // Always choose the bigger one, otherwise the critical + // point is not the same when zooming in or zooming out. + && lastAutoInterval > interval + // If the axis change is caused by chart resize, the cache should not + // be used. Otherwise some hidden labels might not be shown again. + && cache.axisExtent0 === axisExtent[0] + && cache.axisExtent1 === axisExtent[1] + ) { + return lastAutoInterval; + } + // Only update cache if cache not used, otherwise the + // changing of interval is too insensitive. + else { + cache.lastTickCount = tickCount; + cache.lastAutoInterval = interval; + cache.axisExtent0 = axisExtent[0]; + cache.axisExtent1 = axisExtent[1]; + } } function fetchAutoCategoryIntervalCalculationParams(axis: Axis) { - const labelModel = axis.getLabelModel(); - return { - axisRotate: axis.getRotate - ? axis.getRotate() - : ((axis as Axis2D).isHorizontal && !(axis as Axis2D).isHorizontal()) - ? 90 - : 0, - labelRotate: labelModel.get('rotate') || 0, - font: labelModel.getFont() - }; + const labelModel = axis.getLabelModel(); + return { + axisRotate: axis.getRotate + ? axis.getRotate() + : (axis as Axis2D).isHorizontal && !(axis as Axis2D).isHorizontal() + ? 90 + : 0, + labelRotate: labelModel.get('rotate') || 0, + font: labelModel.getFont(), + }; } function makeLabelsByNumericCategoryInterval( - axis: Axis, categoryInterval: number + axis: Axis, + categoryInterval: number ): AxisLabelInfoDetermined[]; function makeLabelsByNumericCategoryInterval( - axis: Axis, categoryInterval: number, onlyTick: false + axis: Axis, + categoryInterval: number, + onlyTick: false ): AxisLabelInfoDetermined[]; function makeLabelsByNumericCategoryInterval( - axis: Axis, categoryInterval: number, onlyTick: true + axis: Axis, + categoryInterval: number, + onlyTick: true ): number[]; function makeLabelsByNumericCategoryInterval( - axis: Axis, categoryInterval: number, onlyTick?: boolean + axis: Axis, + categoryInterval: number, + onlyTick?: boolean ) { - const labelFormatter = makeLabelFormatter(axis); - const ordinalScale = axis.scale as OrdinalScale; - const ordinalExtent = ordinalScale.getExtent(); - const labelModel = axis.getLabelModel(); - const result: (AxisLabelInfoDetermined | number)[] = []; - - // TODO: axisType: ordinalTime, pick the tick from each month/day/year/... - - const step = Math.max((categoryInterval || 0) + 1, 1); - let startTick = ordinalExtent[0]; - const tickCount = ordinalScale.count(); - - // Calculate start tick based on zero if possible to keep label consistent - // while zooming and moving while interval > 0. Otherwise the selection - // of displayable ticks and symbols probably keep changing. - // 3 is empirical value. - if (startTick !== 0 && step > 1 && tickCount / step > 2) { - startTick = Math.round(Math.ceil(startTick / step) * step); - } - - // (1) Only add min max label here but leave overlap checking - // to render stage, which also ensure the returned list - // suitable for splitLine and splitArea rendering. - // (2) Scales except category always contain min max label so - // do not need to perform this process. - const showAllLabel = shouldShowAllLabels(axis); - const includeMinLabel = labelModel.get('showMinLabel') || showAllLabel; - const includeMaxLabel = labelModel.get('showMaxLabel') || showAllLabel; - - if (includeMinLabel && startTick !== ordinalExtent[0]) { - addItem(ordinalExtent[0]); - } - - // Optimize: avoid generating large array by `ordinalScale.getTicks()`. - let tickValue = startTick; - for (; tickValue <= ordinalExtent[1]; tickValue += step) { - addItem(tickValue); - } - - if (includeMaxLabel && tickValue - step !== ordinalExtent[1]) { - addItem(ordinalExtent[1]); - } - - function addItem(tickValue: number) { - const tickObj = { value: tickValue }; - result.push(onlyTick - ? tickValue - : { - formattedLabel: labelFormatter(tickObj), - rawLabel: ordinalScale.getLabel(tickObj), - tickValue: tickValue, - time: undefined, - break: undefined, - } - ); - } - - return result; + const labelFormatter = makeLabelFormatter(axis); + const ordinalScale = axis.scale as OrdinalScale; + const ordinalExtent = ordinalScale.getExtent(); + const labelModel = axis.getLabelModel(); + const result: (AxisLabelInfoDetermined | number)[] = []; + + // TODO: axisType: ordinalTime, pick the tick from each month/day/year/... + + const step = Math.max((categoryInterval || 0) + 1, 1); + let startTick = ordinalExtent[0]; + const tickCount = ordinalScale.count(); + + // Calculate start tick based on zero if possible to keep label consistent + // while zooming and moving while interval > 0. Otherwise the selection + // of displayable ticks and symbols probably keep changing. + // 3 is empirical value. + if (startTick !== 0 && step > 1 && tickCount / step > 2) { + startTick = Math.round(Math.ceil(startTick / step) * step); + } + + // (1) Only add min max label here but leave overlap checking + // to render stage, which also ensure the returned list + // suitable for splitLine and splitArea rendering. + // (2) Scales except category always contain min max label so + // do not need to perform this process. + const showAllLabel = shouldShowAllLabels(axis); + const includeMinLabel = labelModel.get('showMinLabel') || showAllLabel; + const includeMaxLabel = labelModel.get('showMaxLabel') || showAllLabel; + + if (includeMinLabel && startTick > ordinalExtent[0]) { + addItem(ordinalExtent[0]); + } + + // Optimize: avoid generating large array by `ordinalScale.getTicks()`. + let tickValue = startTick; + for (; tickValue <= ordinalExtent[1]; tickValue += step) { + addItem(tickValue); + } + + if (includeMaxLabel && tickValue - step !== ordinalExtent[1]) { + addItem(ordinalExtent[1]); + } + + function addItem(tickValue: number) { + const tickObj = { value: tickValue }; + result.push( + onlyTick + ? tickValue + : { + formattedLabel: labelFormatter(tickObj), + rawLabel: ordinalScale.getLabel(tickObj), + tickValue: tickValue, + time: undefined, + break: undefined, + } + ); + } + + return result; } type CategoryIntervalCb = (tickVal: number, rawLabel: string) => boolean; @@ -551,38 +674,45 @@ type CategoryIntervalCb = (tickVal: number, rawLabel: string) => boolean; // When interval is function, the result `false` means ignore the tick. // It is time consuming for large category data. function makeLabelsByCustomizedCategoryInterval( - axis: Axis, categoryInterval: CategoryIntervalCb + axis: Axis, + categoryInterval: CategoryIntervalCb ): AxisLabelInfoDetermined[]; function makeLabelsByCustomizedCategoryInterval( - axis: Axis, categoryInterval: CategoryIntervalCb, onlyTick: false + axis: Axis, + categoryInterval: CategoryIntervalCb, + onlyTick: false ): AxisLabelInfoDetermined[]; function makeLabelsByCustomizedCategoryInterval( - axis: Axis, categoryInterval: CategoryIntervalCb, onlyTick: true + axis: Axis, + categoryInterval: CategoryIntervalCb, + onlyTick: true ): number[]; function makeLabelsByCustomizedCategoryInterval( - axis: Axis, categoryInterval: CategoryIntervalCb, onlyTick?: boolean + axis: Axis, + categoryInterval: CategoryIntervalCb, + onlyTick?: boolean ) { - const ordinalScale = axis.scale; - const labelFormatter = makeLabelFormatter(axis); - const result: (AxisLabelInfoDetermined | number)[] = []; - - zrUtil.each(ordinalScale.getTicks(), function (tick) { - const rawLabel = ordinalScale.getLabel(tick); - const tickValue = tick.value; - if (categoryInterval(tick.value, rawLabel)) { - result.push( - onlyTick - ? tickValue - : { - formattedLabel: labelFormatter(tick), - rawLabel: rawLabel, - tickValue: tickValue, - time: undefined, - break: undefined, - } - ); - } - }); + const ordinalScale = axis.scale; + const labelFormatter = makeLabelFormatter(axis); + const result: (AxisLabelInfoDetermined | number)[] = []; + + zrUtil.each(ordinalScale.getTicks(), function (tick) { + const rawLabel = ordinalScale.getLabel(tick); + const tickValue = tick.value; + if (categoryInterval(tick.value, rawLabel)) { + result.push( + onlyTick + ? tickValue + : { + formattedLabel: labelFormatter(tick), + rawLabel: rawLabel, + tickValue: tickValue, + time: undefined, + break: undefined, + } + ); + } + }); - return result; + return result; } diff --git a/test/axis-break-showMinLabel.html b/test/axis-break-showMinLabel.html new file mode 100644 index 0000000000..84bd2fa391 --- /dev/null +++ b/test/axis-break-showMinLabel.html @@ -0,0 +1,109 @@ + + + + + Axis Break showMinLabel Test + + + +
+ + +