function applyDragInteraction(
chart,
{
minTheta = 0.1,
...style
} = {}
) {
const { canvas } = chart.getContext();
const { document } = canvas;
const [plot] = document.getElementsByClassName(G2.PLOT_CLASS_NAME);
const elements = document.getElementsByClassName(G2.ELEMENT_CLASS_NAME);
const data = elements.map((d) => d.__data__);
const I = data.map((_, i) => i);
const controlPoints = data.map((d) => d.points[3]);
const circles = controlPoints.map((d) =>
document.createElement("circle", {
style: {
r: 10,
fill: "red",
...style,
cx: d[0],
cy: d[1],
draggable: true
}
})
);
circles.forEach((d) => plot.append(d));
plot.addEventListener("dragstart", dragstart);
plot.addEventListener("drag", drag);
// Gets the layout information.
const coordinate = chart.getCoordinate();
const [cx, cy] = coordinate.getCenter();
const { width, height } = coordinate.getOptions();
const radius = Math.min(width, height) / 2;
let index = -1; // the index of dragging handle
let startAngle = -Math.PI / 2; // the startAngle of current coordinate
// Saves the index of dragging handle.
function dragstart(event) {
const { target } = event;
index = circles.findIndex((d) => d === target);
if (index === -1) return;
}
// Updates handle position and rerender chart.
function drag(event) {
if (index === -1) return;
// Cet prev, current and next handle.
const nextIndex = (index + 1) % circles.length;
const prevIndex = (index - 1 + circles.length) % circles.length;
const current = circles[index];
const next = circles[nextIndex];
const prev = circles[prevIndex];
// Computes new position for current handle.
const [x, y] = mouse(plot, event);
const ct = angle([x, y]);
const center = [cx + radius * Math.cos(ct), cy + radius * Math.sin(ct)];
// Computes the angles between handles.
const nextCenter = [next.style.cx, next.style.cy];
const prevCenter = [prev.style.cx, prev.style.cy];
const t0 = angleBetween(prevCenter, center);
const t1 = angleBetween(center, nextCenter);
// If a sector is too small, it is undraggable.
if (t0 < minTheta || t1 < minTheta) return;
// If current handle is not between prev and next handle, it is undraggable.
const origin = [cx, cy];
const c1 = cross(sub(prevCenter, origin), sub(center, origin));
const c2 = cross(sub(center, origin), sub(nextCenter, origin));
if (c1 * c2 < 0) return;
// Update current handle and startAgnle if it is the first handle.
current.style.cx = center[0];
current.style.cy = center[1];
if (index === 0) startAngle = ct;
// Get options from interval node.
const interval = chart.getNodeByType("interval");
const data = interval.data();
const coordinate = interval.coordinate();
const fieldY = interval.encode().y;
// Compute new data by angles.
const datum = data[index][fieldY];
const prevDatum = data[prevIndex][fieldY];
const total = prevDatum + datum;
const newData = [...data];
newData[prevIndex][fieldY] = (total * t0) / (t0 + t1);
newData[index][fieldY] = (total * t1) / (t0 + t1);
// Update options for interval node.
interval
.data(newData)
.coordinate({
...coordinate,
startAngle,
endAngle: startAngle + Math.PI * 2
})
.animate(false);
// Rerender chart.
chart.render();
}
function angleBetween(v0, v1) {
const a0 = angle(v0);
const a1 = angle(v1);
if (a0 < a1) return a1 - a0;
return Math.PI * 2 - (a0 - a1);
}
function angle([x, y]) {
return Math.atan2(y - cy, x - cx);
}
function cross([x, y], [x1, y1]) {
return x * y1 - x1 * y;
}
function sub([x, y], [x1, y1]) {
return [x - x1, y - y1];
}
function mouse(target, event) {
const { offsetX, offsetY } = event;
const bbox = target.getRenderBounds();
const {
min: [x, y],
max: [x1, y1]
} = bbox;
return [
Math.min(x1, Math.max(x, offsetX)) - x,
Math.min(y1, Math.max(y, offsetY)) - y
];
}
}