@antv/g6
Version:
A Graph Visualization Framework in JavaScript
293 lines • 12.4 kB
JavaScript
import { pick } from '@antv/util';
import { CommonEvent } from '../../constants';
import { Circle } from '../../elements';
import { arrayDiff } from '../../utils/diff';
import { idOf } from '../../utils/id';
import { parsePoint, toPointObject } from '../../utils/point';
import { positionOf } from '../../utils/position';
import { distance } from '../../utils/vector';
import { BasePlugin } from '../base-plugin';
const defaultLensStyle = {
fill: '#ccc',
fillOpacity: 0.1,
lineWidth: 2,
stroke: '#000',
strokeOpacity: 0.8,
labelFontSize: 12,
};
const R_DELTA = 0.05;
const D_DELTA = 0.1;
/**
* <zh/> 鱼眼放大镜
*
* <en/> Fisheye Distortion
* @remarks
* <zh/> Fisheye 鱼眼放大镜是为 focus+context 的探索场景设计的,它能够保证在放大关注区域的同时,保证上下文以及上下文与关注中心的关系不丢失。
*
* <en/> Fisheye is designed for focus+context exploration, it keeps the context and the relationships between context and the focus while magnifying the focus area.
*/
export class Fisheye extends BasePlugin {
constructor(context, options) {
super(context, Object.assign({}, Fisheye.defaultOptions, options));
this.r = this.options.r;
this.d = this.options.d;
this.onCreateFisheye = (event) => {
if (this.options.trigger === 'drag' && this.isLensOn)
return;
const origin = parsePoint(event.canvas);
this.onMagnify(origin);
};
this.onMagnify = (origin) => {
if (origin.some(isNaN))
return;
this.renderLens(origin);
this.renderFocusElements();
};
this.renderLens = (origin) => {
const style = Object.assign({}, defaultLensStyle, this.options.style);
if (!this.isLensOn) {
this.lens = new Circle({ style });
this.canvas.appendChild(this.lens);
}
Object.assign(style, toPointObject(origin), {
size: this.r * 2,
label: this.options.showDPercent,
labelText: this.getDPercent(),
});
this.lens.update(style);
};
this.getDPercent = () => {
const { minD, maxD } = this.options;
const percent = Math.round(((this.d - minD) / (maxD - minD)) * 100);
return `${percent}%`;
};
this.prevMagnifiedStyleMap = new Map();
this.prevOriginStyleMap = new Map();
this.renderFocusElements = () => {
if (!this.isLensOn)
return;
const { graph } = this.context;
const origin = this.lens.getCenter();
const molecularParam = (this.d + 1) * this.r;
const magnifiedStyleMap = new Map();
const originStyleMap = new Map();
const nodeData = graph.getNodeData();
nodeData.forEach((datum) => {
const position = positionOf(datum);
const distanceToOrigin = distance(position, origin);
if (distanceToOrigin > this.r)
return;
const magnifiedDistance = (molecularParam * distanceToOrigin) / (this.d * distanceToOrigin + this.r);
const [nodeX, nodeY] = position;
const [originX, originY] = origin;
const cos = (nodeX - originX) / distanceToOrigin;
const sin = (nodeY - originY) / distanceToOrigin;
const newPoint = [originX + magnifiedDistance * cos, originY + magnifiedDistance * sin];
const nodeId = idOf(datum);
const style = this.getNodeStyle(datum);
const originStyle = pick(graph.getElementRenderStyle(nodeId), Object.keys(style));
magnifiedStyleMap.set(nodeId, Object.assign(Object.assign({}, toPointObject(newPoint)), style));
originStyleMap.set(nodeId, Object.assign(Object.assign({}, toPointObject(position)), originStyle));
});
this.updateStyle(magnifiedStyleMap, originStyleMap);
};
this.getNodeStyle = (datum) => {
const { nodeStyle } = this.options;
return typeof nodeStyle === 'function' ? nodeStyle(datum) : nodeStyle;
};
this.updateStyle = (magnifiedStyleMap, originStyleMap) => {
const { graph, element } = this.context;
const { enter, exit, keep } = arrayDiff(Array.from(this.prevMagnifiedStyleMap.keys()), Array.from(magnifiedStyleMap.keys()), (d) => d);
const relatedEdges = new Set();
const update = (nodeId, style) => {
const node = element.getElement(nodeId);
node === null || node === void 0 ? void 0 : node.update(style);
graph.getRelatedEdgesData(nodeId).forEach((datum) => {
relatedEdges.add(idOf(datum));
});
};
[...enter, ...keep].forEach((nodeId) => {
update(nodeId, magnifiedStyleMap.get(nodeId));
});
exit.forEach((nodeId) => {
update(nodeId, this.prevOriginStyleMap.get(nodeId));
this.prevOriginStyleMap.delete(nodeId);
});
relatedEdges.forEach((edgeId) => {
const edge = element.getElement(edgeId);
edge === null || edge === void 0 ? void 0 : edge.update({});
});
this.prevMagnifiedStyleMap = magnifiedStyleMap;
originStyleMap.forEach((style, nodeId) => {
if (!this.prevOriginStyleMap.has(nodeId)) {
this.prevOriginStyleMap.set(nodeId, style);
}
});
};
this.isWheelValid = (event) => {
if (this.options.preventDefault)
event.preventDefault();
if (!this.isLensOn)
return false;
const { clientX, clientY } = event;
const scaleOrigin = this.context.graph.getCanvasByClient([clientX, clientY]);
const origin = this.lens.getCenter();
if (distance(scaleOrigin, origin) > this.r)
return false;
return true;
};
this.scaleR = (positive) => {
const { maxR, minR } = this.options;
const ratio = positive ? 1 / (1 - R_DELTA) : 1 - R_DELTA;
const canvasR = Math.min(...this.context.canvas.getSize()) / 2;
this.r = Math.max(minR || 0, Math.min(maxR || canvasR, this.r * ratio));
};
this.scaleD = (positive) => {
const { maxD, minD } = this.options;
const newD = positive ? this.d + D_DELTA : this.d - D_DELTA;
this.d = Math.max(minD, Math.min(maxD, newD));
};
this.scaleRByWheel = (event) => {
if (!this.isWheelValid(event))
return;
const { deltaX, deltaY } = event;
this.scaleR(deltaX + deltaY > 0);
const origin = this.lens.getCenter();
this.onMagnify(origin);
};
this.scaleDByWheel = (event) => {
if (!this.isWheelValid(event))
return;
const { deltaX, deltaY } = event;
this.scaleD(deltaX + deltaY > 0);
const origin = this.lens.getCenter();
this.onMagnify(origin);
};
this.isDragValid = (event) => {
if (this.options.preventDefault)
event.preventDefault();
if (!this.isLensOn)
return false;
const dragOrigin = parsePoint(event.canvas);
const origin = this.lens.getCenter();
if (distance(dragOrigin, origin) > this.r)
return false;
return true;
};
this.isLensDragging = false;
this.onDragStart = (event) => {
if (!this.isDragValid(event))
return;
this.isLensDragging = true;
};
this.onDrag = (event) => {
if (!this.isLensDragging)
return;
const dragOrigin = parsePoint(event.canvas);
this.onMagnify(dragOrigin);
};
this.onDragEnd = () => {
this.isLensDragging = false;
};
this.scaleRByDrag = (event) => {
if (!this.isLensDragging)
return;
const { dx, dy } = event;
this.scaleR(dx - dy > 0);
const origin = this.lens.getCenter();
this.onMagnify(origin);
};
this.scaleDByDrag = (event) => {
if (!this.isLensDragging)
return;
const { dx, dy } = event;
this.scaleD(dx - dy > 0);
const origin = this.lens.getCenter();
this.onMagnify(origin);
};
this.bindEvents();
}
get canvas() {
return this.context.canvas.getLayer('transient');
}
get isLensOn() {
return this.lens && !this.lens.destroyed;
}
get graphDom() {
return this.context.graph.getCanvas().getContextService().getDomElement();
}
bindEvents() {
var _a;
const { graph } = this.context;
const { trigger, scaleRBy, scaleDBy } = this.options;
const canvas = graph.getCanvas().getLayer();
if (['click', 'drag'].includes(trigger)) {
canvas.addEventListener(CommonEvent.CLICK, this.onCreateFisheye);
}
if (trigger === 'pointermove') {
canvas.addEventListener(CommonEvent.POINTER_MOVE, this.onCreateFisheye);
}
if (trigger === 'drag' || scaleRBy === 'drag' || scaleDBy === 'drag') {
canvas.addEventListener(CommonEvent.DRAG_START, this.onDragStart);
canvas.addEventListener(CommonEvent.DRAG_END, this.onDragEnd);
const dragFunc = trigger === 'drag' ? this.onDrag : scaleRBy === 'drag' ? this.scaleRByDrag : this.scaleDByDrag;
canvas.addEventListener(CommonEvent.DRAG, dragFunc);
}
if (scaleRBy === 'wheel' || scaleDBy === 'wheel') {
const wheelFunc = scaleRBy === 'wheel' ? this.scaleRByWheel : this.scaleDByWheel;
(_a = this.graphDom) === null || _a === void 0 ? void 0 : _a.addEventListener(CommonEvent.WHEEL, wheelFunc, { passive: false });
}
}
unbindEvents() {
var _a;
const { graph } = this.context;
const { trigger, scaleRBy, scaleDBy } = this.options;
const canvas = graph.getCanvas().getLayer();
if (['click', 'drag'].includes(trigger)) {
canvas.removeEventListener(CommonEvent.CLICK, this.onCreateFisheye);
}
if (trigger === 'pointermove') {
canvas.removeEventListener(CommonEvent.POINTER_MOVE, this.onCreateFisheye);
}
if (trigger === 'drag' || scaleRBy === 'drag' || scaleDBy === 'drag') {
canvas.removeEventListener(CommonEvent.DRAG_START, this.onDragStart);
canvas.removeEventListener(CommonEvent.DRAG_END, this.onDragEnd);
const dragFunc = trigger === 'drag' ? this.onDrag : scaleRBy === 'drag' ? this.scaleRByDrag : this.scaleDByDrag;
canvas.removeEventListener(CommonEvent.DRAG, dragFunc);
}
if (scaleRBy === 'wheel' || scaleDBy === 'wheel') {
const wheelFunc = scaleRBy === 'wheel' ? this.scaleRByWheel : this.scaleDByWheel;
(_a = this.graphDom) === null || _a === void 0 ? void 0 : _a.removeEventListener(CommonEvent.WHEEL, wheelFunc);
}
}
update(options) {
var _a, _b;
this.unbindEvents();
super.update(options);
this.r = (_a = options.r) !== null && _a !== void 0 ? _a : this.r;
this.d = (_b = options.d) !== null && _b !== void 0 ? _b : this.d;
this.bindEvents();
}
destroy() {
var _a;
this.unbindEvents();
if (this.isLensOn) {
(_a = this.lens) === null || _a === void 0 ? void 0 : _a.destroy();
}
this.prevMagnifiedStyleMap.clear();
this.prevOriginStyleMap.clear();
super.destroy();
}
}
Fisheye.defaultOptions = {
trigger: 'pointermove',
r: 120,
d: 1.5,
maxD: 5,
minD: 0,
showDPercent: true,
style: {},
nodeStyle: { label: true },
preventDefault: true,
};
//# sourceMappingURL=index.js.map