From 56c0a3ed9b3d5a7cfa17da04bb0851d3e4027c1b Mon Sep 17 00:00:00 2001
From: bqxbqx <132878537+BQXBQX@users.noreply.github.com>
Date: Thu, 5 Dec 2024 22:56:22 +0800
Subject: [PATCH] feat(label): implement label color inheritance from dependent
elements (#6536)
* feat(label): implement label color inheritance from dependent elements
Implement the ability for labels to inherit colors from their associated
graphical elements. This allows labels to automatically match the color
of the elements they are describing.
- Add style inheritance in label creation
- Pass element style information to label callbacks
- Add test case to verify the implementation
* refactor(label): improve style callback compatibility and clarity
- Remove Proxy usage in label style callback to prevent browser compatibility issues
- Rename style parameter to elementStyle for better semantic clarity
- Update documentation to clarify elementStyle parameter usage in label callbacks
* refactor: simplify object property using shorthand notation
---
__tests__/integration/issue-5474.spec.ts | 35 +
.../snapshots/bugfix/issue5474.svg | 1602 +++++++++++++++++
__tests__/plots/bugfix/index.ts | 1 +
__tests__/plots/bugfix/issue-5474.ts | 37 +
site/docs/spec/label/overview.zh.md | 2 +-
src/runtime/plot.ts | 14 +-
6 files changed, 1687 insertions(+), 4 deletions(-)
create mode 100644 __tests__/integration/issue-5474.spec.ts
create mode 100644 __tests__/integration/snapshots/bugfix/issue5474.svg
create mode 100644 __tests__/plots/bugfix/issue-5474.ts
diff --git a/__tests__/integration/issue-5474.spec.ts b/__tests__/integration/issue-5474.spec.ts
new file mode 100644
index 0000000000..d51366e32c
--- /dev/null
+++ b/__tests__/integration/issue-5474.spec.ts
@@ -0,0 +1,35 @@
+import { issue5474 as render } from '../plots/bugfix/issue-5474';
+import { createNodeGCanvas } from './utils/createNodeGCanvas';
+import { sleep } from './utils/sleep';
+import './utils/useSnapshotMatchers';
+
+describe('issue5474', () => {
+ const canvas = createNodeGCanvas(800, 500);
+
+ it('issue5474.render() should render chart with labels matching element colors', async () => {
+ const { chart } = render({
+ canvas,
+ container: document.createElement('div'),
+ });
+
+ await chart.render();
+ await sleep(20);
+
+ const labels = canvas.document.getElementsByClassName('label');
+ expect(labels.length).toBeGreaterThan(0);
+
+ labels.forEach((label) => {
+ expect(label.style.fill).toBe(
+ // @ts-ignore
+ label.__data__.dependentElement.attributes.fill,
+ );
+ });
+
+ const dir = `${__dirname}/snapshots/bugfix`;
+ await expect(canvas).toMatchDOMSnapshot(dir, render.name);
+ });
+
+ afterAll(() => {
+ canvas?.destroy();
+ });
+});
diff --git a/__tests__/integration/snapshots/bugfix/issue5474.svg b/__tests__/integration/snapshots/bugfix/issue5474.svg
new file mode 100644
index 0000000000..9c7a411610
--- /dev/null
+++ b/__tests__/integration/snapshots/bugfix/issue5474.svg
@@ -0,0 +1,1602 @@
+
\ No newline at end of file
diff --git a/__tests__/plots/bugfix/index.ts b/__tests__/plots/bugfix/index.ts
index 8e2caa72e2..5fb5c75b2f 100644
--- a/__tests__/plots/bugfix/index.ts
+++ b/__tests__/plots/bugfix/index.ts
@@ -2,3 +2,4 @@ export { issue6396 } from './issue-6396';
export { issue6399 } from './issue-6399';
export { issueChart2719 } from './issue-chart-2719';
export { issue6020 } from './issue-6020';
+export { issue5474 } from './issue-5474';
diff --git a/__tests__/plots/bugfix/issue-5474.ts b/__tests__/plots/bugfix/issue-5474.ts
new file mode 100644
index 0000000000..70d505fa35
--- /dev/null
+++ b/__tests__/plots/bugfix/issue-5474.ts
@@ -0,0 +1,37 @@
+import { Chart } from '../../../src';
+
+export function issue5474(context) {
+ const { container, canvas, callback } = context;
+
+ const chart = new Chart({
+ container: container,
+ autoFit: true,
+ insetRight: 10,
+ canvas,
+ });
+
+ if (callback) {
+ callback(chart);
+ } else {
+ chart
+ .interval()
+ .data([
+ { genre: 'Sports', sold: 0 },
+ { genre: 'Strategy', sold: 115 },
+ { genre: 'Action', sold: 120 },
+ { genre: 'Shooter', sold: 350 },
+ { genre: 'Other', sold: 150 },
+ ])
+ .encode('x', 'genre')
+ .encode('y', 'sold')
+ .encode('color', 'genre')
+ .label({
+ text: 'genre',
+ fill: (_, i, array, d) => d.elementStyle.fill,
+ });
+ }
+
+ chart.render();
+
+ return { chart };
+}
diff --git a/site/docs/spec/label/overview.zh.md b/site/docs/spec/label/overview.zh.md
index c7408b5bd0..e327ef3c85 100644
--- a/site/docs/spec/label/overview.zh.md
+++ b/site/docs/spec/label/overview.zh.md
@@ -127,7 +127,7 @@ chart
d,
i,
data,
- { channel }, // 聚合图形的样式
+ { channel, elementStyle }, // 聚合图形的样式 & label依赖元素的样式
) => (channel.y[i] < 11700 ? '#E49361' : '#4787F7'),
);
```
diff --git a/src/runtime/plot.ts b/src/runtime/plot.ts
index a5ee20e6be..ae1725b8c5 100644
--- a/src/runtime/plot.ts
+++ b/src/runtime/plot.ts
@@ -1197,7 +1197,9 @@ function plotLabel(
return elements.flatMap((e) => {
const L = getLabels(options, i, e);
L.forEach((l) => {
- labelShapeFunction.set(l, shapeFunction);
+ labelShapeFunction.set(l, (data) =>
+ shapeFunction({ ...data, elementStyle: e.attributes }),
+ );
labelDescriptor.set(l, labelOption);
});
return L;
@@ -1365,11 +1367,17 @@ function createLabelShapeFunction(
style: abstractStyle,
render,
selector,
+ elementStyle,
...abstractOptions
} = options;
+
const visualOptions = mapObject(
{ ...abstractOptions, ...abstractStyle } as Record,
- (d) => valueOf(d, datum, index, abstractData, { channel }),
+ (d) =>
+ valueOf(d, datum, index, abstractData, {
+ channel: { ...channel },
+ elementStyle,
+ }),
);
const { shape = defaultLabelShape, text, ...style } = visualOptions;
const f = typeof formatter === 'string' ? format(formatter) : formatter;
@@ -1393,7 +1401,7 @@ function valueOf(
datum: Record,
i: number,
data: Record,
- options: { channel: Record },
+ options: { channel: Record; elementStyle?: Record },
) {
if (typeof value === 'function') return value(datum, i, data, options);
if (typeof value !== 'string') return value;