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
20 changes: 17 additions & 3 deletions src/ChartInternal/shape/treemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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}%`;
};
}
Expand Down
10 changes: 8 additions & 2 deletions src/config/Options/shape/treemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br>
* - **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)
Expand All @@ -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
Expand Down
162 changes: 162 additions & 0 deletions test/shape/treemap-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
}
});
});
});
});
2 changes: 1 addition & 1 deletion types/options.shape.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down