@antv/g2
Version:
the Grammar of Graphics in Javascript
586 lines • 24.2 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { Rect, Path } from '@antv/g';
import { subObject, omitPrefixObject } from '../utils/helper';
import { selectionOf, pixelsOf } from '../utils/scale';
import { createElement } from '../utils/createElement';
import { select } from '../utils/selection';
import { selectG2Elements, selectPlotArea, createDatumof, useState, createValueof, setCursor, brushMousePosition, selectFacetG2Elements, mergeState, selectFacetViews, } from './utils';
function intersect(bbox1, bbox2) {
const [minX1, minY1, maxX1, maxY1] = bbox1;
const [minX2, minY2, maxX2, maxY2] = bbox2;
return !(minX2 > maxX1 || maxX2 < minX1 || minY2 > maxY1 || maxY2 < minY1);
}
function normalizeBounds(x, y, x1, y1, extent) {
const [minX, minY, maxX, maxY] = extent;
return [
Math.max(minX, Math.min(x, x1)),
Math.max(minY, Math.min(y, y1)),
Math.min(maxX, Math.max(x, x1)),
Math.min(maxY, Math.max(y, y1)),
];
}
function bboxOf(root) {
const { width, height } = root.getBBox();
return [0, 0, width, height];
}
function applyStyle(selection, style) {
for (const [key, value] of Object.entries(style)) {
selection.style(key, value);
}
}
const ResizableMask = createElement((g) => {
const _a = g.attributes, { x, y, width, height, class: className, renders = {}, handleSize: size = 10, document } = _a, style = __rest(_a, ["x", "y", "width", "height", "class", "renders", "handleSize", "document"]);
if (!document ||
width === undefined ||
height === undefined ||
x === undefined ||
y === undefined)
return;
const half = size / 2;
const renderRect = (g, options, document) => {
if (!g.handle) {
g.handle = document.createElement('rect');
g.append(g.handle);
}
const { handle } = g;
handle.attr(options);
return handle;
};
const _b = subObject(omitPrefixObject(style, 'handleNW', 'handleNE'), 'handleN'), { render: handleNRender = renderRect } = _b, handleNStyle = __rest(_b, ["render"]);
const _c = subObject(style, 'handleE'), { render: handleERender = renderRect } = _c, handleEStyle = __rest(_c, ["render"]);
const _d = subObject(omitPrefixObject(style, 'handleSE', 'handleSW'), 'handleS'), { render: handleSRender = renderRect } = _d, handleSStyle = __rest(_d, ["render"]);
const _e = subObject(style, 'handleW'), { render: handleWRender = renderRect } = _e, handleWStyle = __rest(_e, ["render"]);
const _f = subObject(style, 'handleNW'), { render: handleNWRender = renderRect } = _f, handleNWStyle = __rest(_f, ["render"]);
const _g = subObject(style, 'handleNE'), { render: handleNERender = renderRect } = _g, handleNEStyle = __rest(_g, ["render"]);
const _h = subObject(style, 'handleSE'), { render: handleSERender = renderRect } = _h, handleSEStyle = __rest(_h, ["render"]);
const _j = subObject(style, 'handleSW'), { render: handleSWRender = renderRect } = _j, handleSWStyle = __rest(_j, ["render"]);
const renderHandle = (g, renderNode) => {
const { id } = g;
const handle = renderNode(g, g.attributes, document);
handle.id = id;
handle.style.draggable = true;
};
const appendHandle = (handleRender) => {
return () => {
const Node = createElement((g) => renderHandle(g, handleRender));
return new Node({});
};
};
const container = select(g)
.attr('className', className)
.style('transform', `translate(${x}, ${y})`)
.style('draggable', true);
container
.maybeAppend('selection', 'rect')
.style('draggable', true)
.style('fill', 'transparent')
.call(applyStyle, Object.assign(Object.assign({ width,
height }, omitPrefixObject(style, 'handle')), { transform: undefined }));
container
.maybeAppend('handle-n', appendHandle(handleNRender))
.style('x', half)
.style('y', -half)
.style('width', width - size)
.style('height', size)
.style('fill', 'transparent')
.call(applyStyle, handleNStyle);
container
.maybeAppend('handle-e', appendHandle(handleERender))
.style('x', width - half)
.style('y', half)
.style('width', size)
.style('height', height - size)
.style('fill', 'transparent')
.call(applyStyle, handleEStyle);
container
.maybeAppend('handle-s', appendHandle(handleSRender))
.style('x', half)
.style('y', height - half)
.style('width', width - size)
.style('height', size)
.style('fill', 'transparent')
.call(applyStyle, handleSStyle);
container
.maybeAppend('handle-w', appendHandle(handleWRender))
.style('x', -half)
.style('y', half)
.style('width', size)
.style('height', height - size)
.style('fill', 'transparent')
.call(applyStyle, handleWStyle);
container
.maybeAppend('handle-nw', appendHandle(handleNWRender))
.style('x', -half)
.style('y', -half)
.style('width', size)
.style('height', size)
.style('fill', 'transparent')
.call(applyStyle, handleNWStyle);
container
.maybeAppend('handle-ne', appendHandle(handleNERender))
.style('x', width - half)
.style('y', -half)
.style('width', size)
.style('height', size)
.style('fill', 'transparent')
.call(applyStyle, handleNEStyle);
container
.maybeAppend('handle-se', appendHandle(handleSERender))
.style('x', width - half)
.style('y', height - half)
.style('width', size)
.style('height', size)
.style('fill', 'transparent')
.call(applyStyle, handleSEStyle);
container
.maybeAppend('handle-sw', appendHandle(handleSWRender))
.style('x', -half)
.style('y', height - half)
.style('width', size)
.style('height', size)
.style('fill', 'transparent')
.call(applyStyle, handleSWStyle);
});
export function brush(root, _a) {
var { brushed = () => { }, brushended = () => { }, brushcreated = () => { }, brushstarted = () => { }, brushupdated = () => { }, extent = bboxOf(root), brushRegion = (x, y, x1, y1, extent) => [x, y, x1, y1], reverse = false, fill = '#777', fillOpacity = '0.3', stroke = '#fff', selectedHandles = [
'handle-n',
'handle-e',
'handle-s',
'handle-w',
'handle-nw',
'handle-ne',
'handle-se',
'handle-sw',
] } = _a, style = __rest(_a, ["brushed", "brushended", "brushcreated", "brushstarted", "brushupdated", "extent", "brushRegion", "reverse", "fill", "fillOpacity", "stroke", "selectedHandles"]);
let start = null; // Start point of mask.
let end = null; // End point of mask.
let moveStart = null; // Start point of moving mask.
let mask = null; // Mask instance.
let background = null;
let creating = false;
const [originX, originY, width, height] = extent;
setCursor(root, 'crosshair');
root.style.draggable = true; // Make it response to drag event.
// Remove old mask and init new mask.
const initMask = (x, y, event) => {
brushstarted(event);
if (mask)
mask.remove();
if (background)
background.remove();
start = [x, y];
if (reverse)
return initReverseMask();
initNormalMask();
};
const initReverseMask = () => {
background = new Path({
style: Object.assign(Object.assign({}, style), { fill,
fillOpacity,
stroke, pointerEvents: 'none' }),
});
mask = new ResizableMask({
// @ts-ignore
style: {
x: 0,
y: 0,
width: 0,
height: 0,
draggable: true,
document: root.ownerDocument,
},
className: 'mask',
});
root.appendChild(background);
root.appendChild(mask);
};
const initNormalMask = () => {
mask = new ResizableMask({
// @ts-ignore
style: Object.assign(Object.assign({ document: root.ownerDocument, x: 0, y: 0 }, style), { fill,
fillOpacity,
stroke, draggable: true }),
className: 'mask',
});
root.appendChild(mask);
};
// Remove mask and reset states.
const removeMask = (emit = true) => {
if (mask)
mask.remove();
if (background)
background.remove();
start = null;
end = null;
moveStart = null;
creating = false;
mask = null;
background = null;
brushended(emit);
};
// Update mask and invoke brushended callback.
const updateMask = (start, end, emit = true) => {
const [x, y, x1, y1] = normalizeBounds(start[0], start[1], end[0], end[1], extent);
const [fx, fy, fx1, fy1] = brushRegion(x, y, x1, y1, extent);
if (reverse)
updateReverseMask(fx, fy, fx1, fy1);
else
updateNormalMask(fx, fy, fx1, fy1);
brushed(fx, fy, fx1, fy1, emit);
return [fx, fy, fx1, fy1];
};
const updateNormalMask = (x, y, x1, y1) => {
mask.style.x = x;
mask.style.y = y;
mask.style.width = x1 - x;
mask.style.height = y1 - y;
};
const updateReverseMask = (x, y, x1, y1) => {
background.style.d = `
M${originX},${originY}L${width},${originY}L${width},${height}L${originX},${height}Z
M${x},${y}L${x},${y1}L${x1},${y1}L${x1},${y}Z
`;
mask.style.x = x;
mask.style.y = y;
mask.style.width = x1 - x;
mask.style.height = y1 - y;
};
// Move and update mask.
const moveMask = (current) => {
const clip = (dt, start, end, min, max) => {
if (dt + start < min)
return min - start;
if (dt + end > max)
return max - end;
return dt;
};
const dx = current[0] - moveStart[0];
const dy = current[1] - moveStart[1];
const dx1 = clip(dx, start[0], end[0], originX, width);
const dy1 = clip(dy, start[1], end[1], originY, height);
const currentStart = [start[0] + dx1, start[1] + dy1];
const currentEnd = [end[0] + dx1, end[1] + dy1];
updateMask(currentStart, currentEnd);
};
const handles = {
'handle-n': { vector: [0, 1, 0, 0], cursor: 'ns-resize' },
'handle-e': { vector: [0, 0, 1, 0], cursor: 'ew-resize' },
'handle-s': { vector: [0, 0, 0, 1], cursor: 'ns-resize' },
'handle-w': { vector: [1, 0, 0, 0], cursor: 'ew-resize' },
'handle-nw': { vector: [1, 1, 0, 0], cursor: 'nwse-resize' },
'handle-ne': { vector: [0, 1, 1, 0], cursor: 'nesw-resize' },
'handle-se': { vector: [0, 0, 1, 1], cursor: 'nwse-resize' },
'handle-sw': { vector: [1, 0, 0, 1], cursor: 'nesw-resize' },
};
const isMask = (target) => {
return isSelection(target) || isHandle(target);
};
const isHandle = (target) => {
const { id } = target;
if (selectedHandles.indexOf(id) === -1)
return false;
return new Set(Object.keys(handles)).has(id);
};
const isSelection = (target) => {
return target === mask.getElementById('selection');
};
// If target is plot area, create mask.
// If target is mask, about to update position.
const dragstart = (event) => {
const { target } = event;
const [offsetX, offsetY] = brushMousePosition(root, event);
if (!mask || !isMask(target)) {
initMask(offsetX, offsetY, event);
creating = true;
return;
}
if (isMask(target)) {
moveStart = [offsetX, offsetY];
}
};
const drag = (event) => {
const { target } = event;
const mouse = brushMousePosition(root, event);
if (!start)
return;
// If target is plot area, resize mask.
if (!moveStart)
return updateMask(start, mouse);
// If target is selection area, move mask.
if (isSelection(target))
return moveMask(mouse);
// If target is handle area, resize mask.
const [dx, dy] = [mouse[0] - moveStart[0], mouse[1] - moveStart[1]];
const { id } = target;
if (handles[id]) {
const [sx, sy, ex, ey] = handles[id].vector;
return updateMask([start[0] + dx * sx, start[1] + dy * sy], [end[0] + dx * ex, end[1] + dy * ey]);
}
};
// If target is plot area, finish creating.
// If target is mask, finish moving mask.
const dragend = (event) => {
if (moveStart) {
moveStart = null;
// Update start and end;
const { x, y, width, height } = mask.style;
start = [x, y];
end = [x + width, y + height];
brushupdated(x, y, x + width, y + height, event);
return;
}
end = brushMousePosition(root, event);
const [fx, fy, fx1, fy1] = updateMask(start, end);
creating = false;
brushcreated(fx, fy, fx1, fy1, event);
};
// Hide mask.
const click = (event) => {
const { target } = event;
if (mask && !isMask(target))
removeMask();
};
// Update cursor depends on hovered element.
const pointermove = (event) => {
const { target } = event;
if (!mask || !isMask(target) || creating)
setCursor(root, 'crosshair');
else if (isSelection(target))
setCursor(root, 'move');
else if (isHandle(target))
setCursor(root, handles[target.id].cursor);
};
const pointerleave = () => {
setCursor(root, 'default');
};
root.addEventListener('dragstart', dragstart);
root.addEventListener('drag', drag);
root.addEventListener('dragend', dragend);
root.addEventListener('click', click);
root.addEventListener('pointermove', pointermove);
root.addEventListener('pointerleave', pointerleave);
return {
mask,
move(x, y, x1, y1, emit = true) {
if (!mask)
initMask(x, y, {});
start = [x, y];
end = [x1, y1];
updateMask([x, y], [x1, y1], emit);
},
remove(emit = true) {
if (mask)
removeMask(emit);
},
destroy() {
// Do not emit brush:end event.
if (mask)
removeMask(false);
setCursor(root, 'default');
root.removeEventListener('dragstart', dragstart);
root.removeEventListener('drag', drag);
root.removeEventListener('dragend', dragend);
root.removeEventListener('click', click);
root.removeEventListener('pointermove', pointermove);
root.removeEventListener('pointerleave', pointerleave);
},
};
}
function selectSiblingViews(target, viewInstances, brushKey) {
return viewInstances.filter((d) => {
if (d === target)
return false;
const { interaction = {} } = d.options;
return Object.values(interaction).find((d) => d.brushKey === brushKey);
});
}
function selectSiblingContainers(target, viewInstances, brushKey) {
return selectSiblingViews(target, viewInstances, brushKey).map((d) => selectPlotArea(d.container));
}
function selectSiblingOptions(target, viewInstances, brushKey) {
return selectSiblingViews(target, viewInstances, brushKey).map((d) => d.options);
}
/**
* @todo Brush over view for series view.
* @todo Test perf.
*/
export function brushHighlight(root, _a) {
var { elements: elementof, selectedHandles, siblings: siblingsof = (root) => [], datum, brushRegion, extent: optionalExtent, reverse, scale, coordinate, series = false, key = (d) => d, bboxOf = (root) => {
const { x, y, width, height } = root.style;
return { x, y, width, height };
}, state = {}, emitter } = _a, rest = __rest(_a, ["elements", "selectedHandles", "siblings", "datum", "brushRegion", "extent", "reverse", "scale", "coordinate", "series", "key", "bboxOf", "state", "emitter"]);
const elements = elementof(root);
const siblings = siblingsof(root);
const siblingElements = siblings.flatMap(elementof);
const valueof = createValueof(elements, datum);
const brushStyle = subObject(rest, 'mask');
const { setState, removeState } = useState(state, valueof);
const clonedElement = new Map();
const { width: rootWidth, height: rootHeight, x: ordinalX = 0, y: ordinalY = 0, } = bboxOf(root);
const extent = optionalExtent
? optionalExtent
: [0, 0, rootWidth, rootHeight];
const brushended = () => {
for (const element of [...elements, ...siblingElements]) {
removeState(element, 'active', 'inactive');
}
};
const brushed = (x, y, x1, y1) => {
var _a;
// Hide brush for the sibling view.
for (const sibling of siblings)
(_a = sibling.brush) === null || _a === void 0 ? void 0 : _a.remove();
// Store the key of the active element.
const keys = new Set();
// Highlight and store selected elements.
for (const element of elements) {
const { min, max } = element.getLocalBounds();
const [ex, ey] = min;
const [ex1, ey1] = max;
if (!intersect([ex, ey, ex1, ey1], [x, y, x1, y1])) {
setState(element, 'inactive');
}
else {
setState(element, 'active');
keys.add(key(element));
}
}
// Highlight elements with same key in sibling view.
for (const element of siblingElements) {
if (keys.has(key(element)))
setState(element, 'active');
else
setState(element, 'inactive');
}
};
const seriesBrushend = () => {
for (const element of elements)
removeState(element, 'inactive');
for (const cloned of clonedElement.values())
cloned.remove();
clonedElement.clear();
};
const seriesBrushed = (x, y, x1, y1) => {
const clone = (element) => {
const cloned = element.cloneNode();
cloned.__data__ = element.__data__;
element.parentNode.appendChild(cloned);
clonedElement.set(element, cloned);
return cloned;
};
// Create a clipPath shared between all children.
const clipPath = new Rect({
style: {
x: x + ordinalX,
y: y + ordinalY,
width: x1 - x,
height: y1 - y,
},
});
root.appendChild(clipPath);
for (const element of elements) {
const cloned = clonedElement.get(element) || clone(element);
cloned.style.clipPath = clipPath;
setState(element, 'inactive');
setState(cloned, 'active');
}
};
const brushHandler = brush(root, Object.assign(Object.assign({}, brushStyle), { extent,
brushRegion,
reverse,
selectedHandles, brushended: (emit) => {
const handler = series ? seriesBrushend : brushended;
if (emit) {
emitter.emit('brush:remove', { nativeEvent: true });
}
handler();
}, brushed: (x, y, x1, y1, emit) => {
const selection = selectionOf(x, y, x1, y1, scale, coordinate);
if (emit) {
emitter.emit('brush:highlight', {
nativeEvent: true,
data: { selection },
});
}
const handler = series ? seriesBrushed : brushed;
handler(x, y, x1, y1);
}, brushcreated: (x, y, x1, y1, event) => {
const selection = selectionOf(x, y, x1, y1, scale, coordinate);
emitter.emit('brush:end', Object.assign(Object.assign({}, event), { nativeEvent: true, data: { selection } }));
}, brushupdated: (x, y, x1, y1, event) => {
const selection = selectionOf(x, y, x1, y1, scale, coordinate);
emitter.emit('brush:end', Object.assign(Object.assign({}, event), { nativeEvent: true, data: { selection } }));
}, brushstarted: (e) => {
emitter.emit('brush:start', e);
} }));
// Move brush and highlight data.
const onHighlight = ({ nativeEvent, data }) => {
if (nativeEvent)
return;
const { selection } = data;
const [x, y, x1, y1] = pixelsOf(selection, scale, coordinate);
brushHandler.move(x, y, x1, y1, false);
};
emitter.on('brush:highlight', onHighlight);
// Remove brush and reset data.
const onRemove = ({ nativeEvent } = {}) => {
if (nativeEvent)
return;
brushHandler.remove(false);
};
emitter.on('brush:remove', onRemove);
// Remove event handlers.
const preBrushDestroy = brushHandler.destroy.bind(brushHandler);
brushHandler.destroy = () => {
emitter.off('brush:highlight', onHighlight);
emitter.off('brush:remove', onRemove);
preBrushDestroy();
};
return brushHandler;
}
export function BrushHighlight(_a) {
var { facet, brushKey } = _a, rest = __rest(_a, ["facet", "brushKey"]);
return (target, viewInstances, emitter) => {
const { container, view, options } = target;
const plotArea = selectPlotArea(container);
const defaultOptions = {
maskFill: '#777',
maskFillOpacity: '0.3',
maskStroke: '#fff',
reverse: false,
};
const defaultStates = ['active', ['inactive', { opacity: 0.5 }]];
const { scale, coordinate } = view;
if (facet) {
const bbox = plotArea.getBounds();
const x = bbox.min[0];
const y = bbox.min[1];
const x1 = bbox.max[0];
const y1 = bbox.max[1];
return brushHighlight(plotArea.parentNode.parentNode, Object.assign(Object.assign({ elements: () => selectFacetG2Elements(target, viewInstances), datum: createDatumof(selectFacetViews(target, viewInstances).map((d) => d.view)), brushRegion: (x, y, x1, y1) => [x, y, x1, y1], extent: [x, y, x1, y1], state: mergeState(selectFacetViews(target, viewInstances).map((d) => d.options), defaultStates), emitter,
scale,
coordinate, selectedHandles: undefined }, defaultOptions), rest));
}
const brush = brushHighlight(plotArea, Object.assign(Object.assign({ elements: selectG2Elements, key: (element) => element.__data__.key, siblings: () => selectSiblingContainers(target, viewInstances, brushKey), datum: createDatumof([
view,
...selectSiblingViews(target, viewInstances, brushKey).map((d) => d.view),
]), brushRegion: (x, y, x1, y1) => [x, y, x1, y1], extent: undefined, state: mergeState([options, ...selectSiblingOptions(target, viewInstances, brushKey)], defaultStates), emitter,
scale,
coordinate, selectedHandles: undefined }, defaultOptions), rest));
// Bind brush to the view it belongs to.
//@ts-ignore
plotArea.brush = brush;
return () => brush.destroy();
};
}
//# sourceMappingURL=brushHighlight.js.map