Skip to content

Commit

Permalink
feat(data): Intent to ship data.labels.rotate
Browse files Browse the repository at this point in the history
Implement data.labels.rotate option

Close #2662
  • Loading branch information
netil committed May 27, 2022
1 parent 27b6450 commit 7b7ee08
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 188 deletions.
18 changes: 18 additions & 0 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -2503,6 +2503,24 @@ var demos = {
}
},
],
DataLabelRotate: {
options: {
data: {
columns: [
["data1", 30, -200, -100, 400, 150, 250]
],
type: "bar",
labels: {
rotate: 90
}
},
axis: {
x: {
type: "category"
}
}
}
},
DataSelection: {
options: {
data: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.1",
"tslib": "^2.4.0",
"typescript": "^4.6.3",
"typescript": "4.6.2",
"webpack": "^5.72.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-clean": "^1.2.5",
Expand Down
97 changes: 86 additions & 11 deletions src/ChartInternal/internals/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,70 @@ import {capitalize, getBoundingRect, getRandom, isFunction, isNumber, isObject,
import {IDataRow, IArcData} from "../data/IData";
import {AxisType} from "../../../types/types";

type Coord = {x: number, y: number};
type Anchor = "start" | "middle" | "end";

/**
* Get text-anchor according text.labels.rotate angle
* @param {number} angle Angle value
* @returns {string} Anchor string value
* @private
*/
function getRotateAnchor(angle: number): Anchor {
let anchor: Anchor = "middle";

if (angle > 0 && angle <= 170) {
anchor = "end";
} else if (angle > 190 && angle <= 360) {
anchor = "start";
}

return anchor;
}

/**
* Set rotated position coordinate according text.labels.rotate angle
* @param {number} value Data value
* @param {object} pos Position object
* @param {object} pos.x x coordinate
* @param {object} pos.y y coordinate
* @param {string} anchor string value
* @param {boolean} isRotated If axis is rotated
* @returns {object} x, y coordinate
* @private
*/
function setRotatePos(value: number, pos: Coord, anchor: Anchor, isRotated: boolean): Coord {
let {x, y} = pos;
const isNegative = value < 0;
const gap = 4;
const doubleGap = gap * 2;

if (isRotated) {
if (anchor === "start") {
x += isNegative ? 0 : doubleGap;
y += gap;
} else if (anchor === "middle") {
x += doubleGap;
y -= doubleGap;
} else if (anchor === "end") {
isNegative && (x -= doubleGap);
y += gap;
}
} else {
if (anchor === "start") {
x += gap;
isNegative && (y += doubleGap * 2);
} else if (anchor === "middle") {
y -= doubleGap;
} else if (anchor === "end") {
x -= gap;
isNegative && (y += doubleGap * 2);
}
}

return {x, y};
}

export default {
opacityForText(d): null | "0" {
const $$ = this;
Expand Down Expand Up @@ -171,34 +235,45 @@ export default {

/**
* Redraw chartText
* @param {Function} x Positioning function for x
* @param {Function} y Positioning function for y
* @param {Function} getX Positioning function for x
* @param {Function} getY Positioning function for y
* @param {boolean} forFlow Weather is flow
* @param {boolean} withTransition transition is enabled
* @returns {Array}
* @private
*/
redrawText(x, y, forFlow?: boolean, withTransition?: boolean): true {
redrawText(getX, getY, forFlow?: boolean, withTransition?: boolean): true {
const $$ = this;
const {$T} = $$;
const {$T, config} = $$;
const t = <string>getRandom(true);
const isRotated = config.axis_rotated;
const angle = config.data_labels.rotate;

const anchorString = getRotateAnchor(angle);
const rotateString = angle ? `rotate(${angle})` : "";

$$.$el.text
.style("fill", $$.updateTextColor.bind($$))
.attr("filter", $$.updateTextBacgroundColor.bind($$))
.style("fill-opacity", forFlow ? 0 : $$.opacityForText.bind($$))
.each(function(d, i) {
.each(function(d: IDataRow, i: number) {
// do not apply transition for newly added text elements
const node = $T(this, !!(withTransition && this.getAttribute("x")), t);

const posX = x.bind(this)(d, i);
const posY = y.bind(this)(d, i);
let pos = {
x: getX.bind(this)(d, i),
y: getY.bind(this)(d, i)
};

if (angle) {
pos = setRotatePos(d.value, pos, anchorString, isRotated);
node.attr("text-anchor", anchorString);
}

// when is multiline
if (this.childElementCount) {
node.attr("transform", `translate(${posX} ${posY})`);
if (this.childElementCount || angle) {
node.attr("transform", `translate(${pos.x} ${pos.y}) ${rotateString}`);
} else {
node.attr("x", posX).attr("y", posY);
node.attr("x", pos.x).attr("y", pos.y);
}
});

Expand Down
11 changes: 9 additions & 2 deletions src/config/Options/data/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ export default {
* @property {object} [data.labels.position] Set each dataset position, relative the original.
* @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`.
* - **NOTE:** Depend on rotate value, text position need to be adjusted manually(using `data.labels.position` option) to be shown nicely.
* @memberof Options
* @type {object}
* @default {}
Expand All @@ -321,6 +323,7 @@ export default {
* @see [Demo: label multiline](https://naver.github.io/billboard.js/demo/#Data.DataLabelMultiline)
* @see [Demo: label overlap](https://naver.github.io/billboard.js/demo/#Data.DataLabelOverlap)
* @see [Demo: label position](https://naver.github.io/billboard.js/demo/#Data.DataLabelPosition)
* @see [Demo: label rotate](https://naver.github.io/billboard.js/demo/#Data.DataLabelRotate)
* @example
* data: {
* labels: true,
Expand Down Expand Up @@ -379,7 +382,10 @@ export default {
* position: {
* data1: {x: 5, y: 5},
* data2: {x: 10, y: -20}
* }
* },
*
* // rotate degree for label text
* rotate: 90
* }
* }
*/
Expand All @@ -388,7 +394,8 @@ export default {
centered?: boolean;
format?: Function;
colors?: string|{[key: string]: string};
position?: {[key: string]: number}|{[key: string]: {x?: number; y?: number;}}
position?: {[key: string]: number}|{[key: string]: {x?: number; y?: number;}};
rotate?: number;
}> {},
data_labels_backgroundColors: <string|{[key: string]: string}|undefined> undefined,
data_labels_colors: <string|object|Function|undefined> undefined,
Expand Down
88 changes: 88 additions & 0 deletions test/internals/text-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,94 @@ describe("TEXT", () => {
});
});

describe("rotate", () => {
before(() => {
args = {
data: {
columns: [
["data1", 90, 100, -100]
],
type: "bar",
labels: {
rotate: 90
}
},
axis: {
rotated: false
}
}
});

it("rotate attribute should be applied", () => {
chart.$.text.texts.each(function(d) {
const transform = this.getAttribute("transform");
const anchor = this.getAttribute("text-anchor");

expect(transform.indexOf(`rotate(${args.data.labels.rotate})`) > -1).to.be.true;
expect(anchor).to.be.equal("end");

if (d.value < 0) {
const y = +this.getAttribute("transform").match(/\s(\d+\.\d+)/)[1];

expect(y).to.be.closeTo(405, 1);
}
});
});

it("set options: data.labels.rotate=180", () => {
args.data.labels.rotate = 180;
});

it("text-anchor should be middle for rotate(180deg)", () => {
chart.$.text.texts.each(function() {
const anchor = this.getAttribute("text-anchor");

expect(anchor).to.be.equal("middle");
});
});

it("set options: data.labels.rotate=270", () => {
args.data.labels.rotate = 270;
});

it("text-anchor should be middle for rotate(270deg)", () => {
chart.$.text.texts.each(function(d) {
const anchor = this.getAttribute("text-anchor");

expect(anchor).to.be.equal("start");

if (d.value < 0) {
const y = +this.getAttribute("transform").match(/\s(\d+\.\d+)/)[1];

expect(y).to.be.closeTo(405, 1);
}
});
});

it("set options: axis.rotated=true", () => {
args.axis.rotated = true;
args.data.labels.rotate = 90;
});

it("check for rotated axis", () => {
const expectedY = [80, 220, 362];

chart.$.text.texts.each(function(d, i) {
const transform = +this.getAttribute("transform").match(/\s(\d+\.\d+)/)[1];
const anchor = this.getAttribute("text-anchor");

expect(transform).to.be.closeTo(expectedY[i], 1);
expect(anchor).to.be.equal("end");

if (d.value < 0) {
const x = +this.getAttribute("transform").match(/\((\d+\.\d+)/)[1];

expect(x).to.be.closeTo(57, 1);
}
});
});
});

describe("on area chart", () => {
before(() => {
args = {
Expand Down
7 changes: 6 additions & 1 deletion types/options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1613,7 +1613,12 @@ export interface Data {
*/
y?: number;
};
}
};

/**
* Rotate label text. Specify degree value in a range of `0 ~ 360`.
*/
rotate?: number;
};

/**
Expand Down
Loading

0 comments on commit 7b7ee08

Please sign in to comment.