diff --git a/demo/demo.js b/demo/demo.js
index 38543d805..9e0a6aabc 100644
--- a/demo/demo.js
+++ b/demo/demo.js
@@ -1606,79 +1606,59 @@ var demos = {
]
},
point: {
- type: {
- create: function(element, cssClassFn, sizeFn, fillStyleFn) {
- return element.enter().append("polygon")
- .attr("class", cssClassFn)
- .style("fill", fillStyleFn);
- },
- update: function(element, xPosFn, yPosFn, opacityStyleFn, fillStyleFn,
- withTransition, flow, selectedCircles) {
- var size = this.pointR(element) * 3.0;
- var halfSize = size * 0.5;
-
- function getPoints(d) {
- var x1 = xPosFn(d);
- var y1 = yPosFn(d) - halfSize;
- var x2 = x1 - halfSize;
- var y2 = y1 + size;
- var x3 = x1 + halfSize;
- var y3 = y2;
-
- return [x1, y1, x2, y2, x3, y3].join(" ");
- }
-
- return element
- .attr("points", getPoints)
- .style("opacity", opacityStyleFn)
- .style("fill", fillStyleFn);
- }
- }
+ pattern: [
+ ""
+ ]
}
}
},
- CustomPointsDiamond: {
+ CustomPointsDiamonds: {
options: {
data: {
columns: [
- ['data1', 100, 200, 1000, 900, 500],
+ ['data1', 100, 400, 1000, 900, 500],
['data2', 20, 40, 500, 300, 200]
]
},
point: {
- type: {
- create: function(element, cssClassFn, sizeFn, fillStyle) {
- // create custom an element node
- return element.enter().append("polygon")
- .attr("class", cssClassFn)
- .style("fill", fillStyle);
- },
-
- update: function(element, xPosFn, yPosFn, opacityStyleFn, fillStyleFn,
- withTransition, flow, selectedCircles) {
- var size = this.pointR(element) * 3.0;
- var halfSize = size * 0.5;
-
- function getPoints(d) {
- var x1 = xPosFn(d);
- var y1 = yPosFn(d) - halfSize;
- var x2 = x1 - halfSize;
- var y2 = y1 + halfSize;
- var x3 = x1;
- var y3 = y2 + halfSize;
- var x4 = x1 + halfSize;
- var y4 = y2;
-
- return [x1, y1, x2, y2, x3, y3, x4, y4].join(" ");
- }
-
- // style the custom element added
- return element
- .attr("points", getPoints)
- .style("opacity", opacityStyleFn)
- .style("fill", fillStyleFn);
- }
- }
+ pattern: [
+ ""
+ ]
+ }
+ }
+ },
+ CustomPointsHearts: {
+ options: {
+ data: {
+ columns: [
+ ['data1', 100, 400, 1000, 900, 500],
+ ['data2', 20, 40, 500, 300, 200]
+ ]
+ },
+ point: {
+ pattern: [
+ ""
+ ]
+ }
+ }
+ },
+ CombinationPoints: {
+ options: {
+ data: {
+ columns: [
+ ['data1', 100, 400, 1000, 900, 500],
+ ['data2', 20, 40, 500, 300, 200],
+ ['data3', 80, 350, 800, 450, 500],
+ ['data4', 150, 240, 300, 700, 300]
+ ]
+ },
+ point: {
+ pattern: [
+ "circle",
+ "rectangle",
+ "",
+ ""
+ ]
}
}
}
diff --git a/spec/interaction-spec.js b/spec/interaction-spec.js
index db8b67037..e76fed13e 100644
--- a/spec/interaction-spec.js
+++ b/spec/interaction-spec.js
@@ -163,7 +163,7 @@ describe("INTERACTION", () => {
});
it("set option point.type='rectangle'", () => {
- args.point.type = "rectangle";
+ args.point.pattern = ["rectangle"];
clicked = false;
data = null;
});
@@ -183,34 +183,9 @@ describe("INTERACTION", () => {
});
it("set option point.type=polygon(custom triangle)", () => {
- args.point.type = {
- create: function(element, cssClassFn, sizeFn, fillStyleFn) {
- return element.enter().append("polygon")
- .attr("class", cssClassFn)
- .style("fill", fillStyleFn);
- },
- update: function(element, xPosFn, yPosFn, opacityStyleFn, fillStyleFn,
- withTransition, flow, selectedCircles) {
- var size = this.pointR(element) * 3.0;
- var halfSize = size * 0.5;
-
- function getPoints(d) {
- var x1 = xPosFn(d);
- var y1 = yPosFn(d) - halfSize;
- var x2 = x1 - halfSize;
- var y2 = y1 + size;
- var x3 = x1 + halfSize;
- var y3 = y2;
-
- return [x1, y1, x2, y2, x3, y3].join(" ");
- }
-
- return element
- .attr("points", getPoints)
- .style("opacity", opacityStyleFn)
- .style("fill", fillStyleFn);
- }
- };
+ args.point.pattern = [
+ ""
+ ];
clicked = false;
data = null;
@@ -219,20 +194,20 @@ describe("INTERACTION", () => {
it("check for data click for polygon data point", () => {
const main = chart.internal.main;
const rect = main.select(`.${CLASS.eventRect}.${CLASS.eventRect}`).node();
- const circle = main.select(`.${CLASS.circles}-data2 polygon`).node().getBBox();
+ const circle = main.select(`.${CLASS.circles}-data2 use`).node().getBBox();
util.fireEvent(rect, "click", {
clientX: circle.x,
clientY: circle.y
}, chart);
- expect(clicked).to.be.true;
- expect(data.value).to.be.equal(20);
+ expect(clicked).to.be.false;
+ expect(data).to.be.equal(null);
});
it("set option data.type='area'", () => {
args.data.type = "area";
- args.point.type = "circle";
+ args.point.pattern = ["circle"];
clicked = false;
data = null;
diff --git a/spec/shape.point-spec.js b/spec/shape.point-spec.js
index 920d86ed3..49f2ee567 100644
--- a/spec/shape.point-spec.js
+++ b/spec/shape.point-spec.js
@@ -51,7 +51,7 @@ describe("SHAPE POINT", () => {
]
},
point: {
- type: "rectangle"
+ pattern: ["rectangle"]
}
};
});
@@ -76,46 +76,17 @@ describe("SHAPE POINT", () => {
]
},
point: {
- type: {
- create(element, cssClassFn, sizeFn, fillStyle) {
- return element.enter().append("polygon")
- .attr("class", cssClassFn)
- .style("fill", fillStyle);
- },
-
- update(element, xPosFn, yPosFn, opacityStyleFn, fillStyleFn,
- withTransition, flow, selectedCircles) {
- let mainCircles;
- const triangleSize = 10;
-
- function getPoints(d) {
- const x1 = xPosFn(d);
- const y1 = yPosFn(d) - (triangleSize * 0.5);
- const x2 = x1 - (triangleSize * 0.5);
- const y2 = y1 + (triangleSize * 0.5);
- const x3 = x1;
- const y3 = y2 + (triangleSize * 0.5);
- const x4 = x1 + (triangleSize * 0.5);
- const y4 = y2;
- return `${x1} ${y1} ${x2} ${y2} ${x3} ${y3} ${x4} ${y4}`;
- }
-
- mainCircles = element
- .attr("points", getPoints)
- .style("opacity", opacityStyleFn)
- .style("fill", fillStyleFn);
-
- return mainCircles;
- }
- }
+ pattern: [
+ ""
+ ]
}
};
});
- it("Should render svg polygon elements", () => {
+ it("Should render svg \"use\" elements", () => {
const target = chart.internal.svg.select(".bb-chart-line.bb-target-data1");
const circlesEl = target.select(".bb-circles-data1").node();
- const polygons = circlesEl.getElementsByTagName("polygon");
+ const polygons = circlesEl.getElementsByTagName("use");
expect(polygons.length).to.be.equal(6);
});
diff --git a/src/api/api.flow.js b/src/api/api.flow.js
index 595f91416..954384472 100644
--- a/src/api/api.flow.js
+++ b/src/api/api.flow.js
@@ -406,7 +406,9 @@ extend(ChartInternal.prototype, {
mainCircle
.attr("x", xFunc)
- .attr("y", yFunc);
+ .attr("y", yFunc)
+ .attr("cx", cx) // when pattern is used, it possibly contain 'circle' also.
+ .attr("cy", cy);
}
mainText
diff --git a/src/config/Options.js b/src/config/Options.js
index 059171697..31d282661 100644
--- a/src/config/Options.js
+++ b/src/config/Options.js
@@ -2087,14 +2087,23 @@ export default class Options {
* @property {Boolean} [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`
* @property {Number} [point.select.r=point.r*4] The radius size of each point on selected.
- * @property {String|Object} [point.type="circle"] The type of point to be drawn
- * - **Note:** If chart has 'bubble' type, only circle can be used.
+ * @property {String} [point.type="circle"] The type of point to be drawn
+ * - **Note:**
+ * - If chart has 'bubble' type, only circle can be used.
+ * - For IE, non circle point expansions are not supported due to lack of transform support.
* - **Available Values:**
* - circle
* - rectangle
- *
- * @property {Function} [point.type.create] If specified will be invoked to create data points, this function must return a d3 selection.
- * @property {Function} [point.type.update] If specified will be invoked to update data points, this function must return a d3 selection.
+ * @property {Array} [point.pattern=[]] The type of point or svg shape as string, to be drawn for each line
+ * - **Note:**
+ * - This is an `experimental` feature and can have some unexpected behaviors.
+ * - If chart has 'bubble' type, only circle can be used.
+ * - For IE, non circle point expansions are not supported due to lack of transform support.
+ * - **Available Values:**
+ * - circle
+ * - rectangle
+ * - svg shape tag interpreted as string
+ * (ex. ``)
* @example
* point: {
* show: false,
@@ -2116,22 +2125,15 @@ export default class Options {
* r: 3
* },
*
+ * // valid values are "circle" or "rectangle"
* type: "rectangle",
*
- * // or for custom shapes you can use an object with a "create" and "update" functions
- * type: {
- * // to create a custom type, set create & update functions as well
- * create: function(element, cssClassFn, sizeFn, fillStyleFn) {
- * // should create node element to be used as data point and must return a d3.selection
- * ...
- * return element;
- * },
- * update: function(element, xPosFn, yPosFn, opacityStyleFn, fillStyleFn, withTransition, flow, selectedCircles) {
- * // adjust the position & styles to the given element and must return a d3.selection
- * ...
- * return element;
- * }
- * }
+ * // or indicate as pattern
+ * pattern: [
+ * "circle",
+ * "rectangle",
+ * ""
+ * ],
* }
*/
point_show: true,
@@ -2139,6 +2141,7 @@ export default class Options {
point_sensitivity: 10,
point_focus_expand_enabled: true,
point_focus_expand_r: undefined,
+ point_pattern: [],
point_select_r: undefined,
point_type: "circle",
diff --git a/src/internals/ChartInternal.js b/src/internals/ChartInternal.js
index 69682b42d..65ee8c84f 100644
--- a/src/internals/ChartInternal.js
+++ b/src/internals/ChartInternal.js
@@ -82,7 +82,8 @@ export default class ChartInternal {
const config = $$.config;
// MEMO: clipId needs to be unique because it conflicts when multiple charts exist
- $$.clipId = `bb-${+new Date()}-clip`;
+ $$.datetimeId = `bb-${+new Date()}`;
+ $$.clipId = `${$$.datetimeId}-clip`;
$$.clipIdForXAxis = `${$$.clipId}-xaxis`;
$$.clipIdForYAxis = `${$$.clipId}-yaxis`;
$$.clipIdForGrid = `${$$.clipId}-grid`;
@@ -101,6 +102,7 @@ export default class ChartInternal {
$$.color = $$.generateColor();
$$.levelColor = $$.generateLevelColor();
+ $$.point = $$.generatePoint();
$$.extraLineClasses = $$.generateExtraLineClass();
@@ -257,17 +259,17 @@ export default class ChartInternal {
$$.svg.attr("class", config.svg_classname);
// Define defs
- const defs = $$.svg.append("defs");
+ $$.defs = $$.svg.append("defs");
- $$.clipChart = $$.appendClip(defs, $$.clipId);
- $$.clipXAxis = $$.appendClip(defs, $$.clipIdForXAxis);
- $$.clipYAxis = $$.appendClip(defs, $$.clipIdForYAxis);
- $$.clipGrid = $$.appendClip(defs, $$.clipIdForGrid);
- $$.clipSubchart = $$.appendClip(defs, $$.clipIdForSubchart);
+ $$.clipChart = $$.appendClip($$.defs, $$.clipId);
+ $$.clipXAxis = $$.appendClip($$.defs, $$.clipIdForXAxis);
+ $$.clipYAxis = $$.appendClip($$.defs, $$.clipIdForYAxis);
+ $$.clipGrid = $$.appendClip($$.defs, $$.clipIdForGrid);
+ $$.clipSubchart = $$.appendClip($$.defs, $$.clipIdForSubchart);
// set color patterns
if (isFunction(config.color_tiles) && $$.patterns) {
- $$.patterns.forEach(p => defs.append(() => p.node));
+ $$.patterns.forEach(p => $$.defs.append(() => p.node));
}
$$.updateSvgSize();
diff --git a/src/internals/type.js b/src/internals/type.js
index 060226797..be371c69f 100644
--- a/src/internals/type.js
+++ b/src/internals/type.js
@@ -112,7 +112,11 @@ extend(ChartInternal.prototype, {
// determine if is 'circle' data point
isCirclePoint() {
- return this.config.point_type === "circle";
+ const config = this.config;
+ const pattern = config.point_pattern;
+
+ return config.point_type === "circle" &&
+ (!pattern || (isArray(pattern) && pattern.length === 0));
},
lineData(d) {
diff --git a/src/internals/util.js b/src/internals/util.js
index f063f6d4b..7063430d0 100644
--- a/src/internals/util.js
+++ b/src/internals/util.js
@@ -267,6 +267,7 @@ const colorizePattern = (pattern, color) => {
* Copy array like object to array
* @param {Object} v
* @returns {Array}
+ * @private
*/
const toArray = v => [].slice.call(v);
@@ -274,6 +275,7 @@ const toArray = v => [].slice.call(v);
* Get css rules for specified stylesheets
* @param {Array} styleSheets The stylesheets to get the rules from
* @returns {Array}
+ * @private
*/
const getCssRules = styleSheets => {
let rules = [];
diff --git a/src/shape/line.js b/src/shape/line.js
index 654a974f1..0556a20cf 100644
--- a/src/shape/line.js
+++ b/src/shape/line.js
@@ -435,15 +435,8 @@ extend(ChartInternal.prototype, {
$$.mainCircle.exit().remove();
- let createFn = $$.circle.create;
-
- if ($$.hasValidPointType()) {
- createFn = $$[$$.config.point_type].create;
- } else if ($$.hasValidPointDrawMethods()) {
- createFn = $$.config.point_type.create;
- }
-
- $$.mainCircle = createFn($$.mainCircle, $$.classCircle.bind($$), $$.pointR.bind($$), $$.color)
+ $$.mainCircle = $$.mainCircle.enter()
+ .append($$.point("create", this, $$.classCircle.bind($$), $$.pointR.bind($$), $$.color))
.merge($$.mainCircle)
.style("opacity", $$.initialOpacityForCircle.bind($$));
},
@@ -451,30 +444,20 @@ extend(ChartInternal.prototype, {
redrawCircle(cx, cy, withTransition, flow) {
const $$ = this;
const selectedCircles = $$.main.selectAll(`.${CLASS.selectedCircle}`);
- const pointType = $$.config.point_type;
if (!$$.config.point_show) {
return [];
}
- let updateFn = $$.circle.update;
+ const mainCircles = [];
- if ($$.hasValidPointType()) {
- updateFn = $$[pointType].update;
- } else if ($$.hasValidPointDrawMethods()) {
- updateFn = pointType.update;
- }
+ $$.mainCircle.each(function(d) {
+ const fn = $$.point("update", $$, cx, cy, $$.opacityForCircle.bind($$), $$.color, withTransition, flow, selectedCircles).bind(this);
+ const result = fn(d);
+
+ mainCircles.push(result);
+ });
- const mainCircles = updateFn.bind(this)(
- $$.mainCircle,
- cx,
- cy,
- $$.opacityForCircle.bind($$),
- $$.color,
- withTransition,
- flow,
- $$.main.selectAll(`.${CLASS.selectedCircle}`)
- );
const posAttr = $$.isCirclePoint() ? "c" : "";
return [
@@ -529,17 +512,26 @@ extend(ChartInternal.prototype, {
$$.unexpandCircles();
}
- const circles = $$.getCircles(i, id)
- .classed(CLASS.EXPANDED, true);
+ const circles = $$.getCircles(i, id).classed(CLASS.EXPANDED, true);
+ const scale = r(circles) / $$.config.point_r;
if ($$.isCirclePoint()) {
circles.attr("r", r);
} else {
- const scale = r(circles) / $$.config.point_r;
-
- circles
- .style("transform-origin", "center")
- .style("transform", `scale(${scale})`);
+ // transform must be applied to each node individually
+ circles.each(function(d) {
+ const point = d3Select(this);
+
+ const box = this.getBBox();
+ const x1 = box.x + (box.width * 0.5);
+ const y1 = box.y + (box.height * 0.5);
+ const x2 = (1 - scale) * x1;
+ const y2 = (1 - scale) * y1;
+
+ this.tagName === "circle" ?
+ point.attr("r", r) :
+ point.style("transform", `translate(${x2}px, ${y2}px) scale(${scale})`);
+ });
}
},
@@ -551,13 +543,12 @@ extend(ChartInternal.prototype, {
.filter(function() { return d3Select(this).classed(CLASS.EXPANDED); })
.classed(CLASS.EXPANDED, false);
- if ($$.isCirclePoint()) {
- circles.attr("r", r);
- } else {
- circles
- .style("transform-origin", null)
- .style("transform", null);
- }
+ const scale = r(circles) / $$.config.point_r;
+
+ circles.attr("r", r);
+
+ !$$.isCirclePoint() &&
+ circles.style("transform", `scale(${scale})`);
},
pointR(d) {
diff --git a/src/shape/point.js b/src/shape/point.js
index 5dde82929..774ba2b98 100644
--- a/src/shape/point.js
+++ b/src/shape/point.js
@@ -2,32 +2,145 @@
* Copyright (c) 2017 NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
+import {
+ namespaces as d3Namespaces,
+ select as d3Select
+} from "d3";
import ChartInternal from "../internals/ChartInternal";
-import {isFunction, isObjectType, extend} from "../internals/util";
+import {isFunction, isObjectType, extend, notEmpty} from "../internals/util";
extend(ChartInternal.prototype, {
hasValidPointType(type) {
return /^(circle|rect(angle)?|polygon|ellipse)$/i.test(type || this.config.point_type);
},
- hasValidPointDrawMethods() {
- const pointType = this.config.point_type;
+ hasValidPointDrawMethods(type) {
+ const pointType = type || this.config.point_type;
return isObjectType(pointType) &&
isFunction(pointType.create) && isFunction(pointType.update);
},
+ insertPointInfoDefs(point, id) {
+ const $$ = this;
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(point, "image/svg+xml");
+ const node = doc.firstChild;
+ const clone = document.createElementNS(d3Namespaces.svg, node.nodeName.toLowerCase());
+ const attribs = node.attributes;
+
+ for (let i = 0, l = attribs.length; i < l; i++) {
+ const name = attribs[i].name;
+
+ clone.setAttribute(name, node.getAttribute(name));
+ }
+
+ clone.id = id;
+ clone.style.fill = "inherit";
+ clone.style.stroke = "none";
+
+ $$.defs.node().appendChild(clone);
+ },
+
+ pointFromDefs(id) {
+ return this.defs.select(`#${id}`);
+ },
+
+ generatePoint() {
+ const $$ = this;
+ const config = $$.config;
+ const ids = [];
+ const pattern = notEmpty(config.point_pattern) ? config.point_pattern : [config.point_type];
+
+ return function(method, context, ...args) {
+ return function(d) {
+ const id = d.id || (d.data && d.data.id) || d;
+ const element = d3Select(this);
+ let point;
+
+ if (ids.indexOf(id) < 0) {
+ ids.push(id);
+ }
+ point = pattern[ids.indexOf(id) % pattern.length];
+
+ if ($$.hasValidPointType(point)) {
+ point = $$[point];
+ } else if (!$$.hasValidPointDrawMethods(point)) {
+ const pointId = `${$$.datetimeId}-point-${id}`;
+ const pointFromDefs = $$.pointFromDefs(pointId);
+
+ if (pointFromDefs.size() < 1) {
+ $$.insertPointInfoDefs(point, pointId);
+ }
+
+ if (method === "create") {
+ return $$.custom.create.bind(context)(element, pointId, ...args);
+ } else if (method === "update") {
+ return $$.custom.update.bind(context)(element, ...args);
+ }
+ }
+
+ return point[method].bind(context)(element, ...args);
+ };
+ };
+ },
+
getTransitionName() {
return Math.random().toString();
},
+ custom: {
+ create(element, id, cssClassFn, sizeFn, fillStyleFn) {
+ return element.append("use")
+ .attr("xlink:href", `#${id}`)
+ .attr("class", cssClassFn)
+ .style("fill", fillStyleFn)
+ .node();
+ },
+
+ update(element, xPosFn, yPosFn, opacityStyleFn, fillStyleFn,
+ withTransition, flow, selectedCircles) {
+ const $$ = this;
+ const box = element.node().getBBox();
+ const xPosFn2 = d => xPosFn(d) - (box.width * 0.5);
+ const yPosFn2 = d => yPosFn(d) - (box.height * 0.5);
+ let mainCircles = element;
+
+ if (withTransition) {
+ const transitionName = $$.getTransitionName();
+
+ if (flow) {
+ mainCircles = element
+ .attr("x", xPosFn2);
+ }
+
+ mainCircles = element
+ .transition(transitionName)
+ .attr("x", xPosFn2)
+ .attr("y", yPosFn2)
+ .transition(transitionName);
+
+ selectedCircles.transition($$.getTransitionName());
+ } else {
+ mainCircles = element
+ .attr("x", xPosFn2)
+ .attr("y", yPosFn2);
+ }
+
+ return mainCircles
+ .style("opacity", opacityStyleFn)
+ .style("fill", fillStyleFn);
+ }
+ },
+
// 'circle' data point
circle: {
create(element, cssClassFn, sizeFn, fillStyleFn) {
- return element.enter().append("circle")
+ return element.append("circle")
.attr("class", cssClassFn)
.attr("r", sizeFn)
- .style("fill", fillStyleFn);
+ .style("fill", fillStyleFn)
+ .node();
},
update(element, xPosFn, yPosFn, opacityStyleFn, fillStyleFn,
@@ -73,11 +186,12 @@ extend(ChartInternal.prototype, {
create(element, cssClassFn, sizeFn, fillStyleFn) {
const rectSizeFn = d => sizeFn(d) * 2.0;
- return element.enter().append("rect")
+ return element.append("rect")
.attr("class", cssClassFn)
.attr("width", rectSizeFn)
.attr("height", rectSizeFn)
- .style("fill", fillStyleFn);
+ .style("fill", fillStyleFn)
+ .node();
},
update(element, xPosFn, yPosFn, opacityStyleFn, fillStyleFn,