diff --git a/src/ChartInternal/shape/treemap.ts b/src/ChartInternal/shape/treemap.ts index 2ad6ff742..c19deb4e6 100644 --- a/src/ChartInternal/shape/treemap.ts +++ b/src/ChartInternal/shape/treemap.ts @@ -291,12 +291,12 @@ export default { /** * Get treemap data label format function * @param {object} d Data object - * @returns {function} + * @returns {function} Label formatter function * @private */ treemapDataLabelFormat(d: IDataRow): Function { const $$ = this; - const {config} = $$; + const {$el: {treemap}, config, scale: {x, y}} = $$; const {id, value} = d; const format = config.treemap_label_format; const ratio = $$.getRatio("treemap", d); @@ -306,11 +306,25 @@ export default { null : "0"; + // Get treemap dimensions for the specific data + const treemapNode = treemap.selectAll("g") + .filter(node => node.data.id === id) + .datum(); + + let width = 0; + let height = 0; + + if (treemapNode) { + const {x0, x1, y0, y1} = treemapNode; + width = x(x1) - x(x0); + height = y(y1) - y(y0); + } + return function(node) { node.style("opacity", meetLabelThreshold); return isFunction(format) ? - format.bind($$.api)(value, ratio, id) : + format.bind($$.api)(value, ratio, id, {width, height}) : `${id}\n${percentValue}%`; }; } diff --git a/src/config/Options/shape/treemap.ts b/src/config/Options/shape/treemap.ts index 9a64b13e3..8b5b56cc9 100644 --- a/src/config/Options/shape/treemap.ts +++ b/src/config/Options/shape/treemap.ts @@ -20,7 +20,12 @@ export default { * - sliceDice ([d3.treemapSliceDice](https://github.com/d3/d3-hierarchy/blob/main/README.md#treemapSliceDice)) * - squrify ([d3.treemapSquarify](https://github.com/d3/d3-hierarchy/blob/main/README.md#treemapSquarify)) * - resquarify ([d3.treemapResquarify](https://github.com/d3/d3-hierarchy/blob/main/README.md#treemapResquarify)) - * @property {function} [treemap.label.format] Set formatter for the label text. + * @property {function} [treemap.label.format] Set formatter for the label text.
+ * - **Arguments:** + * - `value {number}`: Data value + * - `ratio {number}`: The `ratio` of how much space this tile occupies relative to the total area (0~1) + * - `id {string}`: Data id + * - `size {object}`: Tile size `{width, height}` in pixels * @property {number} [treemap.label.threshold=0.05] Set threshold ratio to show/hide labels text. * @property {number} [treemap.label.show=true] Show or hide label text. * @see [Demo: treemap](https://naver.github.io/billboard.js/demo/#Chart.TreemapChart) @@ -34,7 +39,8 @@ export default { * show: false, * * // set label text formatter - * format: function(value, ratio, id) { + * format: function(value, ratio, id, size) { + * // size: {width, height} - tile size in pixels * return d3.format("$")(value); * * // to multiline, return with '\n' character diff --git a/test/shape/treemap-spec.ts b/test/shape/treemap-spec.ts index 0a390e220..e6941e31c 100644 --- a/test/shape/treemap-spec.ts +++ b/test/shape/treemap-spec.ts @@ -341,4 +341,166 @@ describe("TREEMAP", () => { expect(element.tagName).to.be.equal("rect"); }); }); + + describe("label format with dimensions", () => { + let formatSpy; + + beforeAll(() => { + formatSpy = sinon.spy((value, ratio, id, dimensions) => { + return `${id}: ${value} (${dimensions.width.toFixed(0)}x${dimensions.height.toFixed(0)})`; + }); + + args = { + data: { + columns: [ + ["data1", 1000], + ["data2", 500], + ["data3", 300], + ["data4", 200] + ], + type: "treemap", + labels: { + colors: "#000", + centered: true + } + }, + treemap: { + label: { + format: formatSpy + } + } + }; + }); + + it("should pass width and height to label format function", () => { + expect(formatSpy.called).to.be.true; + + // Check that format function was called with correct arguments + formatSpy.args.forEach((callArgs, i) => { + const [value, ratio, id, size] = callArgs; + + // Verify all expected parameters are present + expect(value).to.be.a("number"); + expect(ratio).to.be.a("number"); + expect(id).to.be.a("string"); + expect(size).to.be.an("object"); + + // Verify dimensions object has width and height + expect(size).to.have.property("width"); + expect(size).to.have.property("height"); + expect(size.width).to.be.a("number"); + expect(size.height).to.be.a("number"); + + // Width and height should be positive for visible nodes + expect(size.width).to.be.greaterThan(0); + expect(size.height).to.be.greaterThan(0); + }); + }); + + it("should render labels with dimension information", () => { + const texts = chart.$.text.texts; + + texts.each(function(d) { + const textContent = this.textContent; + const pattern = new RegExp(`${d.id}: ${d.value} \\(\\d+x\\d+\\)`); + + expect(pattern.test(textContent)).to.be.true; + }); + }); + + it("should calculate dimensions correctly for each node", () => { + const {internal: {scale: {x, y}}} = chart; + + chart.internal.$el.treemap.selectAll("g").each(function(d) { + const {x0, x1, y0, y1} = d; + const expectedWidth = x(x1) - x(x0); + const expectedHeight = y(y1) - y(y0); + + // Find the corresponding format call for this data id + const formatCall = formatSpy.args.find(args => args[2] === d.data.id); + + if (formatCall) { + const [, , , dimensions] = formatCall; + + expect(dimensions.width).to.be.closeTo(expectedWidth, 0.1); + expect(dimensions.height).to.be.closeTo(expectedHeight, 0.1); + } + }); + }); + }); + + describe("label threshold visibility", () => { + beforeAll(() => { + args = { + data: { + columns: [ + ["data1", 1000], + ["data2", 200], + ["data3", 500], + ["data4", 50], + ["data5", 100], + ["data6", 20] + ], + type: "treemap", + labels: true + }, + treemap: { + label: { + threshold: 0.05, + format: function(value, ratio, id, size) { + return `${id} (${size.width.toFixed(0)}x${size.height.toFixed(0)})`; + } + } + } + }; + }); + + it("should hide labels below threshold regardless of dimensions", () => { + const threshold = args.treemap.label.threshold; + const totalValue = chart.internal.$el.treemap.datum().value; + let hiddenCount = 0; + let visibleCount = 0; + + chart.$.text.texts.each(function(d) { + const ratio = d.value / totalValue; + const isUnderThreshold = ratio < threshold; + const opacity = this.style.opacity; + + if (isUnderThreshold) { + expect(opacity).to.be.equal("0"); + hiddenCount++; + } else { + expect(opacity).to.not.equal("0"); + visibleCount++; + } + }); + + // Verify that some labels are hidden and some are visible + expect(hiddenCount).to.be.greaterThan(0); + expect(visibleCount).to.be.greaterThan(0); + }); + + it("should still calculate dimensions for hidden labels", () => { + const threshold = args.treemap.label.threshold; + const totalValue = chart.internal.$el.treemap.datum().value; + + chart.data().forEach(data => { + const ratio = data.values[0].value / totalValue; + const isUnderThreshold = ratio < threshold; + + if (isUnderThreshold) { + // Even for hidden labels, dimensions should be calculated + const text = chart.$.text.texts.filter(function(d) { + return d.id === data.id; + }); + + text.each(function() { + // The text content should still contain dimension info + const hasPattern = /\(\d+x\d+\)/.test(this.textContent); + expect(hasPattern).to.be.true; + }); + } + }); + }); + }); }); diff --git a/types/options.shape.d.ts b/types/options.shape.d.ts index 02edffb29..6d562f0eb 100644 --- a/types/options.shape.d.ts +++ b/types/options.shape.d.ts @@ -1142,7 +1142,7 @@ export interface TreemapOptions { /** * Set formatter for the label. */ - format?: (this: Chart, value: number, ratio: number, id: string) => string; + format?: (this: Chart, value: number, ratio: number, id: string, size: {width: number, height: number}) => string; /** * Set threshold ratio to show/hide labels.