diff --git a/demo/demo.js b/demo/demo.js index ff513b766..2b5bac34d 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -3946,6 +3946,71 @@ d3.select(".chart_area") } } ], + RadialGradientPoint: [ + { + options: { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 100, 250], + ["data2", 130, 100, 130, 200, 150, 50] + ], + type: "scatter" + }, + point: { + r: 20, + radialGradient: true, + opacity: 1, + sensitivity: "radius" + }, + axis: { + x: { + type: "category" + } + } + } + }, + { + options: { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 100, 250], + ["data2", 130, 100, 130, 200, 150, 50] + ], + type: "bubble" + }, + point: { + r: 10, + radialGradient: { + cx: 0.5, + cy: 0.5, + r: 0.5, + stops: [ + [0.3, "#fff", 0.8], + [0.6, "green", 0.35], + [1, null, 1] + ] + }, + opacity: 1, + sensitivity: "radius" + } + } + }, + { + options: { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 100, 250], + ["data2", 130, 100, 130, 200, 150, 50] + ], + type: "line" + }, + point: { + r: 7, + radialGradient: true, + } + } + } + ], RectanglePoints: { options: { data: { diff --git a/src/ChartInternal/internals/color.ts b/src/ChartInternal/internals/color.ts index eea1a60b9..554128cfa 100644 --- a/src/ChartInternal/internals/color.ts +++ b/src/ChartInternal/internals/color.ts @@ -204,7 +204,9 @@ export default { }, /** - * Update linear gradient definition (for area & bar only) + * Update linear/radial gradient definition + * - linear: area & bar only + * - radial: type which has data points only * @private */ updateLinearGradient(): void { @@ -213,31 +215,55 @@ export default { targets.forEach(d => { const id = `${datetimeId}-gradient${$$.getTargetSelectorSuffix(d.id)}`; + const radialGradient = $$.hasPointType() && config.point_radialGradient; const supportedType = ($$.isAreaType(d) && "area") || ($$.isBarType(d) && "bar"); - const isRotated = config.axis_rotated; - if (supportedType && defs.select(`#${id}`).empty()) { + if ((radialGradient || supportedType) && defs.select(`#${id}`).empty()) { const color = $$.color(d); - const { - x = isRotated ? [1, 0] : [0, 0], - y = isRotated ? [0, 0] : [0, 1], - stops = [[0, color, 1], [1, color, 0]] - } = config[`${supportedType}_linearGradient`]; - - const linearGradient = defs.append("linearGradient") - .attr("id", `${id}`) - .attr("x1", x[0]) - .attr("x2", x[1]) - .attr("y1", y[0]) - .attr("y2", y[1]); - - stops.forEach(v => { - const stopColor = isFunction(v[1]) ? v[1].bind($$.api)(d.id) : v[1]; - - linearGradient.append("stop") - .attr("offset", v[0]) - .attr("stop-color", stopColor || color) - .attr("stop-opacity", v[2]); + const gradient = { + defs: null, + stops: <[number, string|Function|null, number][]>[] + }; + + if (radialGradient) { + const { + cx = 0.3, + cy = 0.3, + r = 0.7, + stops = [[0.1, color, 0], [0.9, color, 1]] + } = radialGradient; + + gradient.stops = stops; + gradient.defs = defs.append("radialGradient") + .attr("id", `${id}`) + .attr("cx", cx) + .attr("cy", cy) + .attr("r", r); + } else { + const isRotated = config.axis_rotated; + const { + x = isRotated ? [1, 0] : [0, 0], + y = isRotated ? [0, 0] : [0, 1], + stops = [[0, color, 1], [1, color, 0]] + } = config[`${supportedType}_linearGradient`]; + + gradient.stops = stops; + gradient.defs = defs.append("linearGradient") + .attr("id", `${id}`) + .attr("x1", x[0]) + .attr("x2", x[1]) + .attr("y1", y[0]) + .attr("y2", y[1]); + } + + gradient.stops.forEach((v: [number, string|Function|null, number]) => { + const [offset, stopColor, stopOpacity] = v; + const colorValue = isFunction(stopColor) ? stopColor.bind($$.api)(d.id) : stopColor; + + gradient.defs && gradient.defs.append("stop") + .attr("offset", offset) + .attr("stop-color", colorValue || color) + .attr("stop-opacity", stopOpacity); }); } }); diff --git a/src/ChartInternal/shape/point.ts b/src/ChartInternal/shape/point.ts index 189ce1b0c..0aabbd74b 100644 --- a/src/ChartInternal/shape/point.ts +++ b/src/ChartInternal/shape/point.ts @@ -130,6 +130,8 @@ export default { const $root = isSub ? $el.subchart : $el; if (config.point_show && !state.toggling) { + config.point_radialGradient && $$.updateLinearGradient(); + const circles = $root.main.selectAll(`.${$CIRCLE.circles}`) .selectAll(`.${$CIRCLE.circle}`) .data(d => ( @@ -142,7 +144,7 @@ export default { circles.enter() .filter(Boolean) - .append($$.point("create", this, $$.pointR.bind($$), $$.getStylePropValue($$.color))); + .append($$.point("create", this, $$.pointR.bind($$), $$.updateCircleColor.bind($$))); $root.circle = $root.main.selectAll(`.${$CIRCLE.circles} .${$CIRCLE.circle}`) .style("stroke", $$.getStylePropValue($$.color)) @@ -150,6 +152,20 @@ export default { } }, + /** + * Update circle color + * @param {object} d Data object + * @returns {string} Color string + * @private + */ + updateCircleColor(d: IDataRow): string { + const $$ = this; + const fn = $$.getStylePropValue($$.color); + + return $$.config.point_radialGradient ? + $$.getGradienColortUrl(d.id) : (fn ? fn(d) : null); + }, + redrawCircle(cx: Function, cy: Function, withTransition: boolean, flow, isSub = false) { const $$ = this; const {state: {rendered}, $el, $T} = $$; @@ -160,7 +176,7 @@ export default { return []; } - const fn = $$.point("update", $$, cx, cy, $$.getStylePropValue($$.color), withTransition, flow, selectedCircles); + const fn = $$.point("update", $$, cx, cy, $$.updateCircleColor.bind($$), withTransition, flow, selectedCircles); const posAttr = $$.isCirclePoint() ? "c" : ""; const t = getRandom(); diff --git a/src/config/Options/common/point.ts b/src/config/Options/common/point.ts index e70212356..66b2a8f8d 100644 --- a/src/config/Options/common/point.ts +++ b/src/config/Options/common/point.ts @@ -17,6 +17,13 @@ export default { * @property {boolean} [point.show=true] Whether to show each point in line. * @property {number|Function} [point.r=2.5] The radius size of each point. * - **NOTE:** Disabled for 'bubble' type + * @property {boolean|object} [point.radialGradient=false] Set the radial gradient on point.

+ * Or customize by giving below object value: + * - cx {number}: `cx` value (default: `0.3`) + * - cy {number}: `cy` value (default: `0.3`) + * - r {number}: `r` value (default: `0.7`) + * - stops {Array}: Each item should be having `[offset, stop-color, stop-opacity]` values. + * - (default: `[[0.1, $DATA_COLOR, 1], [0.9, $DATA_COLOR, 0]]`) * @property {boolean} [point.focus.expand.enabled=true] Whether to expand each point on focus. * @property {number} [point.focus.expand.r=point.r*1.75] The radius size of each point on focus. * - **NOTE:** For 'bubble' type, the default is `bubbleSize*1.15` @@ -59,6 +66,7 @@ export default { * (ex. ``) * @see [Demo: point type](https://naver.github.io/billboard.js/demo/#Point.RectanglePoints) * @see [Demo: point focus only](https://naver.github.io/billboard.js/demo/#Point.FocusOnly) + * @see [Demo: point radialGradient](https://naver.github.io/billboard.js/demo/#Point.RadialGradientPoint) * @see [Demo: point sensitivity](https://naver.github.io/billboard.js/demo/#Point.PointSensitivity) * @example * point: { @@ -71,6 +79,32 @@ export default { * return r; * }, * + * // will generate follwing radialGradient: + * // for more info: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/radialGradient + * // + * // + * // + * // + * radialGradient: true, + * + * // Or customized gradient + * radialGradient: { + * cx: 0.3, // cx attributes + * cy: 0.5, // cy attributes + * r: 0.7, // r attributes + * stops: [ + * // offset, stop-color, stop-opacity + * [0, "#7cb5ec", 1], + * + * // setting 'null' for stop-color, will set its original data color + * [0.5, null, 0], + * + * // setting 'function' for stop-color, will pass data id as argument. + * // It should return color string or null value + * [1, function(id) { return id === "data1" ? "red" : "blue"; }, 0], + * ] + * }, + * * focus: { * expand: { * enabled: true, @@ -117,6 +151,13 @@ export default { */ point_show: true, point_r: 2.5, + point_radialGradient: < + boolean | { + cx?: number; + cy?: number; + r?: number; + stops?: [number, string | null | Function, number] + }> false, point_sensitivity: number)> 10, point_focus_expand_enabled: true, point_focus_expand_r: undefined, diff --git a/src/config/Options/shape/area.ts b/src/config/Options/shape/area.ts index f18736296..466e19f1f 100644 --- a/src/config/Options/shape/area.ts +++ b/src/config/Options/shape/area.ts @@ -18,9 +18,10 @@ export default { * @property {boolean} [area.front=true] Set area node to be positioned over line node. * @property {boolean|object} [area.linearGradient=false] Set the linear gradient on area.

* Or customize by giving below object value: - * - x {Array}: `x1`, `x2` value - * - y {Array}: `y1`, `y2` value + * - x {Array}: `x1`, `x2` value (default: `[0, 0]`) + * - y {Array}: `y1`, `y2` value (default: `[0, 1]`) * - stops {Array}: Each item should be having `[offset, stop-color, stop-opacity]` values. + * - (default: `[[0, $DATA_COLOR, 1], [1, $DATA_COLOR, 0]]`) * @property {boolean} [area.zerobased=true] Set if min or max value will be 0 on area chart. * @see [MDN's <linearGradient>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient), [<stop>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/stop) * @see [Demo](https://naver.github.io/billboard.js/demo/#Chart.AreaChart) @@ -37,6 +38,7 @@ export default { * front: false, * * // will generate follwing linearGradient: + * // for more info: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient * // * // * // @@ -65,7 +67,11 @@ export default { area_below: false, area_front: true, area_linearGradient: < - boolean|{x?: number[]; y?: number[]; stops?: [number, string|Function|null, number]} - > false, + boolean | { + x?: [number, number]; + y?:[number, number]; + stops?: [number, string | null | Function, number] + } + > false, area_zerobased: true }; diff --git a/src/config/Options/shape/bar.ts b/src/config/Options/shape/bar.ts index 27484f2a4..a6d69f9d1 100644 --- a/src/config/Options/shape/bar.ts +++ b/src/config/Options/shape/bar.ts @@ -17,9 +17,10 @@ export default { * @property {number} [bar.label.threshold=0] Set threshold ratio to show/hide labels. * @property {boolean|object} [bar.linearGradient=false] Set the linear gradient on bar.

* Or customize by giving below object value: - * - x {Array}: `x1`, `x2` value - * - y {Array}: `y1`, `y2` value + * - x {Array}: `x1`, `x2` value (default: `[0, 0]`) + * - y {Array}: `y1`, `y2` value (default: `[0, 1]`) * - stops {Array}: Each item should be having `[offset, stop-color, stop-opacity]` values. + * - (default: `[[0, $DATA_COLOR, 1], [1, $DATA_COLOR, 0]]`) * @property {boolean} [bar.overlap=false] Bars will be rendered at same position, which will be overlapped each other. (for non-grouped bars only) * @property {number} [bar.padding=0] The padding pixel value between each bar. * @property {number} [bar.radius] Set the radius of bar edge in pixel. @@ -53,6 +54,7 @@ export default { * }, * * // will generate follwing linearGradient: + * // for more info: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient * // * // * // @@ -121,8 +123,12 @@ export default { bar_indices_removeNull: false, bar_label_threshold: 0, bar_linearGradient: < - boolean|{x?: number[]; y?: number[]; stops?: [number, string|Function|null, number]} - > false, + boolean | { + x?: [number, number]; + y?:[number, number]; + stops?: [number, string | null | Function, number] + } + > false, bar_overlap: false, bar_padding: 0, bar_radius: undefined, diff --git a/test/shape/point-spec.ts b/test/shape/point-spec.ts index e8c2c8c81..d7dfa6c93 100644 --- a/test/shape/point-spec.ts +++ b/test/shape/point-spec.ts @@ -580,4 +580,119 @@ describe("SHAPE POINT", () => { }); }); }); + + describe("point radialGradient", () => { + before(() => { + args = { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 100, 250], + ["data2", 130, 100, 130, 200, 150, 50] + ], + type: "scatter" + }, + point: { + r: 20, + radialGradient: true, + opacity: 1, + sensitivity: "radius" + }, + axis: { + x: { + type: "category" + } + } + }; + }); + + it("should defs correctly generated", () => { + const {$: {circles}, internal: {$el}} = chart; + const radialGradientDefs = $el.defs.selectAll("radialGradient"); + const ids = chart.data().map(v => v.id); + const rx = /.+-(\w+\d+)$/; + const radialGradientIds: string[] = []; + + radialGradientDefs.each(function(d, i) { + const id = this.id.replace(rx, "$1"); + + radialGradientIds.push(this.id); + + expect(id).to.be.equal(ids[i]); + expect(this.querySelectorAll("stop").length).to.be.equal(2); + }); + + ids.forEach((id, i) => { + const radialId = radialGradientIds[i]; + + circles.filter(d => d.id === id).each(function() { + expect(this.style.fill.indexOf(radialId) > -1).to.be.true; + }); + }); + }); + + it("set options", () => { + args = { + data: { + columns: [ + ["data1", 30, 200, 100, 400, 100, 250], + ["data2", 130, 100, 130, 200, 150, 50] + ], + type: "bubble" + }, + point: { + r: 10, + radialGradient: { + cx: 0.5, + cy: 0.5, + r: 0.5, + stops: [ + [0.3, "#fff", 0.8], + [0.6, function(id) { return id === "data1" ? this.color(id) : "green"; }, 0.35], + [1, null, 1] + ] + }, + opacity: 1, + sensitivity: "radius" + } + }; + }); + + it("should radialGradient options are correctly specified.", () => { + const {$: {circles}, internal: {$el}} = chart; + const radialGradientDefs = $el.defs.selectAll("radialGradient"); + const ids = chart.data().map(v => v.id); + const rx = /.+-(\w+\d+)$/; + const radialGradientIds: string[] = []; + const options = args.point.radialGradient; + + radialGradientDefs.each(function(d, i) { + const id = this.id.replace(rx, "$1"); + + radialGradientIds.push(this.id); + + expect(id).to.be.equal(ids[i]); + + expect(+this.getAttribute("cx")).to.be.equal(options.cx); + expect(+this.getAttribute("cy")).to.be.equal(options.cy); + expect(+this.getAttribute("r")).to.be.equal(options.r); + + this.querySelectorAll("stop").forEach((stop, i) => { + const [offset, color, opacity] = options.stops[i]; + + expect(+stop.getAttribute("offset")).to.be.equal(offset); + expect(stop.getAttribute("stop-color")).to.be.equal(typeof color === "function" ? color.bind(chart)(id) : color ?? chart.color(id)); + expect(+stop.getAttribute("stop-opacity")).to.be.equal(opacity); + }); + }); + + ids.forEach((id, i) => { + const radialId = radialGradientIds[i]; + + circles.filter(d => d.id === id).each(function() { + expect(this.style.fill.indexOf(radialId) > -1).to.be.true; + }); + }); + }); + }); }); +0 \ No newline at end of file diff --git a/types/options.d.ts b/types/options.d.ts index 3a8f38fcf..c3bb55d72 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -18,6 +18,7 @@ import { PieOptions, PolarOptions, RadarOptions, + RadialGradientOptions, ScatterOptions, SplineOptions, TreemapOptions @@ -851,6 +852,11 @@ export interface PointOptions { */ opacity?: number | null; + /** + * Set the radial gradient on point. + */ + radialGradient?: RadialGradientOptions; + select?: { /** * The radius size of each point on selected. diff --git a/types/options.shape.d.ts b/types/options.shape.d.ts index 1f09723c8..c77ebfe9e 100644 --- a/types/options.shape.d.ts +++ b/types/options.shape.d.ts @@ -709,6 +709,32 @@ export interface LinearGradientOptions { stops?: Array<[number, string | null | ((this: Chart, id: string) => string), number]>; } +export interface RadialGradientOptions { + /** + * cx attribute + */ + cx?: number; + + /** + * cy attribute + */ + cy?: number; + + /** + * r attribute + */ + r?: number; + + /** + * The ramp of colors to use on a gradient + * + * offset, stop-color, stop-opacity + * - setting 'null' for stop-color, will set its original data color + * - setting 'function' for stop-color, will pass data id as argument. It should return color string or null value + */ + stops?: Array<[number, string | null | ((this: Chart, id: string) => string), number]>; +} + export interface TreemapOptions { /** * Treemap tile type