Skip to content

Commit 4d5ce0b

Browse files
authored
feat(treemap): Enhance label formatter to include tile size
Updated the treemap label formatter to accept an additional parameter for tile size, allowing for more dynamic label rendering based on the width and height of the tiles. Updated documentation to reflect the new argument. Close #4066
1 parent 4379e29 commit 4d5ce0b

File tree

4 files changed

+188
-6
lines changed

4 files changed

+188
-6
lines changed

src/ChartInternal/shape/treemap.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,12 +291,12 @@ export default {
291291
/**
292292
* Get treemap data label format function
293293
* @param {object} d Data object
294-
* @returns {function}
294+
* @returns {function} Label formatter function
295295
* @private
296296
*/
297297
treemapDataLabelFormat(d: IDataRow): Function {
298298
const $$ = this;
299-
const {config} = $$;
299+
const {$el: {treemap}, config, scale: {x, y}} = $$;
300300
const {id, value} = d;
301301
const format = config.treemap_label_format;
302302
const ratio = $$.getRatio("treemap", d);
@@ -306,11 +306,25 @@ export default {
306306
null :
307307
"0";
308308

309+
// Get treemap dimensions for the specific data
310+
const treemapNode = treemap.selectAll("g")
311+
.filter(node => node.data.id === id)
312+
.datum();
313+
314+
let width = 0;
315+
let height = 0;
316+
317+
if (treemapNode) {
318+
const {x0, x1, y0, y1} = treemapNode;
319+
width = x(x1) - x(x0);
320+
height = y(y1) - y(y0);
321+
}
322+
309323
return function(node) {
310324
node.style("opacity", meetLabelThreshold);
311325

312326
return isFunction(format) ?
313-
format.bind($$.api)(value, ratio, id) :
327+
format.bind($$.api)(value, ratio, id, {width, height}) :
314328
`${id}\n${percentValue}%`;
315329
};
316330
}

src/config/Options/shape/treemap.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ export default {
2020
* - sliceDice ([d3.treemapSliceDice](https://github.com/d3/d3-hierarchy/blob/main/README.md#treemapSliceDice))
2121
* - squrify ([d3.treemapSquarify](https://github.com/d3/d3-hierarchy/blob/main/README.md#treemapSquarify))
2222
* - resquarify ([d3.treemapResquarify](https://github.com/d3/d3-hierarchy/blob/main/README.md#treemapResquarify))
23-
* @property {function} [treemap.label.format] Set formatter for the label text.
23+
* @property {function} [treemap.label.format] Set formatter for the label text.<br>
24+
* - **Arguments:**
25+
* - `value {number}`: Data value
26+
* - `ratio {number}`: The `ratio` of how much space this tile occupies relative to the total area (0~1)
27+
* - `id {string}`: Data id
28+
* - `size {object}`: Tile size `{width, height}` in pixels
2429
* @property {number} [treemap.label.threshold=0.05] Set threshold ratio to show/hide labels text.
2530
* @property {number} [treemap.label.show=true] Show or hide label text.
2631
* @see [Demo: treemap](https://naver.github.io/billboard.js/demo/#Chart.TreemapChart)
@@ -34,7 +39,8 @@ export default {
3439
* show: false,
3540
*
3641
* // set label text formatter
37-
* format: function(value, ratio, id) {
42+
* format: function(value, ratio, id, size) {
43+
* // size: {width, height} - tile size in pixels
3844
* return d3.format("$")(value);
3945
*
4046
* // to multiline, return with '\n' character

test/shape/treemap-spec.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,166 @@ describe("TREEMAP", () => {
341341
expect(element.tagName).to.be.equal("rect");
342342
});
343343
});
344+
345+
describe("label format with dimensions", () => {
346+
let formatSpy;
347+
348+
beforeAll(() => {
349+
formatSpy = sinon.spy((value, ratio, id, dimensions) => {
350+
return `${id}: ${value} (${dimensions.width.toFixed(0)}x${dimensions.height.toFixed(0)})`;
351+
});
352+
353+
args = {
354+
data: {
355+
columns: [
356+
["data1", 1000],
357+
["data2", 500],
358+
["data3", 300],
359+
["data4", 200]
360+
],
361+
type: "treemap",
362+
labels: {
363+
colors: "#000",
364+
centered: true
365+
}
366+
},
367+
treemap: {
368+
label: {
369+
format: formatSpy
370+
}
371+
}
372+
};
373+
});
374+
375+
it("should pass width and height to label format function", () => {
376+
expect(formatSpy.called).to.be.true;
377+
378+
// Check that format function was called with correct arguments
379+
formatSpy.args.forEach((callArgs, i) => {
380+
const [value, ratio, id, size] = callArgs;
381+
382+
// Verify all expected parameters are present
383+
expect(value).to.be.a("number");
384+
expect(ratio).to.be.a("number");
385+
expect(id).to.be.a("string");
386+
expect(size).to.be.an("object");
387+
388+
// Verify dimensions object has width and height
389+
expect(size).to.have.property("width");
390+
expect(size).to.have.property("height");
391+
expect(size.width).to.be.a("number");
392+
expect(size.height).to.be.a("number");
393+
394+
// Width and height should be positive for visible nodes
395+
expect(size.width).to.be.greaterThan(0);
396+
expect(size.height).to.be.greaterThan(0);
397+
});
398+
});
399+
400+
it("should render labels with dimension information", () => {
401+
const texts = chart.$.text.texts;
402+
403+
texts.each(function(d) {
404+
const textContent = this.textContent;
405+
const pattern = new RegExp(`${d.id}: ${d.value} \\(\\d+x\\d+\\)`);
406+
407+
expect(pattern.test(textContent)).to.be.true;
408+
});
409+
});
410+
411+
it("should calculate dimensions correctly for each node", () => {
412+
const {internal: {scale: {x, y}}} = chart;
413+
414+
chart.internal.$el.treemap.selectAll("g").each(function(d) {
415+
const {x0, x1, y0, y1} = d;
416+
const expectedWidth = x(x1) - x(x0);
417+
const expectedHeight = y(y1) - y(y0);
418+
419+
// Find the corresponding format call for this data id
420+
const formatCall = formatSpy.args.find(args => args[2] === d.data.id);
421+
422+
if (formatCall) {
423+
const [, , , dimensions] = formatCall;
424+
425+
expect(dimensions.width).to.be.closeTo(expectedWidth, 0.1);
426+
expect(dimensions.height).to.be.closeTo(expectedHeight, 0.1);
427+
}
428+
});
429+
});
430+
});
431+
432+
describe("label threshold visibility", () => {
433+
beforeAll(() => {
434+
args = {
435+
data: {
436+
columns: [
437+
["data1", 1000],
438+
["data2", 200],
439+
["data3", 500],
440+
["data4", 50],
441+
["data5", 100],
442+
["data6", 20]
443+
],
444+
type: "treemap",
445+
labels: true
446+
},
447+
treemap: {
448+
label: {
449+
threshold: 0.05,
450+
format: function(value, ratio, id, size) {
451+
return `${id} (${size.width.toFixed(0)}x${size.height.toFixed(0)})`;
452+
}
453+
}
454+
}
455+
};
456+
});
457+
458+
it("should hide labels below threshold regardless of dimensions", () => {
459+
const threshold = args.treemap.label.threshold;
460+
const totalValue = chart.internal.$el.treemap.datum().value;
461+
let hiddenCount = 0;
462+
let visibleCount = 0;
463+
464+
chart.$.text.texts.each(function(d) {
465+
const ratio = d.value / totalValue;
466+
const isUnderThreshold = ratio < threshold;
467+
const opacity = this.style.opacity;
468+
469+
if (isUnderThreshold) {
470+
expect(opacity).to.be.equal("0");
471+
hiddenCount++;
472+
} else {
473+
expect(opacity).to.not.equal("0");
474+
visibleCount++;
475+
}
476+
});
477+
478+
// Verify that some labels are hidden and some are visible
479+
expect(hiddenCount).to.be.greaterThan(0);
480+
expect(visibleCount).to.be.greaterThan(0);
481+
});
482+
483+
it("should still calculate dimensions for hidden labels", () => {
484+
const threshold = args.treemap.label.threshold;
485+
const totalValue = chart.internal.$el.treemap.datum().value;
486+
487+
chart.data().forEach(data => {
488+
const ratio = data.values[0].value / totalValue;
489+
const isUnderThreshold = ratio < threshold;
490+
491+
if (isUnderThreshold) {
492+
// Even for hidden labels, dimensions should be calculated
493+
const text = chart.$.text.texts.filter(function(d) {
494+
return d.id === data.id;
495+
});
496+
497+
text.each(function() {
498+
// The text content should still contain dimension info
499+
const hasPattern = /\(\d+x\d+\)/.test(this.textContent);
500+
expect(hasPattern).to.be.true;
501+
});
502+
}
503+
});
504+
});
505+
});
344506
});

types/options.shape.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,7 @@ export interface TreemapOptions {
11421142
/**
11431143
* Set formatter for the label.
11441144
*/
1145-
format?: (this: Chart, value: number, ratio: number, id: string) => string;
1145+
format?: (this: Chart, value: number, ratio: number, id: string, size: {width: number, height: number}) => string;
11461146

11471147
/**
11481148
* Set threshold ratio to show/hide labels.

0 commit comments

Comments
 (0)