Public
Edited
Sep 13, 2023
Insert cell
Insert cell
{
const chart = new G2.Chart({ theme: "classic" });

// Specify the options of chart.
chart.options({
type: "interval",
data: [
{ genre: "Sports", sold: 275 },
{ genre: "Strategy", sold: 115 },
{ genre: "Action", sold: 120 },
{ genre: "Shooter", sold: 350 },
{ genre: "Other", sold: 150 }
],
encode: {
y: "sold",
color: "genre"
},
legend: false,
transform: [{ type: "stackY" }],
coordinate: { type: "theta" },
interaction: { tooltip: false }
});

// Apply interaction after renderering.
chart.render().then((chart) =>
applyDragInteraction(chart, {
fill: "#eee",
stroke: "#000"
})
);

return chart.getContainer();
}
Insert cell
function applyDragInteraction(
chart,
{
minTheta = 0.1, // the min theta betweetn pies
...style // the style of the handles
} = {}
) {
// Gets g canvas and the document.
const { canvas } = chart.getContext();
const { document } = canvas;

// Gets the plot area and data driven elements.
const [plot] = document.getElementsByClassName(G2.PLOT_CLASS_NAME);
const elements = document.getElementsByClassName(G2.ELEMENT_CLASS_NAME);

// Draws handles for dragging.
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));

// Adds event listeners for dragging.
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
];
}
}
Insert cell
G2 = require("@antv/g2@5.1.0/dist/g2.min.js")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more