From b4e3bc29f2e78d6547ce695f9ecc6aadf90c22a9 Mon Sep 17 00:00:00 2001 From: Jae Sung Park Date: Mon, 7 Aug 2023 19:45:32 +0900 Subject: [PATCH] feat(regions): Intent to ship regions.label Implement regions.label Close #3319 --- demo/demo.js | 55 +++++++++ src/ChartInternal/internals/region.ts | 47 +++++-- src/config/Options/common/main.ts | 14 ++- src/config/Options/data/axis.ts | 4 +- src/scss/billboard.scss | 6 +- src/scss/theme/dark.scss | 6 +- src/scss/theme/datalab.scss | 6 +- src/scss/theme/graph.scss | 6 +- src/scss/theme/insight.scss | 6 +- test/api/region-spec.ts | 94 +++++++++++++- test/internals/rergions-spec.ts | 170 ++++++++++++++++++++++++++ types/types.d.ts | 16 ++- 12 files changed, 403 insertions(+), 27 deletions(-) create mode 100644 test/internals/rergions-spec.ts diff --git a/demo/demo.js b/demo/demo.js index 0a9d038ee..ff513b766 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -3993,6 +3993,61 @@ d3.select(".chart_area") ] } }, + RegionLabel: { + options: { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 150, 250], + ["data2", 100, 150, 130, 200, 220, 190], + ], + axes: { + data2: "y2", + }, + type: "line", + colors: { + data1: "#ff0000" + } + }, + axis: { + y2: { + show: true + } + }, + regions: [ + { + axis: "x", + start: 1, + end: 2, + class: "regions_class1", + label: { + text: "Regions 1", + color: "red" + } + }, + { + axis: "y", + start: 100, + end: 300, + class: "regions_class2", + label: { + text: "Regions 2", + x: 50, + color: "blue" + } + }, + { + axis: "y2", + start: 200, + end: 220, + class: "regions_class3", + label: { + text: "Regions 3", + y: 10 + } + } + ] + } + }, RegionWithTimeseries: { options: { data: { diff --git a/src/ChartInternal/internals/region.ts b/src/ChartInternal/internals/region.ts index 8b07313e1..91c068844 100644 --- a/src/ChartInternal/internals/region.ts +++ b/src/ChartInternal/internals/region.ts @@ -5,10 +5,10 @@ import {select as d3Select} from "d3-selection"; // selection import {$REGION} from "../../config/classes"; import {isValue, parseDate} from "../../module/util"; -import type {AxisType} from "../../../types/types"; +import type {AxisType, RegionsType} from "../../../types/types"; export default { - initRegion() { + initRegion(): void { const $$ = this; const {$el} = $$; @@ -38,7 +38,8 @@ export default { .style("opacity", "0") .remove(); - const regionsEnter = regions.enter() + const regionsEnter = regions + .enter() .append("g"); regionsEnter @@ -48,12 +49,22 @@ export default { region.list = regionsEnter .merge(regions) .attr("class", $$.classRegion.bind($$)); + + region.list.each(function(d) { + const g = d3Select(this); + + if (g.select("text").size() === 0 && d.label?.text) { + d3Select(this).append("text") + .style("opacity", "0"); + } + }); }, - redrawRegion(withTransition) { + redrawRegion(withTransition: boolean) { const $$ = this; const {$el: {region}, $T} = $$; let regions = region.list.select("rect"); + let label = region.list.selectAll("text"); regions = $T(regions, withTransition) .attr("x", $$.regionX.bind($$)) @@ -61,6 +72,17 @@ export default { .attr("width", $$.regionWidth.bind($$)) .attr("height", $$.regionHeight.bind($$)); + label = $T(label, withTransition) + .attr("transform", d => { + const {x = 0, y = 0, rotated = false} = d.label ?? {}; + + return `translate(${$$.regionX.bind($$)(d) + x}, ${$$.regionY.bind($$)(d) + y})${rotated ? ` rotate(-90)` : ``}`; + }) + .attr("text-anchor", d => (d.label?.rotated ? "end" : null)) + .attr("dy", "1em") + .style("fill", d => d.label?.color ?? null) + .text(d => d.label?.text); + return [ regions .style("fill-opacity", d => (isValue(d.opacity) ? d.opacity : null)) @@ -69,11 +91,12 @@ export default { d3Select(this.parentNode) .selectAll("rect:not([x])") .remove(); - }) + }), + label.style("opacity", null) ]; }, - getRegionXY(type: AxisType, d): number { + getRegionXY(type: AxisType, d: RegionsType): number { const $$ = this; const {config, scale} = $$; const isRotated = config.axis_rotated; @@ -99,15 +122,15 @@ export default { return pos; }, - regionX(d): number { + regionX(d: RegionsType): number { return this.getRegionXY("x", d); }, - regionY(d): number { + regionY(d: RegionsType): number { return this.getRegionXY("y", d); }, - getRegionSize(type, d): number { + getRegionSize(type: "width" | "height", d: RegionsType): number { const $$ = this; const {config, scale, state} = $$; const isRotated = config.axis_rotated; @@ -134,15 +157,15 @@ export default { return end < start ? 0 : end - start; }, - regionWidth(d): number { + regionWidth(d: RegionsType): number { return this.getRegionSize("width", d); }, - regionHeight(d): number { + regionHeight(d: RegionsType): number { return this.getRegionSize("height", d); }, - isRegionOnX(d): boolean { + isRegionOnX(d: RegionsType): boolean { return !d.axis || d.axis === "x"; }, }; diff --git a/src/config/Options/common/main.ts b/src/config/Options/common/main.ts index f9e731f29..76be572ac 100644 --- a/src/config/Options/common/main.ts +++ b/src/config/Options/common/main.ts @@ -2,6 +2,8 @@ * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ +import type {RegionsType} from "../../../../types/types"; + /** * main config options */ @@ -395,15 +397,23 @@ export default { * @memberof Options * @type {Array} * @default [] + * @see [Demo](https://naver.github.io/billboard.js/demo/#Region.RegionLabel) * @example * regions: [ * { * axis: "x", * start: 1, * end: 4, - * class: "region-1-4" + * class: "region-1-4", + * label: { + * text: "Region Text", + * x: 5, // position relative of the initial x coordinate + * y: 5, // position relative of the initial y coordinate + * color: "red", // color string + * rotated: true // make text to show in vertical or horizontal + * } * } * ] */ - regions: <{axis?: string; start?: number; end?: number; class?: string;}[]> [] + regions: [] }; diff --git a/src/config/Options/data/axis.ts b/src/config/Options/data/axis.ts index fc63ff39c..4e8425cc0 100644 --- a/src/config/Options/data/axis.ts +++ b/src/config/Options/data/axis.ts @@ -2,6 +2,8 @@ * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ +import type {DataRegionsType} from "../../../../types/types"; + /** * Axis based chart data config options */ @@ -128,7 +130,7 @@ export default { * } * } */ - data_regions: <{start?: number; end?: number; style?: {dasharray: string;}}[]> {}, + data_regions: {}, /** * Set the stacking to be normalized diff --git a/src/scss/billboard.scss b/src/scss/billboard.scss index 0706f2935..35c249866 100644 --- a/src/scss/billboard.scss +++ b/src/scss/billboard.scss @@ -136,8 +136,10 @@ } /*-- Region --*/ .bb-region { - fill: steelblue; - fill-opacity: .1; + rect { + fill: steelblue; + fill-opacity: .1; + } } /*-- Zoom region --*/ diff --git a/src/scss/theme/dark.scss b/src/scss/theme/dark.scss index 302fb7a5a..ba5283231 100644 --- a/src/scss/theme/dark.scss +++ b/src/scss/theme/dark.scss @@ -162,8 +162,10 @@ rect.bb-circle, use.bb-circle { /*-- Region --*/ .bb-region { - fill: steelblue; - fill-opacity: 0.5; + rect { + fill: steelblue; + fill-opacity: 0.5; + } &.selected rect { fill: rgb(39, 201, 3); diff --git a/src/scss/theme/datalab.scss b/src/scss/theme/datalab.scss index 9275e4767..7cee354e2 100644 --- a/src/scss/theme/datalab.scss +++ b/src/scss/theme/datalab.scss @@ -147,8 +147,10 @@ $text-font-size: 11px; /*-- Region --*/ .bb-region { - fill: steelblue; - fill-opacity: 0.1; + rect { + fill: steelblue; + fill-opacity: 0.1; + } &.selected rect { fill: rgb(39, 201, 3); diff --git a/src/scss/theme/graph.scss b/src/scss/theme/graph.scss index c8ac68fd9..541bd1665 100644 --- a/src/scss/theme/graph.scss +++ b/src/scss/theme/graph.scss @@ -158,8 +158,10 @@ rect.bb-circle, use.bb-circle { /*-- Region --*/ .bb-region { - fill: steelblue; - fill-opacity: 0.1; + rect { + fill: steelblue; + fill-opacity: 0.1; + } &.selected rect { fill: rgb(39, 201, 3); diff --git a/src/scss/theme/insight.scss b/src/scss/theme/insight.scss index 0e3c64764..ee0962cb3 100644 --- a/src/scss/theme/insight.scss +++ b/src/scss/theme/insight.scss @@ -154,8 +154,10 @@ rect.bb-circle, use.bb-circle { /*-- Region --*/ .bb-region { - fill: steelblue; - fill-opacity: 0.1; + rect { + fill: steelblue; + fill-opacity: 0.1; + } &.selected rect { fill: rgb(39, 201, 3); diff --git a/test/api/region-spec.ts b/test/api/region-spec.ts index 9fe73a1db..07a66192e 100644 --- a/test/api/region-spec.ts +++ b/test/api/region-spec.ts @@ -7,8 +7,9 @@ import {expect} from "chai"; import {select as d3Select} from "d3-selection"; import util from "../assets/util"; import {$REGION} from "../../src/config/classes"; +import {testRegions} from "../internals/rergions-spec"; -describe("API region", function() { +describe("API regions", function() { let chart; let args; @@ -370,4 +371,95 @@ describe("API region", function() { expect(chart.regions().length).to.be.equal(0); }); }); + + describe("label text", () => { + before(() => { + args = { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 150, 250], + ["data2", 100, 150, 130, 200, 220, 190], + ], + axes: { + data2: "y2", + }, + type: "line", + colors: { + data1: "#ff0000" + } + }, + axis: { + y2: { + show: true + } + }, + regions: [ + { + axis: "x", + start: 1, + end: 2, + class: "regions_class1", + label: { + text: "Regions 1", + x: 0, + y: 0, + color: "red" + } + }, + { + axis: "y", + start: 100, + end: 300, + class: "regions_class3", + label: { + text: "Regions 3" + } + }, + { + axis: "y2", + start: 200, + end: 220, + class: "regions_class4", + label: { + text: "Regions 4" + } + } + ] + } + }); + + it("labels are updated correctly?", done => { + // when + chart.regions([ + { + axis: "y", + start: 200, + end: 300, + label: { + text: "1 Regions", + x: 150, + color: "rgb(0, 0, 255)" + } + }, + { + axis: "x", + start: 2, + end: 4, + class: "fill_green", + label: { + text: "2 Region", + y: 50, + color: "rgb(165, 42, 42)", + rotated: true + } + } + ]); + + setTimeout(() => { + chart.internal.$el.region.list.each(testRegions(chart)); + + done(); + }, 500); + }); + }); }); diff --git a/test/internals/rergions-spec.ts b/test/internals/rergions-spec.ts new file mode 100644 index 000000000..1f1350147 --- /dev/null +++ b/test/internals/rergions-spec.ts @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2017 ~ present NAVER Corp. + * billboard.js project is licensed under the MIT license + */ +/* eslint-disable */ +import {expect} from "chai"; +import util from "../assets/util"; + +// exported to be used from /test/api/region-spec.ts +export function testRegions(ctx) { + const {scale} = ctx.internal; + + return function(d) { + const axis = scale[d.axis]; + const isX = d.axis === "x"; + const rect = this.querySelector("rect"); + const start = +rect.getAttribute(isX ? "x" : "y"); + const size = +rect.getAttribute(isX ? "width" : "height"); + + // check the diemsion + expect(start).to.be.equal(axis(isX ? d.start : d.end)); + expect(start + size).to.be.equal(axis(isX ? d.end : d.start)); + + d.class && expect(this.getAttribute("class").indexOf(d.class) > -1).to.be.true; + + if (d.label) { + const node = this.querySelector("text"); + const {text, x = 0, y = 0, color, rotated} = d.label; + + expect(node.textContent).to.be.equal(text); + + const transform = node.getAttribute("transform").replace(/(\d+)\.\d+/g, "$1"); + const start = Math.round(axis(d.start)); + const end = Math.round(axis(d.end)); + + if (isX) { + expect(transform).to.be + .equal(`translate(${start + x}, ${y})${rotated ? ` rotate(-90)` : ``}`); + } else { + expect(transform).to.be + .equal(`translate(${x}, ${end + y})${rotated ? ` rotate(-90)` : ``}`); + } + + color && expect(node.style.fill).to.be.equal(color); + + // when has no label, element should not be generated + } else { + expect(this.querySelector("text")).to.be.null; + } + }; +} + +describe("REGIONS", function() { + let chart; + let args; + + beforeEach(() => { + chart = util.generate(args); + }); + + describe("regions", () => { + before(() => { + args = { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 150, 250], + ["data2", 100, 150, 130, 200, 220, 190], + ], + axes: { + data2: "y2", + }, + type: "line", + colors: { + data1: "#ff0000" + } + }, + axis: { + y2: { + show: true + } + }, + regions: [ + { + axis: "x", + start: 1, + end: 2, + class: "regions_class1", + label: { + text: "Regions 1", + x: 0, + y: 0, + color: "red" + } + }, + { + axis: "x", + start: 3, + end: 5, + class: "regions_class2", + label: { + text: "Regions 2", + rotated: true + } + }, + { + axis: "y", + start: 100, + end: 300, + class: "regions_class3" + }, + { + axis: "y2", + start: 200, + end: 220, + class: "regions_class4", + label: { + text: "Regions 4", + x: 100, + y: 100 + } + } + ] + }; + }); + + it("regions are generated correctly?", done => { + const {$el, scale} = chart.internal; + + setTimeout(() => { + $el.region.list.each(function(d) { + const axis = scale[d.axis]; + const isX = d.axis === "x"; + const rect = this.querySelector("rect"); + const start = +rect.getAttribute(isX ? "x" : "y"); + const size = +rect.getAttribute(isX ? "width" : "height"); + + // check the diemsion + expect(start).to.be.equal(axis(isX ? d.start : d.end)); + expect(start + size).to.be.equal(axis(isX ? d.end : d.start)); + + d.class && expect(this.getAttribute("class").indexOf(d.class) > -1).to.be.true; + + + if (d.label) { + const node = this.querySelector("text"); + const {text, x = 0, y = 0, color, rotated} = d.label; + + expect(node.textContent).to.be.equal(text); + + if (isX) { + expect(node.getAttribute("transform")).to.be + .equal(`translate(${axis(d.start) + x}, ${y})${rotated ? ` rotate(-90)` : ``}`); + } else { + expect(node.getAttribute("transform")).to.be + .equal(`translate(${y}, ${axis(d.end) + x})${rotated ? ` rotate(-90)` : ``}`); + } + + color && expect(node.style.fill).to.be.equal(color); + + // when has no label, element should not be generated + } else { + expect(this.querySelector("text")).to.be.null; + } + }); + + done(); + }, 300); + }); + }); +}); diff --git a/types/types.d.ts b/types/types.d.ts index 994f698c1..05f7c36ec 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -54,7 +54,7 @@ export interface DataItem { export type DataArray = DataRow[]; -export interface RegionsType { +export interface DataRegionsType { [key: string]: { start?: number; end?: number; @@ -63,3 +63,17 @@ export interface RegionsType { } }; } + +export interface RegionsType { + axis?: "x" | "y" | "y2"; + start?: number; + end?: number; + class?: string; + label?: { + text: string; + x?: number; + y?: number; + color?: string; + rotated?: boolean; + }; +}