diff --git a/src/axis/line.ts b/src/axis/line.ts index 79a78fd9f..5a78c33e5 100644 --- a/src/axis/line.ts +++ b/src/axis/line.ts @@ -1,8 +1,8 @@ import { IGroup } from '@antv/g-base'; import { vec2 } from '@antv/matrix-util'; -import { each, isFunction, isNil, isNumberEqual } from '@antv/util'; +import { each, isFunction, isNil, isNumberEqual, isObject } from '@antv/util'; import { ILocation } from '../interfaces'; -import { BBox, LineAxisCfg, Point, RegionLocationCfg } from '../types'; +import { AxisLabelAutoHideCfg, BBox, LineAxisCfg, Point, RegionLocationCfg } from '../types'; import Theme from '../util/theme'; import AxisBase from './base'; import * as OverlapUtil from './overlap'; @@ -121,7 +121,7 @@ class Line extends AxisBase implements ILocation } const overlapOrder = this.get('overlapOrder'); each(overlapOrder, (name) => { - if (labelCfg[name]) { + if (labelCfg[name] && this.canProcessOverlap(name)) { this.autoProcessOverlap(name, labelCfg[name], labelGroup, limitLength); } }); @@ -163,19 +163,41 @@ class Line extends AxisBase implements ILocation return maxLength; } + /** + * 是否可以执行某一 overlap + * @param name + */ + private canProcessOverlap(name: string) { + const labelCfg = this.get('label'); + + // 对 autoRotate,如果配置了旋转角度,直接进行固定角度旋转 + if (name === 'autoRotate') { + return !isNil(labelCfg.rotate); + } + + // 默认所有 overlap 都可执行 + return true; + } + private autoProcessOverlap(name: string, value: any, labelGroup: IGroup, limitLength: number) { const isVertical = this.isVertical(); let hasAdjusted = false; const util = OverlapUtil[name]; if (value === true) { const labelCfg = this.get('label'); - // 默认使用固定角度的旋转方案 - hasAdjusted = util.getDefault()(isVertical, labelGroup, limitLength, labelCfg.rotate); + // true 形式的配置:使用 overlap 默认的的处理方法进行处理 + hasAdjusted = util.getDefault()(isVertical, labelGroup, limitLength); } else if (isFunction(value)) { - // 用户可以传入回调函数 + // 回调函数形式的配置: 用户可以传入回调函数 hasAdjusted = value(isVertical, labelGroup, limitLength); + } else if (isObject(value)) { + // object 形式的配置方式:包括 处理方法 type, 和可选参数配置 cfg + const overlapCfg = value as { type: string; cfg?: AxisLabelAutoHideCfg }; + if (util[overlapCfg.type]) { + hasAdjusted = util[overlapCfg.type](isVertical, labelGroup, limitLength, overlapCfg.cfg); + } } else if (util[value]) { - // 按照名称执行旋转函数 + // 字符串类型的配置:按照名称执行 overlap 处理方法 hasAdjusted = util[value](isVertical, labelGroup, limitLength); } if (name === 'autoRotate') { diff --git a/src/axis/overlap/auto-hide.ts b/src/axis/overlap/auto-hide.ts index 36d5978e2..2be41c8c3 100644 --- a/src/axis/overlap/auto-hide.ts +++ b/src/axis/overlap/auto-hide.ts @@ -1,5 +1,5 @@ import { IElement, IGroup } from '@antv/g-base'; -import { getWrapBehavior } from '@antv/util'; +import { AxisLabelAutoHideCfg } from '../../types'; import { getMaxLabelWidth } from '../../util/label'; import { getAngleByMatrix } from '../../util/matrix'; import { near } from '../../util/util'; @@ -32,27 +32,31 @@ function getRotateAngle(label: IElement) { // } // 是否重叠 -function isOverlap(isVertical: boolean, first: IElement, second: IElement) { +function isOverlap(isVertical: boolean, first: IElement, second: IElement, minGap: number) { let overlap = false; const angle = getRotateAngle(first); const distance = isVertical ? Math.abs(second.attr('y') - first.attr('y')) : Math.abs(second.attr('x') - first.attr('x')); - const prevBBox = first.getBBox(); + const prevBBox = (isVertical + ? second.attr('y') > first.attr('y') + : second.attr('x') > first.attr('x')) + ? first.getBBox() + : second.getBBox(); if (isVertical) { const ratio = Math.abs(Math.cos(angle)); if (near(ratio, 0, Math.PI / 180)) { - overlap = prevBBox.width > distance; + overlap = prevBBox.width + minGap > distance; } else { - overlap = prevBBox.height > distance * ratio; + overlap = prevBBox.height / ratio + minGap > distance; } } else { const ratio = Math.abs(Math.sin(angle)); if (near(ratio, 0, Math.PI / 180)) { - overlap = prevBBox.width > distance; + overlap = prevBBox.width + minGap > distance; } else { - overlap = prevBBox.height > distance * ratio; + overlap = prevBBox.height / ratio + minGap > distance; } } @@ -60,7 +64,8 @@ function isOverlap(isVertical: boolean, first: IElement, second: IElement) { } // 保留第一个或者最后一个 -function reserveOne(isVertical: boolean, labelsGroup: IGroup, reversed: boolean) { +function reserveOne(isVertical: boolean, labelsGroup: IGroup, reversed: boolean, autoHideCfg?: AxisLabelAutoHideCfg) { + const minGap = autoHideCfg?.minGap || 0; const labels = labelsGroup .getChildren() .slice() // 复制数组 @@ -80,7 +85,7 @@ function reserveOne(isVertical: boolean, labelsGroup: IGroup, reversed: boolean) const label = labels[i]; const curBBox = label.getBBox(); // 不再考虑超出限制,而仅仅根据是否重叠进行隐藏 isOutLimit(isVertical, label, limitLength) || - const isHide = isOverlap(isVertical, prev, label); + const isHide = isOverlap(isVertical, prev, label, minGap); if (isHide) { label.hide(); hasHide = true; @@ -92,7 +97,8 @@ function reserveOne(isVertical: boolean, labelsGroup: IGroup, reversed: boolean) } // 均匀抽样隐藏标签,注意这里假设 label/tick 是均匀的 -function parityHide(isVertical: boolean, labelsGroup: IGroup) { +function parityHide(isVertical: boolean, labelsGroup: IGroup, autoHideCfg?: AxisLabelAutoHideCfg) { + const minGap = autoHideCfg?.minGap || 0; const labels = labelsGroup.getChildren().slice(); // 复制数组 if (labels.length < 2) { // 如果数量小于 2 则直接返回,等于 2 时可能也会重合 @@ -113,18 +119,18 @@ function parityHide(isVertical: boolean, labelsGroup: IGroup) { const ratio = Math.abs(Math.cos(angle)); if (near(ratio, 0, Math.PI / 180)) { const maxWidth = getMaxLabelWidth(labels); - interval = maxWidth / distance; + interval = (maxWidth + minGap) / distance; } else { - interval = firstBBox.height / (distance * ratio); + interval = (firstBBox.height / ratio + minGap) / distance; } } else { // 水平坐标轴 const ratio = Math.abs(Math.sin(angle)); if (near(ratio, 0, Math.PI / 180)) { const maxWidth = getMaxLabelWidth(labels); - interval = maxWidth / distance; + interval = (maxWidth + minGap) / distance; } else { - interval = firstBBox.height / (distance * ratio); + interval = (firstBBox.height / ratio + minGap) / distance; } } // interval > 1 时需要对 label 进行隐藏 @@ -149,26 +155,48 @@ export function getDefault() { * 保证首个 label 可见,即使超过 limitLength 也不隐藏 * @param {boolean} isVertical 是否垂直 * @param {IGroup} labelsGroup label 的分组 + * @param {number} limitLength 另一个方向的长度限制,autoHide 不关心 + * @param {AxisLabelAutoHideCfg} autoHideCfg autoHide overlap 的可选配置参数 */ -export function reserveFirst(isVertical: boolean, labelsGroup: IGroup): boolean { - return reserveOne(isVertical, labelsGroup, false); +export function reserveFirst( + isVertical: boolean, + labelsGroup: IGroup, + limitLength?: number, + autoHideCfg?: AxisLabelAutoHideCfg +): boolean { + return reserveOne(isVertical, labelsGroup, false, autoHideCfg); } /** * 保证最后一个 label 可见,即使超过 limitLength 也不隐藏 * @param {boolean} isVertical 是否垂直 * @param {IGroup} labelsGroup label 的分组 + * @param {number} limitLength 另一个方向的长度限制,autoHide 不关心 + * @param {AxisLabelAutoHideCfg} autoHideCfg autoHide overlap 的可选配置参数 */ -export function reserveLast(isVertical: boolean, labelsGroup: IGroup): boolean { - return reserveOne(isVertical, labelsGroup, true); +export function reserveLast( + isVertical: boolean, + labelsGroup: IGroup, + limitLength?: number, + autoHideCfg?: AxisLabelAutoHideCfg +): boolean { + return reserveOne(isVertical, labelsGroup, true, autoHideCfg); } /** * 保证第一个最后一个 label 可见,即使超过 limitLength 也不隐藏 * @param {boolean} isVertical 是否垂直 * @param {IGroup} labelsGroup label 的分组 + * @param {number} limitLength 另一个方向的长度限制,autoHide 不关心 + * @param {AxisLabelAutoHideCfg} autoHideCfg autoHide overlap 的可选配置参数 */ -export function reserveBoth(isVertical: boolean, labelsGroup: IGroup): boolean { +export function reserveBoth( + isVertical: boolean, + labelsGroup: IGroup, + limitLength?: number, + autoHideCfg?: AxisLabelAutoHideCfg +): boolean { + const minGap = autoHideCfg?.minGap || 0; const labels = labelsGroup.getChildren().slice(); // 复制数组 if (labels.length <= 2) { // 如果数量小于或等于 2 则直接返回 @@ -184,7 +212,7 @@ export function reserveBoth(isVertical: boolean, labelsGroup: IGroup): boolean { const label = labels[i]; const curBBox = label.getBBox(); // 废弃 isOutLimit(isVertical, label, limitLength) || - const isHide = isOverlap(isVertical, preLabel, label); + const isHide = isOverlap(isVertical, preLabel, label, minGap); if (isHide) { label.hide(); hasHide = true; @@ -193,7 +221,7 @@ export function reserveBoth(isVertical: boolean, labelsGroup: IGroup): boolean { } } - const overlap = isOverlap(isVertical, preLabel, last); + const overlap = isOverlap(isVertical, preLabel, last, minGap); if (overlap) { // 发生冲突,则隐藏前一个保留后一个 preLabel.hide(); @@ -206,9 +234,16 @@ export function reserveBoth(isVertical: boolean, labelsGroup: IGroup): boolean { * 保证 label 均匀显示 和 不出现重叠,主要解决文本层叠的问题,对于 limitLength 不处理 * @param {boolean} isVertical 是否垂直 * @param {IGroup} labelsGroup label 的分组 + * @param {number} limitLength 另一个方向的长度限制,autoHide 不关心 + * @param {AxisLabelAutoHideCfg} autoHideCfg autoHide overlap 的可选配置参数 */ -export function equidistance(isVertical: boolean, labelsGroup: IGroup): boolean { - let hasHide = parityHide(isVertical, labelsGroup); +export function equidistance( + isVertical: boolean, + labelsGroup: IGroup, + limitLength?: number, + autoHideCfg?: AxisLabelAutoHideCfg +): boolean { + let hasHide = parityHide(isVertical, labelsGroup, autoHideCfg); // 处理 timeCat 类型的 tick,在均匀的基础上,再次检查出现重叠的进行隐藏 if (reserveOne(isVertical, labelsGroup, false)) { @@ -222,10 +257,17 @@ export function equidistance(isVertical: boolean, labelsGroup: IGroup): boolean * 同 equidistance, 首先会保证 labels 均匀显示,然后会保留首尾 * @param isVertical * @param labelsGroup + * @param {number} limitLength 另一个方向的长度限制,autoHide 不关心 + * @param {AxisLabelAutoHideCfg} autoHideCfg autoHide overlap 的可选配置参数 */ -export function equidistanceWithReverseBoth(isVertical: boolean, labelsGroup: IGroup): boolean { +export function equidistanceWithReverseBoth( + isVertical: boolean, + labelsGroup: IGroup, + limitLength?: number, + autoHideCfg?: AxisLabelAutoHideCfg +): boolean { const labels = labelsGroup.getChildren().slice(); // 复制数组 - let hasHide = parityHide(isVertical, labelsGroup); + let hasHide = parityHide(isVertical, labelsGroup, autoHideCfg); if (labels.length > 2) { const first = labels[0]; @@ -234,7 +276,7 @@ export function equidistanceWithReverseBoth(isVertical: boolean, labelsGroup: IG // 如果第一个被隐藏了 if (!first.get('visible')) { first.show(); - if (reserveOne(isVertical, labelsGroup, false)) { + if (reserveOne(isVertical, labelsGroup, false, autoHideCfg)) { hasHide = true; } } @@ -242,7 +284,7 @@ export function equidistanceWithReverseBoth(isVertical: boolean, labelsGroup: IG // 如果最后一个被隐藏了 if (!last.get('visible')) { last.show(); - if (reserveOne(isVertical, labelsGroup, true)) { + if (reserveOne(isVertical, labelsGroup, true, autoHideCfg)) { hasHide = true; } } diff --git a/src/types.ts b/src/types.ts index 78790955b..cb553c754 100644 --- a/src/types.ts +++ b/src/types.ts @@ -156,6 +156,12 @@ export interface AxisTickLineCfg { type avoidCallback = (isVertical: boolean, labelGroup: IGroup, limitLength?: number) => boolean; +/** 坐标轴自动隐藏的配置 */ +export interface AxisLabelAutoHideCfg { + /** 最小间距配置 */ + minGap?: number; +} + /** * @interface * 坐标轴文本定义 @@ -188,9 +194,9 @@ export interface AxisLabelCfg { autoRotate?: boolean | avoidCallback | string; /** * 是否自动隐藏,默认 false - * @type {boolean|avoidCallback|string} + * @type {boolean|avoidCallback|string|{type:string,cfg?:AxisLabelAutoHideCfg}} */ - autoHide?: boolean | avoidCallback | string; + autoHide?: boolean | avoidCallback | string | { type: string; cfg?: AxisLabelAutoHideCfg }; /** * 是否自动省略,默认 false * @type {boolean|avoidCallback|string} diff --git a/tests/unit/axis/auto-hide-spec.ts b/tests/unit/axis/auto-hide-spec.ts index 6cb444a27..7105ddf3a 100644 --- a/tests/unit/axis/auto-hide-spec.ts +++ b/tests/unit/axis/auto-hide-spec.ts @@ -1,38 +1,63 @@ -import { Canvas } from '@antv/g-canvas'; +import { Canvas, IGroup } from '@antv/g-canvas'; import * as HideUtil from '../../../src/axis/overlap/auto-hide'; import { getMatrixByAngle } from '../../../src/util/matrix'; +const doAddLabels = (group: IGroup, labels: string[], dx: number, dy: number, angle?: number) => { + group.clear(); + labels.forEach((label, index) => { + const x = 100 + dx * index; + const y = 100 + dy * index; + const shape = group.addShape({ + type: 'text', + id: `label${index}`, + attrs: { + x, + y, + text: label, + fill: 'red', + }, + }); + if (angle) { + const matrix = getMatrixByAngle({ x, y }, angle); + shape.attr('matrix', matrix); + } + }); +}; + +const hasOverlap = (group: IGroup, minGap = 0) => { + const labels = group.getChildren().filter((child) => child.get('visible')); + if (labels.length < 2) { + return false; + } + const isVertical = labels[1].attr('x') === labels[0].attr('x'); + const sorted = labels.sort((label) => (isVertical ? label.attr('x') : label.attr('y'))); + let prev = sorted[0]; + for (let i = 1; i < sorted.length; i += 1) { + if (isVertical) { + if (prev.getBBox().maxY + minGap > sorted[i].getBBox().minY) { + return true; + } + } else { + if (prev.getBBox().maxX + minGap > sorted[i].getBBox().minX) { + return true; + } + } + prev = sorted[i]; + } + return false; +}; + describe('test auto hide', () => { const dom = document.createElement('div'); document.body.appendChild(dom); - dom.id = 'cah'; const canvas = new Canvas({ - container: 'cah', + container: dom, width: 1000, height: 1000, }); const group = canvas.addGroup(); const labels = ['123', '12', '2344', '13455222', '2345', '2333', '222', '2222', '11', '33']; - function addLabels(dx, dy, angle?) { - group.clear(); - labels.forEach((label, index) => { - const x = 100 + dx * index; - const y = 100 + dy * index; - const shape = group.addShape({ - type: 'text', - attrs: { - x, - y, - text: label, - fill: 'red', - }, - }); - if (angle) { - const matrix = getMatrixByAngle({ x, y }, angle); - shape.attr('matrix', matrix); - } - }); - } + const addLabels = (dx: number, dy: number, angle?: number) => doAddLabels(group, labels, dx, dy, angle); function addLabel(label: string, dx: number, dy: number, angle?: number) { const last = group.getLast(); const x = (last ? last.attr('x') : 100) + dx; @@ -386,4 +411,298 @@ describe('test auto hide', () => { expect(label.attr('text')).toBe(labels[idx * 2]); }); }); + + afterAll(() => { + dom.remove(); + }); +}); + +describe('auto hide /w cfg', () => { + const dom = document.createElement('div'); + document.body.appendChild(dom); + const canvas = new Canvas({ + container: dom, + width: 1000, + height: 1000, + }); + const group = canvas.addGroup(); + const COUNT = 8; + const evenLabels = []; + for (let i = 0; i < COUNT; i += 1) { + evenLabels.push(`2020-12-0${i}`); + } + const labels = ['123', '12', '2344', '13455222', '2345', '2333', '222', '2222', '11', '33']; + const addEvenLabels = (dx: number, dy: number, angle?: number) => doAddLabels(group, evenLabels, dx, dy, angle); + const addLabels = (dx: number, dy: number, angle?: number) => doAddLabels(group, labels, dx, dy, angle); + + function getCount() { + const children = group.getChildren(); + let count = 0; + children.forEach((shape) => { + if (shape.get('visible')) { + count++; + } + }); + return count; + } + addEvenLabels(0, 0); + const itemLen = group.getFirst().getBBox().width; + + it('equidistance, horizontal', () => { + addEvenLabels(itemLen, 0); + // 默认配置: 无最小间距 → 全部显示 + HideUtil.equidistance(false, group); + expect(getCount()).toBe(COUNT); + expect(hasOverlap(group)).toBeFalsy(); + + // 设置最小间距 → 抽样 + HideUtil.equidistance(false, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + expect(hasOverlap(group)).toBeFalsy(); + HideUtil.equidistance(false, group, undefined, { minGap: itemLen + 1 }); + expect(getCount()).toBe(Math.ceil(COUNT / 4)); + expect(hasOverlap(group)).toBeFalsy(); + }); + + it('equidistance, horizontal rotate', () => { + // 45° + // 无最小间距 → 全部显示 + addEvenLabels(13 / Math.sin(Math.PI / 4), 0, Math.PI / 4); + HideUtil.equidistance(false, group); + expect(getCount()).toBe(COUNT); + // 设置最小间距 → 抽样 + addEvenLabels(12 / Math.sin(Math.PI / 4), 0, Math.PI / 4); + HideUtil.equidistance(false, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + + // 90° + // 无最小间距 → 全部显示 + addEvenLabels(13, 0, Math.PI / 2); + HideUtil.equidistance(false, group); + expect(getCount()).toBe(COUNT); + // 设置最小间距 → 抽样 + addEvenLabels(13, 0, Math.PI / 2); + HideUtil.equidistance(false, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + + // 135° + // 无最小间距 → 全部显示 + addEvenLabels(13 / Math.sin((3 * Math.PI) / 4), 0, (3 * Math.PI) / 4); + HideUtil.equidistance(false, group); + expect(getCount()).toBe(COUNT); + // 设置最小间距 → 抽样 + addEvenLabels(13 / Math.sin((3 * Math.PI) / 4), 0, (3 * Math.PI) / 4); + HideUtil.equidistance(false, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + }); + + it('equidistance, vertical', () => { + // 无最小间距 → 全部显示 + addEvenLabels(0, 13); + HideUtil.equidistance(true, group); + expect(getCount()).toBe(COUNT); + expect(hasOverlap(group)).toBeFalsy(); + + // 设置最小间距 → 抽样 + addEvenLabels(0, 13); + HideUtil.equidistance(true, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + expect(hasOverlap(group)).toBeFalsy(); + }); + + it('equidistance, vertical, rotate', () => { + // 45° + // 无最小间距 → 全部显示 + addEvenLabels(0, 13 / Math.sin(Math.PI / 4), Math.PI / 4); + HideUtil.equidistance(true, group); + expect(getCount()).toBe(COUNT); + addEvenLabels(0, 13 / Math.sin(Math.PI / 4), Math.PI / 4); + HideUtil.equidistance(true, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + + // 90° + addEvenLabels(0, (itemLen + 1) / Math.sin(Math.PI / 2), Math.PI / 2); + HideUtil.equidistance(true, group); + expect(getCount()).toBe(COUNT); + addEvenLabels(0, (itemLen + 1) / Math.sin(Math.PI / 2), Math.PI / 2); + HideUtil.equidistance(true, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + + // 145° + addEvenLabels(0, 13 / Math.sin((3 * Math.PI) / 4), (3 * Math.PI) / 4); + HideUtil.equidistance(true, group); + expect(getCount()).toBe(COUNT); + addEvenLabels(0, 13 / Math.sin((Math.PI * 3) / 4), (Math.PI * 3) / 4); + HideUtil.equidistance(true, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + }); + + it('equidistanceWithReserveBoth, horizontal', () => { + addEvenLabels(itemLen + 1, 0); + HideUtil.equidistanceWithReverseBoth(false, group); + expect(getCount()).toBe(COUNT); + expect(hasOverlap(group)).toBeFalsy(); + + addEvenLabels(itemLen + 1, 0); + HideUtil.equidistanceWithReverseBoth(false, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById(`label${COUNT - 1}`).get('visible')).toBeTruthy(); + expect(hasOverlap(group)).toBeFalsy(); + }); + + it('equidistanceWithReserveBoth, vertical', () => { + addEvenLabels(0, 13); + HideUtil.equidistanceWithReverseBoth(true, group); + expect(getCount()).toBe(COUNT); + expect(hasOverlap(group)).toBeFalsy(); + + addEvenLabels(0, 13); + HideUtil.equidistanceWithReverseBoth(true, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById(`label${COUNT - 1}`).get('visible')).toBeTruthy(); + expect(hasOverlap(group)).toBeFalsy(); + }); + + it('equidistanceWithReverseBoth, vertical, rotate', () => { + // 45° + addEvenLabels(0, 13 / Math.sin(Math.PI / 4), Math.PI / 4); + HideUtil.equidistanceWithReverseBoth(true, group); + expect(getCount()).toBe(COUNT); + + addEvenLabels(0, 13 / Math.sin(Math.PI / 4), Math.PI / 4); + HideUtil.equidistanceWithReverseBoth(true, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById(`label${COUNT - 1}`).get('visible')).toBeTruthy(); + + // 90° + addEvenLabels(0, (itemLen + 1) / Math.sin(Math.PI / 2), Math.PI / 2); + HideUtil.equidistanceWithReverseBoth(true, group); + expect(getCount()).toBe(COUNT); + addEvenLabels(0, (itemLen + 1) / Math.sin(Math.PI / 2), Math.PI / 2); + HideUtil.equidistanceWithReverseBoth(true, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById(`label${COUNT - 1}`).get('visible')).toBeTruthy(); + + // 135° + addEvenLabels(0, 13 / Math.sin((Math.PI * 3) / 4), (Math.PI * 3) / 4); + HideUtil.equidistanceWithReverseBoth(true, group); + expect(getCount()).toBe(COUNT); + addEvenLabels(0, 13 / Math.sin((Math.PI * 3) / 4), (Math.PI * 3) / 4); + HideUtil.equidistanceWithReverseBoth(true, group, undefined, { minGap: 6 }); + expect(getCount()).toBe(Math.ceil(COUNT / 2)); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById(`label${COUNT - 1}`).get('visible')).toBeTruthy(); + }); + + it('reserveFirst, horizontal', () => { + addLabels(20, 0); + HideUtil.reserveFirst(false, group); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById('label1').get('visible')).toBeTruthy(); + expect(hasOverlap(group)).toBeFalsy(); + + addLabels(20, 0); + HideUtil.reserveFirst(false, group, undefined, { minGap: 6 }); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById('label1').get('visible')).toBeFalsy(); + expect(hasOverlap(group)).toBeFalsy(); + }); + + it('reserveFirst, horizontal, rotate', () => { + addLabels(13 / Math.sin(Math.PI / 4), 0, Math.PI / 4); + HideUtil.reserveFirst(false, group); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById('label1').get('visible')).toBeTruthy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, Math.PI / 4); + HideUtil.reserveFirst(false, group, undefined, { minGap: 6 }); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById('label1').get('visible')).toBeFalsy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, -Math.PI / 4); + HideUtil.reserveFirst(false, group); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById('label1').get('visible')).toBeTruthy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, -Math.PI / 4); + HideUtil.reserveFirst(false, group, undefined, { minGap: 6 }); + expect(group.findById('label0').get('visible')).toBeTruthy(); + expect(group.findById('label1').get('visible')).toBeFalsy(); + }); + + it('reserveLast, horizontal', () => { + addLabels(20, 0); + HideUtil.reserveLast(false, group); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 2}`).get('visible')).toBeTruthy(); + expect(hasOverlap(group)).toBeFalsy(); + + addLabels(20, 0); + HideUtil.reserveLast(false, group, undefined, { minGap: 12 }); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 2}`).get('visible')).toBeFalsy(); + expect(hasOverlap(group)).toBeFalsy(); + }); + + it('reserveLast, horizontal, rotated', () => { + addLabels(13 / Math.sin(Math.PI / 4), 0, Math.PI / 4); + HideUtil.reserveLast(false, group); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 2}`).get('visible')).toBeTruthy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, Math.PI / 4); + HideUtil.reserveLast(false, group, undefined, { minGap: 6 }); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 2}`).get('visible')).toBeFalsy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, -Math.PI / 4); + HideUtil.reserveLast(false, group); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 2}`).get('visible')).toBeTruthy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, -Math.PI / 4); + HideUtil.reserveLast(false, group, undefined, { minGap: 6 }); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 2}`).get('visible')).toBeFalsy(); + }); + + it('reverseBoth, horizontal', () => { + addLabels(20, 0); + HideUtil.reserveBoth(false, group); + expect(group.findById(`label0`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + expect(hasOverlap(group)).toBeFalsy(); + + addLabels(20, 0); + HideUtil.reserveBoth(false, group, undefined, { minGap: 12 }); + expect(group.findById(`label0`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + expect(hasOverlap(group)).toBeFalsy(); + }); + + it('reserveBoth, horizontal, rotated', () => { + addLabels(13 / Math.sin(Math.PI / 4), 0, Math.PI / 4); + HideUtil.reserveBoth(false, group); + expect(group.findById(`label0`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, Math.PI / 4); + HideUtil.reserveBoth(false, group, undefined, { minGap: 6 }); + expect(group.findById(`label0`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, -Math.PI / 4); + HideUtil.reserveBoth(false, group); + expect(group.findById(`label0`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + + addLabels(13 / Math.sin(Math.PI / 4), 0, -Math.PI / 4); + HideUtil.reserveBoth(false, group, undefined, { minGap: 6 }); + expect(group.findById(`label0`).get('visible')).toBeTruthy(); + expect(group.findById(`label${labels.length - 1}`).get('visible')).toBeTruthy(); + }); });