@antv/g6
Version:
A Graph Visualization Framework in JavaScript
355 lines • 13.9 kB
JavaScript
import { debounce, throttle } from '@antv/util';
import { GraphEvent } from '../../constants';
import { idOf } from '../../utils/id';
import { parsePadding } from '../../utils/padding';
import { toPointObject } from '../../utils/point';
import { BasePlugin } from '../base-plugin';
import { createPluginCanvas } from '../utils/canvas';
/**
* <zh/> 缩略图插件
*
* <en/> Minimap plugin
*/
export class Minimap extends BasePlugin {
constructor(context, options) {
super(context, Object.assign({}, Minimap.defaultOptions, options));
this.onDraw = (event) => {
var _a;
if ((_a = event === null || event === void 0 ? void 0 : event.data) === null || _a === void 0 ? void 0 : _a.render)
return;
this.onRender();
};
this.shapes = new Map();
this.landmarkMap = new Map();
this.mask = null;
this.isMaskDragging = false;
this.onMaskDragStart = (event) => {
if (!this.mask)
return;
this.isMaskDragging = true;
this.mask.setPointerCapture(event.pointerId);
this.mask.addEventListener('pointermove', this.onMaskDrag);
this.mask.addEventListener('pointerup', this.onMaskDragEnd);
this.mask.addEventListener('pointercancel', this.onMaskDragEnd);
};
this.onMaskDrag = (event) => {
if (!this.mask || !this.isMaskDragging)
return;
const { size: [minimapWidth, minimapHeight], } = this.options;
const { movementX, movementY } = event;
const { left, top, width: w, height: h } = this.mask.style;
const [, , fullWidth, fullHeight] = this.maskBBox;
let x = parseInt(left) + movementX;
let y = parseInt(top) + movementY;
let width = parseInt(w);
let height = parseInt(h);
// 确保 mask 在 minimap 内部
// Ensure that the mask is inside the minimap
if (x < 0)
x = 0;
if (y < 0)
y = 0;
if (x + width > minimapWidth)
x = lower(minimapWidth - width, 0);
if (y + height > minimapHeight)
y = lower(minimapHeight - height, 0);
// 当拖拽画布导致 mask 缩小时,拖拽 mask 时,能够恢复到实际大小
// When dragging the canvas causes the mask to shrink, dragging the mask will restore it to its actual size
if (width < fullWidth) {
if (movementX > 0)
(x = lower(x - movementX, 0)), (width = upper(width + movementX, minimapWidth));
else if (movementX < 0)
width = upper(width - movementX, minimapWidth);
}
if (height < fullHeight) {
if (movementY > 0)
(y = lower(y - movementY, 0)), (height = upper(height + movementY, minimapHeight));
else if (movementY < 0)
height = upper(height - movementY, minimapHeight);
}
Object.assign(this.mask.style, {
left: x + 'px',
top: y + 'px',
width: width + 'px',
height: height + 'px',
});
// 基于 movement 进行相对移动
// Move relative to movement
const deltaX = parseInt(left) - x;
const deltaY = parseInt(top) - y;
if (deltaX === 0 && deltaY === 0)
return;
const zoom1 = this.context.canvas.getCamera().getZoom();
const zoom2 = this.canvas.getCamera().getZoom();
const ratio = zoom1 / zoom2;
this.context.graph.translateBy([deltaX * ratio, deltaY * ratio], false);
};
this.onMaskDragEnd = (event) => {
if (!this.mask)
return;
this.isMaskDragging = false;
this.mask.releasePointerCapture(event.pointerId);
this.mask.removeEventListener('pointermove', this.onMaskDrag);
this.mask.removeEventListener('pointerup', this.onMaskDragEnd);
this.mask.removeEventListener('pointercancel', this.onMaskDragEnd);
};
this.onTransform = throttle(() => {
if (this.isMaskDragging)
return;
this.updateMask();
this.setCamera();
}, 32, { leading: true });
this.setOnRender();
this.bindEvents();
}
update(options) {
this.unbindEvents();
super.update(options);
if ('delay' in options)
this.setOnRender();
this.bindEvents();
}
setOnRender() {
this.onRender = debounce(() => {
this.renderMinimap();
this.renderMask();
}, this.options.delay);
}
bindEvents() {
const { graph } = this.context;
graph.on(GraphEvent.AFTER_DRAW, this.onDraw);
graph.on(GraphEvent.AFTER_RENDER, this.onRender);
graph.on(GraphEvent.AFTER_TRANSFORM, this.onTransform);
}
unbindEvents() {
const { graph } = this.context;
graph.off(GraphEvent.AFTER_DRAW, this.onDraw);
graph.off(GraphEvent.AFTER_RENDER, this.onRender);
graph.off(GraphEvent.AFTER_TRANSFORM, this.onTransform);
}
/**
* <zh/> 创建或更新缩略图
*
* <en/> Create or update the minimap
*/
renderMinimap() {
const data = this.getElements();
const canvas = this.initCanvas();
this.setShapes(canvas, data);
}
getElements() {
const { filter } = this.options;
const { model } = this.context;
const data = model.getData();
if (!filter)
return data;
const { nodes, edges, combos } = data;
return {
nodes: nodes.filter((node) => filter(idOf(node), 'node')),
edges: edges.filter((edge) => filter(idOf(edge), 'edge')),
combos: combos.filter((combo) => filter(idOf(combo), 'combo')),
};
}
setShapes(canvas, data) {
const { nodes, edges, combos } = data;
const { shape } = this.options;
const { element } = this.context;
if (shape === 'key') {
const ids = new Set();
const iterate = (datum) => {
const id = idOf(datum);
ids.add(id);
const target = element.getElement(id);
if (!target)
return;
const shape = target.getShape('key');
const cloneShape = this.shapes.get(id) || shape.cloneNode();
cloneShape.setPosition(shape.getPosition());
// keep zIndex / id
if (target.style.zIndex)
cloneShape.style.zIndex = target.style.zIndex;
cloneShape.id = target.id;
if (!this.shapes.has(id)) {
canvas.appendChild(cloneShape);
this.shapes.set(id, cloneShape);
}
else {
Object.entries(shape.attributes).forEach(([key, value]) => {
if (cloneShape.style[key] !== value)
cloneShape.style[key] = value;
});
}
};
// 注意执行顺序 / Note the execution order
edges.forEach(iterate);
combos.forEach(iterate);
nodes.forEach(iterate);
this.shapes.forEach((shape, id) => {
if (!ids.has(id)) {
canvas.removeChild(shape);
this.shapes.delete(id);
}
});
return;
}
const setPosition = (id, shape) => {
const target = element.getElement(id);
const position = target.getPosition();
shape.setPosition(position);
return shape;
};
canvas.removeChildren();
edges.forEach((datum) => canvas.appendChild(shape(idOf(datum), 'edge')));
combos.forEach((datum) => {
canvas.appendChild(setPosition(idOf(datum), shape(idOf(datum), 'combo')));
});
nodes.forEach((datum) => {
canvas.appendChild(setPosition(idOf(datum), shape(idOf(datum), 'node')));
});
}
initCanvas() {
const { renderer, size: [width, height], } = this.options;
if (this.canvas) {
const { width: w, height: h } = this.canvas.getConfig();
if (width !== w || height !== h)
this.canvas.resize(width, height);
if (renderer)
this.canvas.setRenderer(renderer);
}
else {
const { className, position, container, containerStyle } = this.options;
const [$container, canvas] = createPluginCanvas({
renderer,
width,
height,
placement: position,
className: 'minimap',
container,
containerStyle,
graphCanvas: this.context.canvas,
});
if (className)
$container.classList.add(className);
this.container = $container;
this.canvas = canvas;
}
this.setCamera();
return this.canvas;
}
createLandmark(position, focalPoint, zoom) {
const key = `${position.join(',')}-${focalPoint.join(',')}-${zoom}`;
if (this.landmarkMap.has(key))
return this.landmarkMap.get(key);
const camera = this.canvas.getCamera();
const landmark = camera.createLandmark(key, {
position,
focalPoint,
zoom,
});
this.landmarkMap.set(key, landmark);
return landmark;
}
setCamera() {
var _a;
const { canvas } = this.context;
const camera = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getCamera();
if (!camera)
return;
const { size: [minimapWidth, minimapHeight], padding, } = this.options;
const [top, right, bottom, left] = parsePadding(padding);
const { min: boundsMin, max: boundsMax, center } = canvas.getBounds('elements');
const boundsWidth = boundsMax[0] - boundsMin[0];
const boundsHeight = boundsMax[1] - boundsMin[1];
const availableWidth = minimapWidth - left - right;
const availableHeight = minimapHeight - top - bottom;
const scaleX = availableWidth / boundsWidth;
const scaleY = availableHeight / boundsHeight;
const scale = Math.min(scaleX, scaleY);
const landmark = this.createLandmark(center, center, scale);
camera.gotoLandmark(landmark, 0);
}
get maskBBox() {
const { canvas: graphCanvas } = this.context;
const canvasSize = graphCanvas.getSize();
const canvasMin = graphCanvas.getCanvasByViewport([0, 0]);
const canvasMax = graphCanvas.getCanvasByViewport(canvasSize);
const maskMin = this.canvas.canvas2Viewport(toPointObject(canvasMin));
const maskMax = this.canvas.canvas2Viewport(toPointObject(canvasMax));
const width = maskMax.x - maskMin.x;
const height = maskMax.y - maskMin.y;
return [maskMin.x, maskMin.y, width, height];
}
/**
* <zh/> 计算遮罩包围盒
*
* <en/> Calculate the bounding box of the mask
* @returns <zh/> 遮罩包围盒 | <en/> Mask bounding box
*/
calculateMaskBBox() {
const { size: [minimapWidth, minimapHeight], } = this.options;
let [x, y, width, height] = this.maskBBox;
// clamp x, y, width, height
if (x < 0)
(width = upper(width + x, minimapWidth)), (x = 0);
if (y < 0)
(height = upper(height + y, minimapHeight)), (y = 0);
if (x + width > minimapWidth)
width = lower(minimapWidth - x, 0);
if (y + height > minimapHeight)
height = lower(minimapHeight - y, 0);
return [upper(x, minimapWidth), upper(y, minimapHeight), lower(width, 0), lower(height, 0)];
}
/**
* <zh/> 创建或更新遮罩
*
* <en/> Create or update the mask
*/
renderMask() {
const { maskStyle } = this.options;
if (!this.mask) {
this.mask = document.createElement('div');
this.mask.addEventListener('pointerdown', this.onMaskDragStart);
this.mask.draggable = true;
this.mask.addEventListener('dragstart', (event) => event.preventDefault && event.preventDefault());
}
this.container.appendChild(this.mask);
Object.assign(this.mask.style, Object.assign(Object.assign({}, maskStyle), { cursor: 'move', position: 'absolute', pointerEvents: 'auto' }));
this.updateMask();
}
updateMask() {
if (!this.mask)
return;
const [x, y, width, height] = this.calculateMaskBBox();
Object.assign(this.mask.style, {
top: y + 'px',
left: x + 'px',
width: width + 'px',
height: height + 'px',
});
}
destroy() {
var _a;
this.unbindEvents();
this.canvas.destroy();
(_a = this.mask) === null || _a === void 0 ? void 0 : _a.remove();
super.destroy();
}
}
Minimap.defaultOptions = {
size: [240, 160],
shape: 'key',
padding: 10,
position: 'right-bottom',
maskStyle: {
border: '1px solid #ddd',
background: 'rgba(0, 0, 0, 0.1)',
},
containerStyle: {
border: '1px solid #ddd',
background: '#fff',
},
delay: 128,
};
const upper = (value, max) => Math.min(value, max);
const lower = (value, min) => Math.max(value, min);
//# sourceMappingURL=index.js.map