Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support label firstVisible in autoHide and linear axis sampling #1470

Merged
merged 4 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@visactor/vrender-components",
"comment": "feat: support axis label `firstVisible` in autoHide and linear axis sampling",
"type": "none"
}
],
"packageName": "@visactor/vrender-components"
}
4 changes: 3 additions & 1 deletion packages/vrender-components/src/axis/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ export class LineAxis extends AxisBase<LineAxisAttributes> {
autoHideMethod,
autoHideSeparation,
lastVisible,
firstVisible,
autoWrap,
overflowLimitLength
} = label;
Expand Down Expand Up @@ -568,7 +569,8 @@ export class LineAxis extends AxisBase<LineAxisAttributes> {
orient,
method: autoHideMethod,
separation: autoHideSeparation,
lastVisible
lastVisible,
firstVisible
});
}
}
Expand Down
90 changes: 48 additions & 42 deletions packages/vrender-components/src/axis/overlap/auto-hide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
* @description 自动隐藏
*/

import type { IText } from '@visactor/vrender-core';
import type { IBounds } from '@visactor/vutils';
import { createRect, type IText } from '@visactor/vrender-core';
// eslint-disable-next-line no-duplicate-imports
import { isEmpty, isFunction, last } from '@visactor/vutils';
import type { CustomMethod } from '../type';
import { textIntersect as intersect, hasOverlap } from '../util';

const methods = {
parity: function (items: IText[]) {
Expand All @@ -24,25 +24,6 @@ const methods = {
}
};

function intersect(textA: IText, textB: IText, sep: number) {
let a: IBounds = textA.OBBBounds;
let b: IBounds = textB.OBBBounds;
if (a && b && !a.empty() && !b.empty()) {
return a.intersects(b);
}
a = textA.AABBBounds;
b = textB.AABBBounds;
return sep > Math.max(b.x1 - a.x2, a.x1 - b.x2, b.y1 - a.y2, a.y1 - b.y2);
}

function hasOverlap(items: IText[], pad: number) {
for (let i = 1, n = items.length, a = items[0], b; i < n; a = b, ++i) {
if (intersect(a, (b = items[i]), pad)) {
return true;
}
}
}

function hasBounds(item: IText) {
let bounds;
if (!item.OBBBounds.empty()) {
Expand All @@ -59,6 +40,23 @@ function reset(items: IText[]) {
return items;
}

function forceItemVisible(sourceItem: IText, items: IText[], check: boolean, comparator: any, inverse = false) {
if (check && !sourceItem.attribute.opacity) {
const remainLength = items.length;
if (remainLength > 1) {
sourceItem.setAttribute('opacity', 1);
for (let i = 0; i < remainLength; i++) {
const item = inverse ? items[remainLength - 1 - i] : items[i];
if (comparator(item)) {
item.setAttribute('opacity', 0);
} else {
break;
}
}
}
}
}

type HideConfig = {
/**
* 轴的方向
Expand All @@ -79,6 +77,10 @@ type HideConfig = {
* 保证最后的label展示
*/
lastVisible?: boolean;
/**
* 保证第一个的label展示
*/
firstVisible?: boolean;
};

export function autoHide(labels: IText[], config: HideConfig) {
Expand All @@ -91,7 +93,7 @@ export function autoHide(labels: IText[], config: HideConfig) {
return;
}

let items;
let items: IText[];

items = reset(source);

Expand All @@ -106,27 +108,31 @@ export function autoHide(labels: IText[], config: HideConfig) {
/**
* 0.17.10 之前,当最后label个数小于3 的时候,才做最后的label强制显示的策略
*/
const checkLast = items.length < 3 || config.lastVisible;

if (checkLast) {
const lastSourceItem = last(source);

if (!lastSourceItem.attribute.opacity) {
const remainLength = items.length;
if (remainLength > 1) {
lastSourceItem.setAttribute('opacity', 1);

for (let i = remainLength - 1; i >= 0; i--) {
if (intersect(items[i], lastSourceItem, sep)) {
items[i].setAttribute('opacity', 0);
} else {
// 当遇到第一个不相交的label的时候,就可以停止了
break;
}
}
}
}

const shouldCheck = (length: number, visibility: boolean) => length < 3 || visibility;

const checkFirst = shouldCheck(items.length, config.firstVisible);
let checkLast = shouldCheck(items.length, config.lastVisible);

const firstSourceItem = source[0];
const lastSourceItem = last(source);

if (intersect(firstSourceItem, lastSourceItem, sep)) {
lastSourceItem.setAttribute('opacity', 0); // Or firstSourceItem, depending on preference
checkLast = false;
}

forceItemVisible(firstSourceItem, items, checkFirst, (item: IText) => intersect(item, firstSourceItem, sep));

forceItemVisible(
lastSourceItem,
items,
checkLast,
(item: IText) =>
intersect(item, lastSourceItem, sep) ||
(checkFirst && item !== firstSourceItem ? intersect(item, firstSourceItem, sep) : false),
true
);
}

source.forEach(item => {
Expand Down
82 changes: 76 additions & 6 deletions packages/vrender-components/src/axis/tick-data/continuous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { isContinuous } from '@visactor/vscale';
import { isFunction, isValid, last } from '@visactor/vutils';
import type { ICartesianTickDataOpt, ILabelItem, ITickData, ITickDataOpt } from '../type';
// eslint-disable-next-line no-duplicate-imports
import { convertDomainToTickData, getCartesianLabelBounds, hasOverlap, intersect } from './util';

import { convertDomainToTickData, getCartesianLabelBounds } from './util';
import { textIntersect as intersect, hasOverlap } from '../util';
function getScaleTicks(
op: ITickDataOpt,
scale: ContinuousScale,
Expand Down Expand Up @@ -38,6 +38,34 @@ function getScaleTicks(
return scaleTicks;
}

function forceItemVisible(
sourceItem: ILabelItem<number>,
items: ILabelItem<number>[],
check: boolean,
comparator: any,
inverse = false
) {
if (check && !items.includes(sourceItem)) {
let remainLength = items.length;
if (remainLength > 1) {
if (inverse) {
items.push(sourceItem);
} else {
items.unshift(sourceItem);
}
for (let i = 0; i < remainLength; i++) {
const index = inverse ? remainLength - 1 - i : i;
if (comparator(items[index])) {
items.splice(index, 1);
i--;
remainLength--;
} else {
break;
}
}
}
}
}
/** 连续轴默认 tick 数量 */
export const DEFAULT_CONTINUOUS_TICK_COUNT = 5;
/**
Expand Down Expand Up @@ -97,7 +125,20 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick
});
}

if (op.sampling) {
const domain = scale.domain();

if (op.labelFirstVisible && domain[0] !== scaleTicks[0] && !scaleTicks.includes(domain[0])) {
scaleTicks.unshift(domain[0]);
}

if (
op.labelLastVisible &&
domain[domain.length - 1] !== scaleTicks[scaleTicks.length - 1] &&
!scaleTicks.includes(domain[domain.length - 1])
) {
scaleTicks.push(domain[domain.length - 1]);
}
if (op.sampling && scaleTicks.length > 1) {
// 判断重叠
if (op.coordinateType === 'cartesian' || (op.coordinateType === 'polar' && op.axisOrientType === 'radius')) {
const { labelGap = 4, labelFlush } = op as ICartesianTickDataOpt;
Expand All @@ -108,10 +149,40 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick
value: scaleTicks[i]
} as ILabelItem<number>)
);
const source = [...items];
const firstSourceItem = source[0];
const lastSourceItem = last(source);

const samplingMethod = breakData && breakData() ? methods.greedy : methods.parity; // 由于轴截断后刻度会存在不均匀的情况,所以不能使用 parity 算法
while (items.length >= 3 && hasOverlap(items, labelGap)) {
while (items.length >= 3 && hasOverlap(items as any, labelGap)) {
items = samplingMethod(items, labelGap);
}

const shouldCheck = (length: number, visibility: boolean) => length < 3 || visibility;

const checkFirst = shouldCheck(items.length, op.labelFirstVisible);
let checkLast = shouldCheck(items.length, op.labelLastVisible);

if (intersect(firstSourceItem as any, lastSourceItem as any, labelGap)) {
if (items.includes(lastSourceItem) && items.length > 1) {
items.splice(items.indexOf(lastSourceItem), 1);
checkLast = false;
}
}

forceItemVisible(firstSourceItem, items, checkFirst, (item: ILabelItem<number>) =>
intersect(item as any, firstSourceItem as any, labelGap)
);
forceItemVisible(
lastSourceItem,
items,
checkLast,
(item: ILabelItem<number>) =>
intersect(item as any, lastSourceItem as any, labelGap) ||
(checkFirst && item !== firstSourceItem ? intersect(item as any, firstSourceItem as any, labelGap) : false),
true
);

const ticks = items.map(item => item.value);

if (ticks.length < 3 && labelFlush) {
Expand All @@ -126,7 +197,6 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick
scaleTicks = ticks;
}
}

return convertDomainToTickData(scaleTicks);
};

Expand All @@ -137,7 +207,7 @@ const methods = {
greedy: function <T>(items: ILabelItem<T>[], sep: number) {
let a: ILabelItem<T>;
return items.filter((b, i) => {
if (!i || !intersect(a.AABBBounds, b.AABBBounds, sep)) {
if (!i || !intersect(a as any, b as any, sep)) {
a = b;
return true;
}
Expand Down
18 changes: 1 addition & 17 deletions packages/vrender-components/src/axis/tick-data/util.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { IBaseScale } from '@visactor/vscale';
import type { IBoundsLike } from '@visactor/vutils';
// eslint-disable-next-line no-duplicate-imports
import { AABBBounds, degreeToRadian } from '@visactor/vutils';
import type { TextAlignType, TextBaselineType } from '@visactor/vrender-core';
import { initTextMeasure } from '../../util/text';
import type { ICartesianTickDataOpt, ILabelItem, IOrientType, ITickData } from '../type';
import type { ICartesianTickDataOpt, IOrientType, ITickData } from '../type';

export const convertDomainToTickData = (domain: any[]): ITickData[] => {
const ticks = domain.map((t: number, index: number) => {
Expand Down Expand Up @@ -42,20 +40,6 @@ export const labelDistance = (prevLabel: AABBBounds, nextLabel: AABBBounds): [nu
return [horizontal, vertical];
};

export function intersect(a: IBoundsLike, b: IBoundsLike, sep: number) {
return sep > Math.max(b.x1 - a.x2, a.x1 - b.x2, b.y1 - a.y2, a.y1 - b.y2);
}

export function hasOverlap<T>(items: ILabelItem<T>[], pad: number): boolean {
for (let i = 1, n = items.length, a = items[0], b; i < n; a = b, ++i) {
b = items[i];
if (intersect(a.AABBBounds, b.AABBBounds, pad)) {
return true;
}
}
return false;
}

export const MIN_TICK_GAP = 12;

const calculateFlushPos = (basePosition: number, size: number, rangePosition: number, otherEnd: number) => {
Expand Down
9 changes: 8 additions & 1 deletion packages/vrender-components/src/axis/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ export interface LineAxisAttributes extends Omit<AxisBaseAttributes, 'label'> {
* @since 0.17.10
*/
lastVisible?: boolean;
/**
* 保证第一个的label必须展示
* @default false
* @since 0.20.7
*/
firstVisible?: boolean;
};
/**
* 坐标轴背景配置
Expand Down Expand Up @@ -538,6 +544,8 @@ export interface ITickDataOpt {
labelFormatter?: (value: any) => string;
labelStyle: ITextGraphicAttribute;
labelGap?: number;
labelFirstVisible?: boolean;
labelLastVisible?: boolean;
/**
* 截断数据范围配置
*/
Expand All @@ -546,7 +554,6 @@ export interface ITickDataOpt {

export interface ICartesianTickDataOpt extends ITickDataOpt {
axisOrientType: IOrientType;
labelLastVisible: boolean;
labelFlush: boolean;
/**
* 截断数据范围配置
Expand Down
25 changes: 23 additions & 2 deletions packages/vrender-components/src/axis/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// eslint-disable-next-line no-duplicate-imports
import type { IGraphic, IGroup, ITextGraphicAttribute, TextAlignType, TextBaselineType } from '@visactor/vrender-core';
import type { Dict } from '@visactor/vutils';
import type { IGraphic, IGroup, IText, TextAlignType, TextBaselineType } from '@visactor/vrender-core';
import type { Dict, IBounds } from '@visactor/vutils';
// eslint-disable-next-line no-duplicate-imports
import { isGreater, isLess, tau, normalizeAngle, polarToCartesian, merge } from '@visactor/vutils';
import { traverseGroup } from '../util/common';
Expand Down Expand Up @@ -150,3 +150,24 @@ export function getPolygonPath(points: Point[], closed: boolean) {

return path;
}

export function textIntersect(textA: IText, textB: IText, sep: number) {
let a: IBounds = textA.OBBBounds;
let b: IBounds = textB.OBBBounds;
if (a && b && !a.empty() && !b.empty()) {
return a.intersects(b);
}
a = textA.AABBBounds;
b = textB.AABBBounds;
return sep > Math.max(b.x1 - a.x2, a.x1 - b.x2, b.y1 - a.y2, a.y1 - b.y2);
}

export function hasOverlap<T>(items: IText[], pad: number): boolean {
for (let i = 1, n = items.length, a = items[0], b; i < n; a = b, ++i) {
b = items[i];
if (textIntersect(a, b, pad)) {
return true;
}
}
return false;
}
Loading