From cfc856c081ff1cebe47c8929723dca6d2cf3bb66 Mon Sep 17 00:00:00 2001 From: Jae Sung Park Date: Fri, 8 Dec 2023 17:23:31 +0900 Subject: [PATCH] feat(axis): Add x.tick.text.inner option Implement tick text inner option Close #3552 --- .eslintrc | 4 ++ demo/demo.js | 97 ++++++++++++++++++++++++++ src/ChartInternal/Axis/AxisRenderer.ts | 14 ++++ src/config/Options/axis/x.ts | 26 +++++++ test/api/export-spec.ts | 6 +- test/internals/axis-x-spec.ts | 69 ++++++++++++++++++ types/options.d.ts | 8 +++ 7 files changed, 221 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index 279ca82d1..b1e5ed3a3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,9 @@ { "parser": "@typescript-eslint/parser", + "parserOptions": { + "warnOnUnsupportedTypeScriptVersion": false + }, + "root": true, "plugins": [ "jsdoc", "@typescript-eslint" diff --git a/demo/demo.js b/demo/demo.js index 58067932d..1f577f2ad 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -1867,6 +1867,103 @@ var demos = { } } }, + XAxisTickInner: [ + { + options: { + data: { + x: "x", + xFormat: "%Y", + columns: [ + ["x", "2020", "2021", "2022", "2023", "2024"], + ["data1", 30, 200, 100, 400, 150], + ["data2", 130, 340, 200, 500, 250] + ], + type: "line" + }, + axis: { + x: { + type: "timeseries", + tick: { + format: "%Y-%m-%d %H:%M:%S" + } + }, + y: { + show: false + } + } + } + }, + { + options: { + title: { + text: "axis.x.tick.text.inner = true", + padding: { + top: 20 + } + }, + data: { + x: "x", + xFormat: "%Y", + columns: [ + ["x", "2020", "2021", "2022", "2023", "2024"], + ["data1", 30, 200, 100, 400, 150], + ["data2", 130, 340, 200, 500, 250] + ], + type: "line" + }, + axis: { + x: { + type: "timeseries", + tick: { + text: { + inner: true + }, + format: "%Y-%m-%d %H:%M:%S" + } + }, + y: { + show: false + } + } + } + }, + { + options: { + title: { + text: "axis.x.tick.text.inner.last = true", + padding: { + top: 20 + } + }, + data: { + x: "x", + xFormat: "%Y", + columns: [ + ["x", "2020", "2021", "2022", "2023", "2024"], + ["data1", 30, 200, 100, 400, 150], + ["data2", 130, 340, 200, 500, 250] + ], + type: "line" + }, + axis: { + x: { + type: "timeseries", + tick: { + text: { + inner: { + last: true + } + }, + format: "%Y-%m-%d %H:%M:%S" + } + }, + y: { + show: false + } + } + } + } + ], XAxisTickMultiline: { options: { data: { diff --git a/src/ChartInternal/Axis/AxisRenderer.ts b/src/ChartInternal/Axis/AxisRenderer.ts index 75b83df4f..8ec07b6fd 100644 --- a/src/ChartInternal/Axis/AxisRenderer.ts +++ b/src/ChartInternal/Axis/AxisRenderer.ts @@ -313,6 +313,11 @@ export default class AxisRenderer { return r ? 11.5 - 2.5 * r2 * (r > 0 ? 1 : -1) : tickLength; }; + const {config: { + axis_rotated: isRotated, + axis_x_tick_text_inner: inner + }} = this.params.owner; + switch (orient) { case "bottom": lineUpdate @@ -324,6 +329,15 @@ export default class AxisRenderer { .attr("x", 0) .attr("y", yForText(rotate)) .style("text-anchor", textAnchorForText(rotate)) + .style("text-anchor", (d, i, {length}) => { + if (!isRotated && i === 0 && (inner === true || inner.first)) { + return "start"; + } else if (!isRotated && i === length - 1 && (inner === true || inner.last)) { + return "end"; + } + + return textAnchorForText(rotate); + }) .attr("transform", textTransform(rotate)); break; case "top": diff --git a/src/config/Options/axis/x.ts b/src/config/Options/axis/x.ts index 34bd4640b..0ef7f9e15 100644 --- a/src/config/Options/axis/x.ts +++ b/src/config/Options/axis/x.ts @@ -259,6 +259,32 @@ export default { */ axis_x_tick_text_show: true, + /** + * Set the first/last axis tick text to be positioned inside of the chart on non-rotated axis. + * @name axis․x․tick․text․inner + * @memberof Options + * @type {boolean|object} + * @default false + * @see [Demo](https://naver.github.io/billboard.js/demo/#Axis.XAxisTickInner) + * @example + * axis: { + * x: { + * tick: { + * text: { + * inner: true, + * + * // or specify each position of the first and last tick text + * inner: { + * first: true, + * last: true + * } + * } + * } + * } + * } + */ + axis_x_tick_text_inner: <{first?: boolean, last?: boolean}|boolean> false, + /** * Set the x Axis tick text's position relatively its original position * @name axis․x․tick․text․position diff --git a/test/api/export-spec.ts b/test/api/export-spec.ts index 5f979b5c6..62cd5650e 100644 --- a/test/api/export-spec.ts +++ b/test/api/export-spec.ts @@ -201,9 +201,9 @@ describe("API export", () => { // pattern for local: preserveFontStyle=true [ - "RIYJUAB6NhIiLgAtL/ub4XqgdKfecox9OPcoQB0OTv0bSIB7e3qA0Y2gY", - "AgBISAEhIAQKGoCJp0CLmrQKTTueAADAfQE8BWAp93Lr/UvPgn5IaAPIOm3z", - "KxX72Ifp34GYBSAiQB6A9ArZNWuoLwQwFr3EfDLeRCA2wB4CMCu7srkyBgBqFcq" + "fvvffeIxM58XrgmUMC0SNAARi9nDKiAAjwhhcARJoIHYFUKvVBAE8WOPZ7AO", + "bMh8hbpc48ut3UqlbpHVTcVEV1rrbX2nj17NreBKRce69VEQHtib0KH9wfMuHmnNRmtc", + "FIFP52aPiu66mVdn4sl1yFwBoCIgBLrzXo5SlXNQwb9Cn1PKYRZRvpGmo" ], // pattern for CI diff --git a/test/internals/axis-x-spec.ts b/test/internals/axis-x-spec.ts index ece7eb9ee..c178bab90 100644 --- a/test/internals/axis-x-spec.ts +++ b/test/internals/axis-x-spec.ts @@ -288,5 +288,74 @@ describe("X AXIS", function() { ).to.be.above(state.height); }); }); + + describe("tick.text.inner", () => { + before(() => { + args = { + data: { + x: "x", + xFormat: "%Y", + columns: [ + ["x", "2020", "2021", "2022", "2023", "2024"], + ["data1", 30, 200, 100, 400, 150], + ["data2", 130, 340, 200, 500, 250] + ], + type: "line" + }, + axis: { + x: { + type: "timeseries", + tick: { + format: "%Y-%m-%d %H:%M:%S", + text: { + inner: true + } + } + } + } + }; + }); + + it("should first & last tick text to be positioned at inner.", () => { + chart.internal.$el.axis.x + .selectAll(".tick:first-of-type > text, .tick:last-of-type > text").each(function(d, i) { + const anchor = this.style.textAnchor; + + expect(anchor).to.be.equal(i === 0 ? "start" : "end"); + }); + }); + + it("set options: axis.x.tick.text.inner={first:true, last:false}", () => { + args.axis.x.tick.text.inner = { + first: true, + last: false + }; + }); + + it("should first tick text to be positioned at inner.", () => { + chart.internal.$el.axis.x + .selectAll(".tick:first-of-type > text, .tick:last-of-type > text").each(function(d, i) { + const anchor = this.style.textAnchor; + + expect(anchor).to.be.equal(i === 0 ? "start" : "middle"); + }); + }); + + it("set options: axis.x.tick.text.inner={first:false, last:true}", () => { + args.axis.x.tick.text.inner = { + first: false, + last: true + }; + }); + + it("should last tick text to be positioned at inner.", () => { + chart.internal.$el.axis.x + .selectAll(".tick:first-of-type > text, .tick:last-of-type > text").each(function(d, i) { + const anchor = this.style.textAnchor; + + expect(anchor).to.be.equal(i === 0 ? "middle" : "end"); + }); + }); + }); }); }); diff --git a/types/options.d.ts b/types/options.d.ts index 508865c1b..e64228490 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -747,6 +747,14 @@ export interface SubchartOptions { * Show or hide x axis tick text. */ show?: boolean; + + /** + * * Set the first/last axis tick text to be positioned inside of the chart on non-rotated axis. + */ + inner?: boolean | { + first?: boolean; + last?: boolean; + } }; }; };