Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 33 additions & 3 deletions component/src/charts/__tests__/circle-packing-chart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,12 @@ describe("CirclePackingChart", () => {
expect(typeof optionsCall.series[0].renderItem).toBe("function");
});

// [x, y, r, depth, value, color, name, kind]; kind 0 = circle(+leaf label),
// 1 = parent-label pass. depth 2 = maxDepth (leaf) for hierarchicalData.
function leafText(color: string) {
render(<CirclePackingChart data={hierarchicalData} />);
const { renderItem } = mockSetOption.mock.calls[0][0].series[0];
// [x, y, r, depth, value, color, name] — a leaf big enough to label.
const vals = [120, 120, 40, 1, 50, color, "React"];
const vals = [120, 120, 40, 2, 50, color, "React", 0];
const api = { value: (d: number) => vals[d], style: () => ({}) };
const group = renderItem(undefined, api) as {
children: { type: string; style?: Record<string, unknown> }[];
Expand All @@ -107,11 +108,40 @@ describe("CirclePackingChart", () => {
expect(text?.style?.fill).toBe("#000000");
});

it("renders parent labels as a top-rim pill drawn on top (separate pass)", () => {
render(<CirclePackingChart data={hierarchicalData} />);
const { renderItem } = mockSetOption.mock.calls[0][0].series[0];
// kind 1 = parent-label entry -> returns a standalone pill text element.
const vals = [120, 120, 40, 1, 50, "", "Backend", 1];
const api = { value: (d: number) => vals[d], style: () => ({}) };
const el = renderItem(undefined, api) as {
type: string;
style?: Record<string, unknown>;
};
expect(el.type).toBe("text");
expect(el.style?.text).toBe("Backend");
expect(el.style?.backgroundColor).toBe("rgba(0, 0, 0, 0.55)");
expect(el.style?.fill).toBe("#ffffff");
expect(el.style?.y as number).toBeLessThan(120); // top rim
});

it("emits parent-label entries after all circle entries (z-order on top)", () => {
render(<CirclePackingChart data={hierarchicalData} />);
const data = mockSetOption.mock.calls[0][0].series[0].data as {
value: unknown[];
}[];
const kinds = data.map((d) => d.value[7]);
const firstLabel = kinds.indexOf(1);
const lastCircle = kinds.lastIndexOf(0);
expect(firstLabel).toBeGreaterThan(-1); // there are parent labels
expect(firstLabel).toBeGreaterThan(lastCircle); // ...and they come last
});

it("fills depth circles from the citrine palette (no stock ECharts colors)", () => {
render(<CirclePackingChart data={hierarchicalData} />);
const { renderItem } = mockSetOption.mock.calls[0][0].series[0];
// depth 1, no per-node color -> falls back to the citrine depth palette.
const vals = [120, 120, 40, 1, 50, "", "Frontend"];
const vals = [120, 120, 40, 1, 50, "", "Frontend", 0];
const api = { value: (d: number) => vals[d], style: () => ({}) };
const group = renderItem(undefined, api) as {
children: { type: string; shape?: object; style?: { fill?: string } }[];
Expand Down
55 changes: 47 additions & 8 deletions component/src/charts/circle-packing-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ function CirclePackingChart({
const offsetX = (width - size) / 2;
const offsetY = (height - size) / 2;

// Deepest level = leaf circles. Parent labels are pinned to the top rim so
// they don't sit dead-centre under their (centred) child circles.
const maxDepth = nodes.reduce((m, n) => Math.max(m, n.depth), 0);

const renderItem = (
_params: unknown,
api: {
Expand All @@ -127,6 +131,34 @@ function CirclePackingChart({
const depth = api.value(3);
const nodeColor = api.value(5);
const name = String(api.value(6));
const kind = api.value(7); // 0 = circle (+leaf label), 1 = parent label

const fontSize = Math.max(8, Math.min(r / 3, 14));

// Parent-label pass: a small dark pill pinned to the top rim. Emitted as a
// separate, later data entry so it draws ON TOP of every circle — readable
// over the fill or any child that packs against the top, never hidden
// dead-centre under the (centred) children.
if (kind === 1) {
return {
type: "text",
style: {
text: name,
x: cx,
y: cy - r + fontSize,
fill: "#ffffff",
backgroundColor: "rgba(0, 0, 0, 0.55)",
padding: [2, 6] as [number, number],
borderRadius: 4,
fontSize,
fontWeight: "bold",
textAlign: "center",
textVerticalAlign: "top",
overflow: "truncate",
width: r * 1.4,
},
};
}

// depth 1 → first citrine color, depth 2 → second, … (cycling).
const fillColor =
Expand Down Expand Up @@ -155,17 +187,15 @@ function CirclePackingChart({
],
};

// Add label for leaf nodes or nodes with enough radius
if (showLabels && r > 18 && depth > 0) {
const fontSize = Math.max(8, Math.min(r / 3, 14));
// Leaf labels sit centred inside their circle (nothing draws over them);
// per-circle contrast text — black on light fills, white on dark.
if (showLabels && r > 18 && depth > 0 && depth >= maxDepth) {
(group.children as unknown[]).push({
type: "text",
style: {
text: name,
x: cx,
y: cy,
// Per-circle contrast: black on the light moss/mint leaves, white
// on the dark blue parents — crisp and readable, no halo.
fill: contrastTextColor(String(fillColor)),
fontSize,
fontWeight: depth <= 1 ? "bold" : "normal",
Expand All @@ -180,10 +210,19 @@ function CirclePackingChart({
return group;
};

// Build series data: [x, y, r, depth, value, color, name]
const seriesData = nodes.map((n) => ({
value: [n.x, n.y, n.r, n.depth, n.value, n.color ?? "", n.name],
// Circle entries first; parent-label entries appended so they render last
// (on top of all circles). value: [x, y, r, depth, value, color, name, kind]
const circleData = nodes.map((n) => ({
value: [n.x, n.y, n.r, n.depth, n.value, n.color ?? "", n.name, 0],
}));
const parentLabelData = showLabels
? nodes
.filter((n) => n.depth > 0 && n.depth < maxDepth && n.r > 18)
.map((n) => ({
value: [n.x, n.y, n.r, n.depth, n.value, n.color ?? "", n.name, 1],
}))
: [];
const seriesData = [...circleData, ...parentLabelData];

return {
tooltip: {
Expand Down
Loading