Skip to content

Commit

Permalink
feat(data): Intent to ship data.labels.position function
Browse files Browse the repository at this point in the history
Implement customizable position function for data labels

Close #3237
  • Loading branch information
netil committed Jun 12, 2023
1 parent b22966f commit 59be3ec
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 34 deletions.
24 changes: 24 additions & 0 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -2703,6 +2703,30 @@ var demos = {
}
}
},
{
options: {
data: {
columns: [
["data1", 150, 240, 400, 350, 300],
["data2", 80, 10, 200, 240, 100]
],
type: "step",
labels: {
show: true,
position: function(type, v, id, i, texts) {
let pos = 0;
const len = texts.size() / 2 - 1;

if (type === "x" && (i === 0 || i === len)) {
pos = i === 0 ? 20 : -20;
}

return pos;
}
}
}
}
}
],
DataLabelRotate: {
options: {
Expand Down
35 changes: 20 additions & 15 deletions src/ChartInternal/internals/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ function setRotatePos(
return {x, y};
}

/**
* Get data.labels.position value
* @param {object} d Data object
* @param {string} type x | y
* @returns {number} Position value
* @private
*/
function getTextPos(d, type): number {
const position = this.config.data_labels_position;
const {id, index, value} = d;

return (
isFunction(position) ?
position.bind(this.api)(type, value, id, index, this.$el.text) :
(id in position ? position[id] : position)[type]
) ?? 0;
}

export default {
opacityForText(d): null | "0" {
const $$ = this;
Expand Down Expand Up @@ -424,19 +442,6 @@ export default {
return 0;
},

/**
* Get data.labels.position value
* @param {string} id Data id value
* @param {string} type x | y
* @returns {number} Position value
* @private
*/
getTextPos(id, type): number {
const pos = this.config.data_labels_position;

return (id in pos ? pos[id] : pos)[type] || 0;
},

/**
* Gets the x coordinate of the text
* @param {object} points Data points position
Expand Down Expand Up @@ -494,7 +499,7 @@ export default {
xPos += $$.getCenteredTextPos(d, points, textElement, "x");
}

return xPos + $$.getTextPos(d.id, "x");
return xPos + getTextPos.call(this, d, "x");
},

/**
Expand Down Expand Up @@ -582,7 +587,7 @@ export default {
yPos += $$.getCenteredTextPos(d, points, textElement, "y");
}

return yPos + $$.getTextPos(d.id, "y");
return yPos + getTextPos.call(this, d, "y");
},

/**
Expand Down
28 changes: 22 additions & 6 deletions src/config/Options/data/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import type {ChartTypes} from "../../../../types/types";
import type {ChartTypes, d3Selection} from "../../../../types/types";

/**
* data config options
Expand Down Expand Up @@ -321,7 +321,7 @@ export default {
* @property {boolean} [data.labels=false] Show or hide labels on each data points
* @property {boolean} [data.labels.centered=false] Centerize labels on `bar` shape. (**NOTE:** works only for 'bar' type)
* @property {Function} [data.labels.format] Set formatter function for data labels.<br>
* The formatter function receives 4 arguments such as v, id, i, j and it **must return a string**(`\n` character will be used as line break) that will be shown as the label.<br><br>
* The formatter function receives 4 arguments such as `v, id, i, texts` and it **must return a string** (`\n` character will be used as line break) that will be shown as the label.<br><br>
* The arguments are:<br>
* - `v` is the value of the data point where the label is shown.
* - `id` is the id of the data where the label is shown.
Expand All @@ -330,7 +330,14 @@ export default {
* Formatter function can be defined for each data by specifying as an object and D3 formatter function can be set (ex. d3.format('$'))
* @property {string|object} [data.labels.backgroundColors] Set label text background colors.
* @property {string|object|Function} [data.labels.colors] Set label text colors.
* @property {object} [data.labels.position] Set each dataset position, relative the original.
* @property {object|Function} [data.labels.position] Set each dataset position, relative the original.<br><br>
* When function is specified, will receives 5 arguments such as `type, v, id, i, texts` and it must return a position number.<br><br>
* The arguments are:<br>
* - `type` coordinate type string, which will be 'x' or 'y'.
* - `v` is the value of the data point where the label is shown.
* - `id` is the id of the data where the label is shown.
* - `i` is the index of the data series point where the label is shown.
* - `texts` is the array of whole corresponding data series' text labels.<br><br>
* @property {number} [data.labels.position.x=0] x coordinate position, relative the original.
* @property {number} [data.labels.position.y=0] y coordinate position, relative the original.
* @property {object} [data.labels.rotate] Rotate label text. Specify degree value in a range of `0 ~ 360`.
Expand All @@ -351,7 +358,7 @@ export default {
*
* // or set specific options
* labels: {
* format: function(v, id, i, j) {
* format: function(v, id, i, texts) {
* ...
* // to multiline, return with '\n' character
* return "Line1\nLine2";
Expand Down Expand Up @@ -393,6 +400,13 @@ export default {
* return d.value > 200 ? "cyan" : color;
* },
*
* // return x, y coordinate position
* // apt to handle each text position manually
* position: function(type, v, id, i, texts) {
* ...
* return type == "x" ? 10 : 20;
* },
*
* // set x, y coordinate position
* position: {
* x: -10,
Expand All @@ -413,9 +427,11 @@ export default {
data_labels:
<boolean | {
centered?: boolean;
format?: Function;
format?: (v: number, id: string, i: number, texts: d3Selection) => number;
colors?: string|{[key: string]: string};
position?: {[key: string]: number}|{[key: string]: {x?: number; y?: number;}};
position?: (type: "x" | "y", v: number, id: string, i: number, texts: d3Selection) => number |
{[key: string]: number} |
{[key: string]: {x?: number; y?: number;}};
rotate?: number;
}> {},
data_labels_backgroundColors: <string|{[key: string]: string}|undefined> undefined,
Expand Down
52 changes: 51 additions & 1 deletion test/internals/text-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ describe("TEXT", () => {

it("newly added text shouldn't be transitioning from the top/left", done => {
const main = chart.$.main;
const pos = [];
const pos: number[] = [];
let text;
let interval;

Expand Down Expand Up @@ -1401,6 +1401,56 @@ describe("TEXT", () => {
});
});

describe("Labels' postion callback", () => {
let pos: number[] = [];

before(() => {
args = {
data: {
columns: [
["data1", 30, 200, 200],
["data2", 130, 100, 140]
],
type: "line",
labels: {
show: true
}
}
};
});

it("get normal position", () => {
chart.$.text.texts.each(function(d, i) {
if (i === 0 || i === 2) {
pos.push(+this.getAttribute("x"));
}
});
});

it("set options data.labels.position", () => {
args.data.labels.position = function(type, v, id, i, texts) {
let pos = 0;
const len = texts.size() / 2 - 1;

if (type === "x" && (i === 0 || i === len)) {
pos = i === 0 ? 20 : -20;
}

return pos;
}
});

it("position coordinate should specified as callback returns.", () => {
chart.$.text.texts.each(function(d, i) {
if (i === 0 || i === 2) {
expect(+this.getAttribute("x")).to.be.equal(
(pos.shift() ?? 0) + (i === 0 ? 20 : -20)
);
}
});
});
});

describe("labels.colors callback", () => {
let ctx = [];

Expand Down
41 changes: 29 additions & 12 deletions types/options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ import {
TreemapOptions
} from "./options.shape";

export type FormatFunction = (
this: Chart,
v: any,
id: string,
i: number,
texts: d3Selection
) => string;

export type PositionFunction = (
this: Chart,
type: "x" | "y",
v: number,
id: string,
i: number,
texts: d3Selection
) => number;

export interface ChartOptions {
/**
* Specify the CSS selector or the element which the chart will be set to. D3 selection object can be specified also.
Expand Down Expand Up @@ -1081,10 +1098,18 @@ export interface Data {
*/
format?: FormatFunction | { [key: string]: FormatFunction };

position?: {
/**
* Set each dataset position, relative the original.
*/
/**
* Set each dataset position, relative the original.
*
* When function is specified, will receives 5 arguments such as `type, v, id, i, texts` and it must return a position number.<br><br>
* The arguments are:<br>
* - `type` coordinate type string, which will be 'x' or 'y'.
* - `v` is the value of the data point where the label is shown.
* - `id` is the id of the data where the label is shown.
* - `i` is the index of the data series point where the label is shown.
* - `texts` is the array of whole corresponding data series' text labels.<br><br>
*/
position?: PositionFunction | {
[key: string]: {
/**
* x coordinate position, relative the original.
Expand Down Expand Up @@ -1271,11 +1296,3 @@ export interface Data {
*/
onhidden?(this: Chart, ids: string[]): void;
}

export type FormatFunction = (
this: Chart,
v: any,
id: string,
i: number,
texts: SVGTextElement[]
) => void;

0 comments on commit 59be3ec

Please sign in to comment.