@antv/g2
Version:
the Grammar of Graphics in Javascript
252 lines (234 loc) • 6.32 kB
text/typescript
import { each } from '@antv/util';
import { BBox, IGroup, IShape } from '../../../dependents';
import { LabelItem } from '../interface';
const MAX_TIMES = 100;
/** @ignore */
interface Bitmap {
[key: number]: {
[key: number]: boolean;
};
}
/** @ignore */
interface GreedyCfg {
readonly xGap?: number;
readonly yGap?: number;
}
/**
* @ignore
* Greedy 贪婪算法
*/
class Greedy {
public readonly xGap: number;
/** optimizing for text overlapping detection: use a min text height as gap */
public readonly yGap: number;
private bitmap: Bitmap = {};
constructor(cfg: GreedyCfg = {}) {
const { xGap = 1, yGap = 8 } = cfg;
this.xGap = xGap;
this.yGap = yGap;
}
public hasGap(bbox: BBox): boolean {
let hasGap = true;
const bitmap = this.bitmap;
const minX = Math.round(bbox.minX);
const maxX = Math.round(bbox.maxX);
const minY = Math.round(bbox.minY);
const maxY = Math.round(bbox.maxY);
for (let i = minX; i <= maxX; i += 1) {
if (!bitmap[i]) {
bitmap[i] = {};
continue;
}
if (i === minX || i === maxX) {
for (let j = minY; j <= maxY; j++) {
if (bitmap[i][j]) {
hasGap = false;
break;
}
}
} else {
if (bitmap[i][minY] || bitmap[i][maxY]) {
hasGap = false;
break;
}
}
}
return hasGap;
}
public fillGap(bbox: BBox): void {
const bitmap = this.bitmap;
const minX = Math.round(bbox.minX);
const maxX = Math.round(bbox.maxX);
const minY = Math.round(bbox.minY);
const maxY = Math.round(bbox.maxY);
// filling grid
for (let i = minX; i <= maxX; i += 1) {
if (!bitmap[i]) {
bitmap[i] = {};
}
}
for (let i = minX; i <= maxX; i += this.xGap) {
for (let j = minY; j <= maxY; j += this.yGap) {
bitmap[i][j] = true;
}
bitmap[i][maxY] = true;
}
// filling y edges
if (this.yGap !== 1) {
for (let i = minY; i <= maxY; i += 1) {
bitmap[minX][i] = true;
bitmap[maxX][i] = true;
}
}
// filling x edges
if (this.xGap !== 1) {
for (let i = minX; i <= maxX; i += 1) {
bitmap[i][minY] = true;
bitmap[i][maxY] = true;
}
}
}
public destroy(): void {
this.bitmap = {};
}
}
function spiralFill(label: IShape, greedy: Greedy, maxTimes: number = MAX_TIMES) {
const dt = -1;
const { x, y } = label.attr();
const bbox = label.getCanvasBBox();
const maxDelta = Math.sqrt(bbox.width * bbox.width + bbox.height * bbox.height);
let dxdy;
let t = -dt;
let dx = 0;
let dy = 0;
const f = (param: number) => {
const nt = param * 0.1;
return [nt * Math.cos(nt), nt * Math.sin(nt)];
};
if (greedy.hasGap(bbox)) {
greedy.fillGap(bbox);
return true;
}
let canFill = false;
let times = 0;
const accessedCache = {};
while (Math.min(Math.abs(dx), Math.abs(dy)) < maxDelta && times < maxTimes) {
dxdy = f((t += dt));
dx = ~~dxdy[0];
dy = ~~dxdy[1];
if ((!dx && !dy) || accessedCache[`${dx}-${dy}`]) {
continue;
}
label.attr({ x: x + dx, y: y + dy });
if (dx + dy < 0) {
label.attr('textAlign', 'right');
}
times++;
if (greedy.hasGap(label.getCanvasBBox())) {
greedy.fillGap(label.getCanvasBBox());
canFill = true;
accessedCache[`${dx}-${dy}`] = true;
break;
}
}
return canFill;
}
/*
* 根据如下规则尝试放置label
* 5
* ------------------
* | 1 | 0 |
* 8 —————————4———————— 7
* | 2 | 3 |
* ——————————————————
* 6
*/
function adjustLabelPosition(label: IShape, x: number, y: number, index: number) {
const { width, height } = label.getCanvasBBox();
const attrs = {
x,
y,
textAlign: 'center',
};
switch (index) {
case 0:
attrs.y -= height + 1;
attrs.x += 1;
attrs.textAlign = 'left';
break;
case 1:
attrs.y -= height + 1;
attrs.x -= 1;
attrs.textAlign = 'right';
break;
case 2:
attrs.y += height + 1;
attrs.x -= 1;
attrs.textAlign = 'right';
break;
case 3:
attrs.y += height + 1;
attrs.x += 1;
attrs.textAlign = 'left';
break;
case 5:
attrs.y -= height * 2 + 2;
break;
case 6:
attrs.y += height * 2 + 2;
break;
case 7:
attrs.x += width + 1;
attrs.textAlign = 'left';
break;
case 8:
attrs.x -= width + 1;
attrs.textAlign = 'right';
break;
default:
break;
}
label.attr(attrs);
return label.getCanvasBBox();
}
/**
* @ignore
* label 防遮挡布局:在不改变 label 位置的情况下对相互重叠的 label 进行调整。
* 不同于 'overlap' 类型的布局,该布局不会对 label 的位置进行偏移调整。
* @param labels 参与布局调整的 label 数组集合
*/
export function fixedOverlap(items: LabelItem[], labels: IGroup[], shapes: IShape[] | IGroup[], region: BBox) {
const greedy = new Greedy();
each(labels, (label: IGroup) => {
const labelShape = label.find((shape) => shape.get('type') === 'text') as IShape;
if (!spiralFill(labelShape, greedy)) {
label.remove(true);
}
});
greedy.destroy();
}
/**
* @ignore
* label 防遮挡布局:为了防止 label 之间相互覆盖同时保证尽可能多 的 label 展示,通过尝试将 label 向**四周偏移**来剔除放不下的 label
* @param labels 参与布局调整的 label 数组集合
*/
export function overlap(items: LabelItem[], labels: IGroup[], shapes: IShape[] | IGroup[], region: BBox) {
const greedy = new Greedy();
each(labels, (label: IGroup) => {
const labelShape = label.find((shape) => shape.get('type') === 'text') as IShape;
const { x, y } = labelShape.attr();
let canFill = false;
for (let i = 0; i <= 8; i++) {
const bbox = adjustLabelPosition(labelShape, x, y, i);
if (greedy.hasGap(bbox)) {
greedy.fillGap(bbox);
canFill = true;
break;
}
}
if (!canFill) {
label.remove(true);
}
});
greedy.destroy();
}