diff --git a/demo/demo.js b/demo/demo.js index 3b4496bfd..5d683059b 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -5528,6 +5528,57 @@ setTimeout(function() { ] } ], +DonutRangeText: [{ + options: { + title: { + text: "Range text in 'absolute' value" + }, + data: { + columns: [ + ["data1", 30], + ["data2", 120], + ["data3", 50] + ], + type: "donut" + }, + arc: { + rangeText: { + values: [15, 50, 70, 110, 160, 195], + unit: "absolute", + format: function(v) { + return v === 50 ? "Fifty" : v; + } + } + } + } + }, { + options: { + title: { + text: "Range text in 'percent' value" + }, + data: { + columns: [ + ["data1", 30], + ["data2", 120], + ["data3", 50] + ], + type: "donut" + }, + arc: { + rangeText: { + values: [15, 25, 40, 50, 63, 70, 80, 99], + unit: "%", + position: function(v) { + if (v === 25) { + return { + y: -30 + } + } + } + } + } + } + }], LabelRatio: { options: { data: { @@ -5896,6 +5947,95 @@ setTimeout(function() { } }, ], + GaugeRangeText: [{ + options: { + title: { + text: "Range text in 'absolute' value" + }, + size: { + height: 220 + }, + data: { + columns: [ + ["data1", 30], + ["data2", 120], + ["data3", 50] + ], + type: "gauge" + }, + arc: { + rangeText: { + values: [15, 50, 70, 110, 160, 195], + unit: "absolute" + } + }, + gauge: { + label: { + format: function(value, ratio) { return value; }, + extents: function() { return ""; } + } + }, + + } + }, { + options: { + title: { + text: "Range text in 'percent' value" + }, + size: { + height: 220 + }, + data: { + columns: [ + ["data1", 30], + ["data2", 120], + ["data3", 50] + ], + type: "gauge" + }, + arc: { + rangeText: { + values: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + unit: "%" + } + }, + gauge: { + label: { + extents: function() { return ""; } + } + } + } + }, + { + options: { + title: { + text: "Fixed range text in 'percent' value" + }, + size: { + height: 220 + }, + data: { + columns: [ + ["data1", 30], + ["data2", 120], + ["data3", 50] + ], + type: "gauge" + }, + arc: { + rangeText: { + values: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + unit: "%", + fixed: true + } + }, + gauge: { + label: { + extents: function() { return ""; } + } + } + } + }], GaugeStackData: [ { options: { diff --git a/demo/simple-sidebar.css b/demo/simple-sidebar.css index 55eb3b8e0..416176593 100644 --- a/demo/simple-sidebar.css +++ b/demo/simple-sidebar.css @@ -329,6 +329,44 @@ div.row { text-decoration: underline; } +#exportPreserveFontStyle svg { + font-family: 'Alfa Slab One'; +} + +#fitPadding_1 svg { + border: 1px dashed blue; + margin-bottom: 20px; +} + +#fitPadding_2 svg { + border: 1px dashed red; +} + +#donutNeedle_1 .bb-chart-arcs-title, #donutNeedle_2 .bb-chart-arcs-title { + font-size: 3em; + fill: blue; + transform: translateY(40px); +} + +.dark #donutNeedle_1 .bb-chart-arcs-title, .dark #donutNeedle_2 .bb-chart-arcs-title { + fill: #828181; +} + +#gaugeNeedle_1 .bb-chart-arcs-gauge-title tspan:first-child { + font-size: 0.3em; +} + +#gaugeNeedle_1 .bb-chart-arcs-gauge-title { + transform: translateY(-80px); + font-size: 4em; +} + +#gaugeNeedle_2 .bb-chart-arcs-gauge-title { + font-size: 3.5em; + fill: red; + transform: translateY(-20px); +} + /* Style For Region */ #styleForRegion .bb-region-0 {fill:red;} #styleForRegion .bb-region.foo {fill:green;} @@ -384,3 +422,8 @@ div.row { /* Background */ .myBgClass { transform: scale(0.9) translate(15px, -10px); opacity: 0.1; } + +div[id$="_2"], div[id$="_3"], div[id$="_4"], div[id$="_5"] { + margin: 20px 0; + box-shadow: 0 -1px 0 #ccc; +} diff --git a/demo/tomorrow.css b/demo/tomorrow.css index 748fede1b..2e017df35 100644 --- a/demo/tomorrow.css +++ b/demo/tomorrow.css @@ -34,6 +34,12 @@ color: #8959a8; } +g > text.bb-title { + font-size: 1.3em; + stroke-width: 2px; + font-weight: bold; +} + @media (min-width: 1200px) { .example-grid { display: grid; @@ -80,6 +86,7 @@ pre code { line-height: 1.3; border: 1px solid #ccc; padding: 10px; + overscroll-behavior: contain; } code.html { @@ -123,44 +130,6 @@ code.html { src: url(https://fonts.gstatic.com/s/alfaslabone/v17/6NUQ8FmMKwSEKjnm5-4v-4Jh2dJhe_escmA.woff2) format('woff2'); } -#exportPreserveFontStyle svg { - font-family: 'Alfa Slab One'; -} - -#fitPadding_1 svg { - border: 1px dashed blue; - margin-bottom: 20px; -} - -#fitPadding_2 svg { - border: 1px dashed red; -} - -#donutNeedle_1 .bb-chart-arcs-title, #donutNeedle_2 .bb-chart-arcs-title { - font-size: 3em; - fill: blue; - transform: translateY(40px); -} - -.dark #donutNeedle_1 .bb-chart-arcs-title, .dark #donutNeedle_2 .bb-chart-arcs-title { - fill: #828181; -} - -#gaugeNeedle_1 .bb-chart-arcs-gauge-title tspan:first-child { - font-size: 0.3em; -} - -#gaugeNeedle_1 .bb-chart-arcs-gauge-title { - transform: translateY(-80px); - font-size: 4em; -} - - #gaugeNeedle_2 .bb-chart-arcs-gauge-title { - font-size: 3.5em; - fill: red; - transform: translateY(-20px); -} - body.dark { background-color: #000; color: #d4d4d4; diff --git a/src/ChartInternal/data/data.ts b/src/ChartInternal/data/data.ts index 3d832a8d1..c2b5ac082 100644 --- a/src/ChartInternal/data/data.ts +++ b/src/ChartInternal/data/data.ts @@ -626,10 +626,13 @@ export default { if (orderAsc || orderDesc) { const reducer = (p, c) => p + Math.abs(c.value); + const sum = v => (isNumber(v) ? v : ( + "values" in v ? v.values.reduce(reducer, 0) : v.value) + ); fn = (t1: IData | IDataRow, t2: IData | IDataRow) => { - const t1Sum = "values" in t1 ? t1.values.reduce(reducer, 0) : t1.value; - const t2Sum = "values" in t2 ? t2.values.reduce(reducer, 0) : t2.value; + const t1Sum = sum(t1); + const t2Sum = sum(t2); return isReversed ? (orderAsc ? t1Sum - t2Sum : t2Sum - t1Sum) : @@ -965,7 +968,7 @@ export default { // otherwise, based on the rendered angle value } else { const gaugeArcLength = config.gauge_fullCircle ? - $$.getArcLength() : $$.getGaugeStartAngle() * -2; + $$.getArcLength() : $$.getStartingAngle() * -2; const arcLength = $$.hasType("gauge") ? gaugeArcLength : Math.PI * 2; ratio = (d.endAngle - d.startAngle) / arcLength; diff --git a/src/ChartInternal/internals/size.ts b/src/ChartInternal/internals/size.ts index 59707c8e9..78fd47849 100644 --- a/src/ChartInternal/internals/size.ts +++ b/src/ChartInternal/internals/size.ts @@ -395,6 +395,17 @@ export default { state.arcWidth = state.width - (isLegendRight ? currLegend.width + 10 : 0) - textWidth; state.arcHeight = state.height - (isLegendRight && !hasGauge ? 0 : 10); + if (config.arc_rangeText_values?.length) { + if (hasGauge) { + state.arcWidth -= 25; + state.arcHeight -= 10; + state.margin.left += 10; + } else { + state.arcHeight -= 20; + state.margin.top += 10; + } + } + if (hasGauge && !config.gauge_fullCircle) { state.arcHeight += state.height - $$.getPaddingBottomForGauge(); } diff --git a/src/ChartInternal/internals/transform.ts b/src/ChartInternal/internals/transform.ts index 5df0a3859..666f52f4a 100644 --- a/src/ChartInternal/internals/transform.ts +++ b/src/ChartInternal/internals/transform.ts @@ -44,6 +44,10 @@ export default { } else if (target === "arc") { x = state.arcWidth / 2; y = state.arcHeight / 2; + + if (config.arc_rangeText_values?.length) { + y += 5 + ($$.hasType("gauge") && config.title_text ? 10 : 0); + } } else if (target === "polar") { x = state.arcWidth / 2; y = state.arcHeight / 2; diff --git a/src/ChartInternal/shape/arc.ts b/src/ChartInternal/shape/arc.ts index c29161149..3a528f0af 100644 --- a/src/ChartInternal/shape/arc.ts +++ b/src/ChartInternal/shape/arc.ts @@ -152,7 +152,7 @@ export default { .startAngle(startingAngle) .endAngle(startingAngle + (2 * Math.PI)) .padAngle(padAngle) - .value((d: IData | any) => d.values.reduce((a, b) => a + b.value, 0)) + .value((d: IData | any) => d.values?.reduce((a, b) => a + b.value, 0) ?? d) .sort($$.getSortCompareFn.bind($$)(true)); }, @@ -236,13 +236,14 @@ export default { return len * Math.PI; }, - getGaugeStartAngle(): number { + getStartingAngle(): number { const $$ = this; const {config} = $$; - const isFullCircle = config.gauge_fullCircle; + const dataType = config.data_type; + const isFullCircle = $$.hasType("gauge") ? config.gauge_fullCircle : false; const defaultStartAngle = -1 * Math.PI / 2; const defaultEndAngle = Math.PI / 2; - let startAngle = config.gauge_startingAngle; + let startAngle = config[`${dataType}_startingAngle`] || 0; if (!isFullCircle && startAngle <= defaultStartAngle) { startAngle = defaultStartAngle; @@ -255,9 +256,20 @@ export default { return startAngle; }, - updateAngle(dValue) { + /** + * Update angle data + * @param {object} dValue Data object + * @param {boolean} forRange Weather is for ranged text option(arc.rangeText.values) + * @returns {object|null} Updated angle data + * @private + */ + updateAngle(dValue: IArcData, forRange = false) { const $$ = this; const {config, state} = $$; + const hasGauge = forRange && $$.hasType("gauge"); + + // to prevent excluding total data sum during the init(when data.hide option is used), use $$.rendered state value + // const totalSum = $$.getTotalDataSum(state.rendered); let {pie} = $$; let d = dValue; let found = false; @@ -266,30 +278,34 @@ export default { return null; } - const gStart = $$.getGaugeStartAngle(); - const radius = config.gauge_fullCircle ? $$.getArcLength() : gStart * -2; + const gStart = $$.getStartingAngle(); + const radius = config.gauge_fullCircle || (forRange && !hasGauge) ? + $$.getArcLength() : gStart * -2; if (d.data && $$.isGaugeType(d.data) && !$$.hasMultiArcGauge()) { - const {gauge_min: min, gauge_max: max} = config; + const {gauge_min: gMin, gauge_max: gMax} = config; // to prevent excluding total data sum during the init(when data.hide option is used), use $$.rendered state value const totalSum = $$.getTotalDataSum(state.rendered); + // https://github.com/naver/billboard.js/issues/2123 - const gEnd = radius * ((totalSum - min) / (max - min)); + const gEnd = radius * ((totalSum - gMin) / (gMax - gMin)); pie = pie .startAngle(gStart) .endAngle(gEnd + gStart); } - pie($$.filterTargetsToShow()) - .forEach((t, i) => { - if (!found && t.data.id === d.data?.id) { - found = true; - d = t; - d.index = i; - } - }); + if (forRange === false) { + pie($$.filterTargetsToShow()) + .forEach((t, i) => { + if (!found && t.data.id === d.data?.id) { + found = true; + d = t; + d.index = i; + } + }); + } if (isNaN(d.startAngle)) { d.startAngle = 0; @@ -299,17 +315,18 @@ export default { d.endAngle = d.startAngle; } - if (d.data && (config.gauge_enforceMinMax || $$.hasMultiArcGauge())) { - const gMin = config.gauge_min; - const gMax = config.gauge_max; - const gTic = radius / (gMax - gMin); - const gValue = d.value < gMin ? 0 : d.value < gMax ? d.value - gMin : (gMax - gMin); + if (forRange || (d.data && (config.gauge_enforceMinMax || $$.hasMultiArcGauge()))) { + const {gauge_min: gMin, gauge_max: gMax} = config; + const max = forRange && !hasGauge ? $$.getTotalDataSum(state.rendered) : gMax; + const gTic = radius / (max - gMin); + const value = d.value ?? 0; + const gValue = value < gMin ? 0 : value < max ? value - gMin : (max - gMin); d.startAngle = gStart; d.endAngle = gStart + gTic * gValue; } - return found ? d : null; + return found || forRange ? d : null; }, getSvgArc(): Function { @@ -373,25 +390,83 @@ export default { return force || this.isArcType(d.data) ? this.svgArc(d, withoutUpdate) : "M 0 0"; }, + /** + * Render range value text + * @private + */ + redrawArcRangeText(): void { + const $$ = this; + const {config, $el: {arcs}, state, $T} = $$; + const format = config.arc_rangeText_format; + const fixed = $$.hasType("gauge") && config.arc_rangeText_fixed; + let values = config.arc_rangeText_values; + + if (values?.length) { + const isPercent = config.arc_rangeText_unit === "%"; + const totalSum = $$.getTotalDataSum(state.rendered); + + if (isPercent) { + values = values.map(v => totalSum / 100 * v); + } + + const pieData = $$.pie(values).map((d, i) => ((d.index = i), d)); + let rangeText = arcs.selectAll(`.${$ARC.arcRange}`) + .data(values); + + rangeText.exit(); + + rangeText = $T(rangeText.enter() + .append("text") + .attr("class", $ARC.arcRange) + .style("text-anchor", "middle") + .style("pointer-events", "none") + .style("opacity", "0") + .text(v => { + const range = isPercent ? (v / totalSum * 100) : v; + + return isFunction(format) ? format(range) : ( + `${range}${isPercent ? "%" : ""}` + ); + }) + .merge(rangeText) + ); + + if ((!state.rendered || (state.rendered && !fixed)) && totalSum > 0) { + rangeText.attr("transform", (d, i) => $$.transformForArcLabel(pieData[i], true)); + } + + rangeText.style("opacity", d => (!fixed && (d > totalSum || totalSum === 0) ? "0" : null)); + } + }, + /** * Set transform attributes to arc label text * @param {object} d Data object + * @param {boolean} forRange Weather is for ranged text option(arc.rangeText.values) * @returns {string} Translate attribute string * @private */ - transformForArcLabel(d: IArcData): string { + transformForArcLabel(d: IArcData, forRange = false): string { const $$ = this; const {config, state: {radiusExpanded}} = $$; - - const updated = $$.updateAngle(d); + const updated = $$.updateAngle(d, forRange); let translate = ""; if (updated) { - if ($$.hasMultiArcGauge()) { + if (forRange || $$.hasMultiArcGauge()) { const y1 = Math.sin(updated.endAngle - Math.PI / 2); + const rangeTextPosition = config.arc_rangeText_position; + let x = Math.cos(updated.endAngle - Math.PI / 2) * (radiusExpanded + (forRange ? 5 : 25)); + let y = y1 * (radiusExpanded + 15 - Math.abs(y1 * 10)) + 3; - const x = Math.cos(updated.endAngle - Math.PI / 2) * (radiusExpanded + 25); - const y = y1 * (radiusExpanded + 15 - Math.abs(y1 * 10)) + 3; + if (forRange && rangeTextPosition) { + const rangeValues = config.arc_rangeText_values; + const pos = isFunction(rangeTextPosition) ? + rangeTextPosition(rangeValues[d.index]) : rangeTextPosition; + + x += pos?.x ?? 0; + y += pos?.y ?? 0; + } translate = `translate(${x},${y})`; } else if (!$$.hasType("gauge") || $$.data.targets.length > 1) { @@ -722,7 +797,7 @@ export default { if ($$.hasType("gauge")) { $$.updateGaugeMax(); - $$.hasMultiArcGauge() && $$.redrawMultiArcGauge(); + $$.hasMultiArcGauge() && $$.redrawArcGaugeLine(); } mainArc @@ -799,6 +874,7 @@ export default { config.arc_needle_show && $$.redrawNeedle(); $$.redrawArcText(duration); + $$.redrawArcRangeText(); }, /** @@ -894,7 +970,7 @@ export default { state.current.needle = value; if (hasGauge) { - startingAngle = $$.getGaugeStartAngle(); + startingAngle = $$.getStartingAngle(); const radius = config.gauge_fullCircle ? arcLength : startingAngle * -2; const {gauge_min: min, gauge_max: max} = config; @@ -915,7 +991,7 @@ export default { const showEmptyTextLabel = $$.filterTargetsToShow($$.data.targets).length === 0 && !!config.data_empty_label_text; - const startAngle = $$.getGaugeStartAngle(); + const startAngle = $$.getStartingAngle(); const endAngle = isFullCircle ? startAngle + $$.getArcLength() : startAngle * -1; let backgroundArc = $$.$el.arcs.select( @@ -1083,7 +1159,7 @@ export default { .style("opacity", "0") .attr("class", d => ($$.isGaugeType(d.data) ? $GAUGE.gaugeValue : null)) .call($$.textForArcLabel.bind($$)) - .attr("transform", $$.transformForArcLabel.bind($$)) + .attr("transform", d => $$.transformForArcLabel.bind($$)(d)) .style("font-size", d => ( $$.isGaugeType(d.data) && $$.data.targets.length === 1 && !hasMultiArcGauge ? `${Math.round(state.radius / 5)}px` : null diff --git a/src/ChartInternal/shape/gauge.ts b/src/ChartInternal/shape/gauge.ts index 17883560b..3cdb345bf 100644 --- a/src/ChartInternal/shape/gauge.ts +++ b/src/ChartInternal/shape/gauge.ts @@ -10,11 +10,12 @@ export default { initGauge(): void { const $$ = this; const {config, $el: {arcs}} = $$; - const appendText = className => { + const appendText = (className = null, value = "") => { arcs.append("text") .attr("class", className) .style("text-anchor", "middle") - .style("pointer-events", "none"); + .style("pointer-events", "none") + .text(value); }; if ($$.hasType("gauge")) { @@ -26,6 +27,7 @@ export default { config.gauge_units && appendText($GAUGE.chartArcsGaugeUnit); + // append min/max value text if (config.gauge_label_show) { appendText($GAUGE.chartArcsGaugeMin); !config.gauge_fullCircle && appendText($GAUGE.chartArcsGaugeMax); @@ -50,7 +52,7 @@ export default { } }, - redrawMultiArcGauge(): void { + redrawArcGaugeLine(): void { const $$ = this; const {config, state, $el} = $$; const {hiddenTargetIds} = $$.state; diff --git a/src/config/Options/shape/arc.ts b/src/config/Options/shape/arc.ts index ad6c3fee7..0b7e15e1c 100644 --- a/src/config/Options/shape/arc.ts +++ b/src/config/Options/shape/arc.ts @@ -36,11 +36,23 @@ export default { * @property {number} [arc.needle.bottom.ry=1] Set needle bottom [ry radius value](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#elliptical_arc_curve). * @property {number} [arc.needle.bottom.width=15] Set needle bottom width in pixel. * @property {number} [arc.needle.bottom.len=0] Set needle bottom length in pixel. Setting this value, will make bottom larger starting from center. + * @property {object} [arc.rangeText] Set rangeText options. + * @property {Array} [arc.rangeText.values] Set range text values to be shown around Arc. + * - When `unit: 'absolute'`: Given values are treated as absolute values. + * - When `unit: '%'`: Given values are treated as percentages. + * @property {string} [arc.rangeText.unit="absolute"] Specify the range text unit. + * - "absolute": Show absolute value + * - "%": Show percentage value + * @property {boolean} [arc.rangeText.fiexed=false] Set if range text shown will be fixed w/o data toggle update. Only available for gauge chart. + * @property {Function} [arc.rangeText.format] Set format function for the range text. + * @property {number} [arc.rangeText.position] Set position function or object for the range text. * @see [Demo: Donut corner radius](https://naver.github.io/billboard.js/demo/#DonutChartOptions.DonutCornerRadius) - * @see [Demo: Gauge corner radius](https://naver.github.io/billboard.js/demo/#GaugeChartOptions.GaugeCornerRadius) * @see [Demo: Donut corner radius](https://naver.github.io/billboard.js/demo/#PieChartOptions.CornerRadius) * @see [Demo: Donut needle](https://naver.github.io/billboard.js/demo/#DonutChartOptions.DonutNeedle) - * @see [Demo: Gauge needle](https://naver.github.io/billboard.js/demo/##GaugeChartOptions.GaugeNeedle) + * @see [Demo: Donut RangeText](https://naver.github.io/billboard.js/demo/#DonutChartOptions.DonutRangeText) + * @see [Demo: Gauge corner radius](https://naver.github.io/billboard.js/demo/#GaugeChartOptions.GaugeCornerRadius) + * @see [Demo: Gauge needle](https://naver.github.io/billboard.js/demo/#GaugeChartOptions.GaugeNeedle) + * @see [Demo: Gauge RangeText](https://naver.github.io/billboard.js/demo/#GaugeChartOptions.GaugeRangeText) * @example * arc: { * cornerRadius: 12, @@ -97,6 +109,21 @@ export default { * width: 10 * len: 10 * } + * }, + * + * rangeText: { + * values: [15, 30, 50, 75, 95], + * unit: "%", + * fixed: false, // only available for gauge chart + * format: function(v) { + * return v === 15 ? "Fifteen" : v; + * }, + * + * position: function(v) { + * return v === 15 ? {x: 20, y: 10} : null; // can return one props value also. + * }, + * position: {x: 10, y: 15}, + * position: {x: 10} * } * } */ @@ -115,5 +142,15 @@ export default { arc_needle_bottom_rx: 1, arc_needle_bottom_ry: 1, arc_needle_bottom_width: 15, - arc_needle_bottom_len: 0 + arc_needle_bottom_len: 0, + arc_rangeText_values: undefined, + arc_rangeText_unit: <"absolute"|"%"> "absolute", + arc_rangeText_fixed: false, + arc_rangeText_format: < + ((v: number) => number)|undefined + > undefined, + arc_rangeText_position: < + ((v: number) => {x?: number; y?: number})| + {x?: number, y?: number}|undefined + > undefined }; diff --git a/src/config/classes.ts b/src/config/classes.ts index d702453af..ad364e0b1 100644 --- a/src/config/classes.ts +++ b/src/config/classes.ts @@ -18,6 +18,7 @@ export const $COMMON = { export const $ARC = { arc: "bb-arc", arcLabelLine: "bb-arc-label-line", + arcRange: "bb-arc-range", arcs: "bb-arcs", chartArc: "bb-chart-arc", chartArcs: "bb-chart-arcs", diff --git a/test/shape/arc-rangeText-spec.ts b/test/shape/arc-rangeText-spec.ts new file mode 100644 index 000000000..4837505a1 --- /dev/null +++ b/test/shape/arc-rangeText-spec.ts @@ -0,0 +1,326 @@ +/** + * Copyright (c) 2017 ~ present NAVER Corp. + * billboard.js project is licensed under the MIT license + */ +/* eslint-disable */ +// @ts-nocheck +/* global describe, beforeEach, it, expect */ +import {expect} from "chai"; +import {$ARC} from "../../src/config/classes"; +import util from "../assets/util"; + +describe("SHAPE ARC: rangeText option", () => { + let chart; + let args = { + data: { + columns: [ + ["data1", 30], + ["data2", 120], + ["data3", 50] + ], + type: "donut", + }, + arc: { + rangeText: { + values: [ + 15, + 50, + 70, + 110, + 160, + 195 + ], + unit: "absolute", + fixed: false, + format: v => v === 50 ? "Fifty" : v + }, + position: () => {} + } + }; + + beforeEach(() => { + chart = util.generate(args); + }); + + it("basic functionality: donut", () => { + const expected = [ + [98, -192], + [218, 3], + [176, 133], + [-67, 210], + [-207, -66], + [-34, -212] + ]; + const rangeText = chart.$.main.selectAll(`.${$ARC.arcRange}`); + + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").split(",").map(util.parseNum); + const [nx, ny] = expected[i]; + + expect(x).to.be.closeTo(nx, 1); + expect(y).to.be.closeTo(ny, 1); + }); + + // check for format function + expect(rangeText.filter(d => d === 50).text()).to.be.equal(args.arc.rangeText.format(50)); + + new Promise(resolve => { + chart.hide("data2"); + + setTimeout(resolve, 300); + }).then(() => { + rangeText.filter(d => d > 70).each(function() { + expect(this.style.opacity).to.be.equal("0"); + }); + + return new Promise(resolve => { + chart.show("data2"); + + setTimeout(resolve, 300); + }) + }).then(() => { + rangeText.filter(d => d > 70).each(function() { + expect(this.style.opacity).to.be.empty; + }); + + done(); + }); + }); + + it("set options: data.type='pie' / rangeText.unit='%'", () => { + args.arc.rangeText.values = [10, 25, 50, 75, 99]; + args.arc.rangeText.unit = "%"; + args.data.type = "pie"; + }); + + it("basic functionality: pie", done => { + const expected = [ + [128, -174], + [218, 3], + [-8, 221], + [-218, 3], + [-13, -214] + ]; + const rangeText = chart.$.main.selectAll(`.${$ARC.arcRange}`); + + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + expect(x).to.be.closeTo(nx, 1); + expect(y).to.be.closeTo(ny, 1); + }); + + new Promise(resolve => { + chart.hide("data2"); + + setTimeout(resolve, 300); + }).then(() => { + rangeText.each(function() { + expect(this.style.opacity).to.be.empty; + }); + + return new Promise(resolve => { + chart.show("data2"); + + setTimeout(resolve, 300); + }) + }).then(() => { + rangeText.each(function() { + expect(this.style.opacity).to.be.empty; + }); + + done(); + }); + }); + + it("set options: data.type='gauge'", () => { + args.data.type = "gauge"; + }); + + it("basic functionality: gauge", done => { + const expected = [ + [-297, -95], + [-220, -220], + [8, -309], + [220, -220], + [312, -7] + ]; + const rangeText = chart.$.main.selectAll(`.${$ARC.arcRange}`); + + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + expect(x).to.be.closeTo(nx, 1); + expect(y).to.be.closeTo(ny, 1); + }); + + new Promise(resolve => { + chart.hide("data2"); + + setTimeout(resolve, 300); + }).then(() => { + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + expect(x < nx).to.be.true; + expect(i <= 2 ? y > ny : y < ny).to.be.true; + }); + + return new Promise(resolve => { + chart.show("data2"); + + setTimeout(resolve, 300); + }); + }).then(() => { + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + expect(x).to.be.closeTo(nx, 1); + expect(y).to.be.closeTo(ny, 1); + }); + + done(); + }); + }); + + it("set options: arc.rangeText.fixed=true", () => { + args.arc.rangeText.fixed = true; + }); + + it("should rangeText position fixed on gauge type.", done => { + const expected = [ + [-297, -95], + [-220, -220], + [8, -309], + [220, -220], + [312, -7] + ]; + const rangeText = chart.$.main.selectAll(`.${$ARC.arcRange}`); + + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + expect(x).to.be.closeTo(nx, 1); + expect(y).to.be.closeTo(ny, 1); + }); + + new Promise(resolve => { + chart.hide("data2"); + + setTimeout(resolve, 300); + }).then(() => { + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + expect(x).to.be.closeTo(nx, 1); + expect(y).to.be.closeTo(ny, 1); + }); + + return new Promise(resolve => { + chart.show("data2"); + + setTimeout(resolve, 300); + }); + }).then(() => { + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + expect(x).to.be.closeTo(nx, 1); + expect(y).to.be.closeTo(ny, 1); + }); + + done(); + }); + }); + + it("set options: arc.rangeText.position #1", () => { + args.arc.rangeText.position = { + x: 5, + y: 5 + }; + }); + + it("position option worked?", () => { + const expected = [ + [-297, -95], + [-220, -220], + [8, -309], + [220, -220], + [312, -7] + ]; + const rangeText = chart.$.main.selectAll(`.${$ARC.arcRange}`); + const {x: x1, y: y1} = args.arc.rangeText.position + + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + expect(x).to.be.closeTo(nx, x1); + expect(y).to.be.closeTo(ny, y1); + }); + }); + + it("set options: arc.rangeText.position #2", () => { + args = { + data: { + columns: [ + ["data1", 30], + ["data2", 120], + ["data3", 50] + ], + type: "gauge" + }, + arc: { + rangeText: { + values: [10, 25, 50, 75, 99], + unit: "%", + position: function(v) { + if (v === 50) { + return {x: 30}; + } + } + } + } + } + }); + + it("position function worked?", () => { + const expected = [ + [-297, -95], + [-220, -220], + [8, -309], + [220, -220], + [312, -7] + ]; + const rangeText = chart.$.main.selectAll(`.${$ARC.arcRange}`); + + rangeText.each(function(d, i) { + const [x, y] = this.getAttribute("transform").replace(/(translate\(|\))/g, "") + .split(",").map(v => parseInt(v, 10)); + const [nx, ny] = expected[i]; + + if (this.textContent === "50%") { + expect(x).to.be.equal(30); + expect(y).to.be.closeTo(ny, 1); + } else { + expect(x).to.be.closeTo(nx, 1); + expect(y).to.be.closeTo(ny, 1); + } + }); + }); +}); diff --git a/test/shape/arc-spec.ts b/test/shape/arc-spec.ts index 3fe233f6e..8377d9c10 100644 --- a/test/shape/arc-spec.ts +++ b/test/shape/arc-spec.ts @@ -19,9 +19,9 @@ describe("SHAPE ARC", () => { shape: `path.${$SHAPE.shape}.${$ARC.arc}.${$ARC.arc}` }; - after(() => { - util.destroyAll(); - }); + // after(() => { + // util.destroyAll(); + // }); describe("show pie chart", () => { let instChart; diff --git a/test/shape/gauge-spec.ts b/test/shape/gauge-spec.ts index b81d1efcf..f7f93f656 100644 --- a/test/shape/gauge-spec.ts +++ b/test/shape/gauge-spec.ts @@ -492,7 +492,6 @@ describe("SHAPE GAUGE", () => { ]; setTimeout(() => { - chart.$.arc.selectAll(".bb-shapes").each(function(d, i) { expect(parseInt(this.nextSibling.textContent)).to.be.equal(expected[i].value); expect(this.querySelector("path").getTotalLength() < expected[i].length).to.be diff --git a/types/options.shape.d.ts b/types/options.shape.d.ts index 63fa5ba61..2d904ce7f 100644 --- a/types/options.shape.d.ts +++ b/types/options.shape.d.ts @@ -86,6 +86,38 @@ export interface ArcOptions { len?: number; } }; + + /** + * Set range text options. + */ + rangeText?: { + /** + * Set range text values to be shown around Arc. + * - When `unit: 'absolute'`: Given values are treated as absolute values. + * - When `unit: '%'`: Given values are treated as percentages. + */ + values?: number[]; + + /** + * Specify the range text unit. + */ + unit?: "absolute" | "%"; + + /** + * Set if range text shown will be fixed w/o data toggle update. Only available for gauge chart. + */ + fixed?: boolean; + + /** + * Set format function for the range text. + */ + format?: (v: number) => number; + + /** + * Set position function or object for the range text. + */ + position?: ((v: number) => {x?: number; y?: number})|{x?: number, y?: number}; + } } export interface AreaOptions {