Skip to content

Commit

Permalink
feat(Options): Implement color.tile option
Browse files Browse the repository at this point in the history
Allow defining background patterns via option

Fix #193
Close #196
  • Loading branch information
Julien Castelain authored and netil committed Nov 27, 2017
1 parent 9e8de8f commit bec92dd
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 8 deletions.
48 changes: 48 additions & 0 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,54 @@ var demos = {
}
}
},
ColorTiles: {
options: {
data: {
columns: [
['data1', 10],
['data2', 15],
['data3', 30],
['data4', 45],
],
type: "pie"
},
color: {
tiles: function() {
function circlePattern(fillColor, opacity, radiusMin, radiusMax) {
var pattern = d3.select(document.createElementNS(d3.namespaces.svg, "pattern"))
.attr("patternUnits", "userSpaceOnUse")
.attr("width", "32")
.attr("height", "32");

var g = pattern
.append("g")
.style("fill", fillColor || "#000")
.style("opactiy", opacity || "0.2");

g
.append("circle")
.attr("cx", "3")
.attr("cy", "3")
.attr("r", radiusMin || "3");

g
.append("circle")
.attr("cx", "13")
.attr("cy", "13")
.attr("r", radiusMax || "9");

return pattern.node();
}

// Should return an array of SVGPatternElement
return [
circlePattern("#FFF", "0.2", "3", "10"),
circlePattern("yellow", "0.3", "3", "3")
];
}
}
}
},
DurationOfTransition: {
options: {
data: {
Expand Down
133 changes: 133 additions & 0 deletions spec/color-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Copyright (c) 2017 NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
/* eslint-disable */
/* global describe, beforeEach, it, expect */
import util from "./assets/util";
import CLASS from "../src/config/classes";
import {isString} from "../src/internals/util";

describe("COLOR", () => {
let chart;
let args;

beforeEach(() => {
chart = util.generate(args);
});

describe("tiles", () => {
before(() => {
args = {
data: {
columns: [
["data1", 30, 200, -100, 400, -150, 250],
["data2", 50, 20, 10, 40, 15, 25]
],
type: "bar"
},
color: {
tiles: function() {
function circlePattern(fillColor, opacity, radiusMin, radiusMax) {
const pattern = d3.select(document.createElementNS(d3.namespaces.svg, "pattern"))
.attr("patternUnits", "userSpaceOnUse")
.attr("width", "32")
.attr("height", "32");

const g = pattern
.append("g")
.style("fill", fillColor || "#000")
.style("opactiy", opacity || "0.2");

g
.append("circle")
.attr("cx", "3")
.attr("cy", "3")
.attr("r", radiusMin || "3");

g
.append("circle")
.attr("cx", "13")
.attr("cy", "13")
.attr("r", radiusMax || "9");

return pattern.node();
}

return [
circlePattern("#FFF", "0.2", "3", "10"),
circlePattern("yellow", "0.3", "3", "3")
];
}
}
};
});

it("should create patterns property", () => {
expect(chart.internal.patterns).to.be.an('array');
});

it("patterns should be an array with id and pattern objects", () => {
const patterns = chart.internal.patterns;
const numPatterns = patterns.length;

const valid = patterns.map(p => {
return isString(p.id) && p.node.nodeName.toLowerCase() === 'pattern';
});

expect(valid.length).to.be.equal(numPatterns);
});

it("check for legend color tiles", () => {
const colors = [chart.color("data1"), chart.color("data2")];

d3.selectAll(`.${CLASS.legendItem} .${CLASS.legendItemTile}`)
.each(function(v, i) {
const stroke = d3.select(this).style("stroke").replace(/\"/g, "");

expect(stroke).to.be.equal(colors[i]);
});
});

it("check for tooltip color tiles", () => {
const colors = [chart.color("data1"), chart.color("data2")];
const eventRect = chart.internal.main
.select(`.${CLASS.eventRect}-1`)
.node();

util.fireEvent(eventRect, "mousemove", {
clientX: 100,
clientY: 100
}, chart);

d3.select(chart.element)
.selectAll(`.${CLASS.tooltip} td rect`)
.each(function(v, i) {
const fill = d3.select(this).style("fill").replace(/\"/g, "");

expect(fill).to.be.equal(colors[i]);
});
});

describe("pattern names", () => {
before(() => {
args.color.pattern = ['red', 'gold', 'green'];
});

it("should create correct pattern names", () => {
const expectedIds = [
`${CLASS.colorizePattern}-red`,
`${CLASS.colorizePattern}-gold`,
`${CLASS.colorizePattern}-green`
];

const patterns = chart.internal.patterns;

patterns.forEach((p, idx) => {
expect(p.id).to.be.equal(expectedIds[idx]);
expect(p.node.id).to.be.equal(expectedIds[idx]);
});
});
});
});
});
28 changes: 27 additions & 1 deletion src/config/Options.js
Original file line number Diff line number Diff line change
Expand Up @@ -972,16 +972,42 @@ export default class Options {
* @memberof Options
* @type {Object}
* @property {Array} [color.pattern] custom color pattern
* @property {Function} [color.tiles] if defined, allows use svg's patterns. It should return an array of [SVGPatternElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGPatternElement).
* @property {Object} [color.threshold] color threshold
* @property {String} [color.threshold.unit] unit
* @property {Array} [color.threshold.value] value
* @property {Array} [color.threshold.max=100] max value
* @example
* color: {
* pattern: ["#1f77b4", "#aec7e8", ...]
* pattern: ["#1f77b4", "#aec7e8", ...],
*
* // Set colors' patterns
* // it should return an array of SVGPatternElement
* tiles: function() {
* var pattern = document.createElementNS("http://www.w3.org/2000/svg", "pattern");
* var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
* var circle1 = document.createElementNS("http://www.w3.org/2000/svg", "circle");
*
* pattern.setAttribute("patternUnits", "userSpaceOnUse");
* pattern.setAttribute("width", "32");
* pattern.setAttribute("height", "32");
*
* g.style.fill = "#000";
* g.style.opacity = "0.2";
*
* circle1.setAttribute("cx", "3");
* circle1.setAttribute("cy", "3");
* circle1.setAttribute("r", "3");
*
* g.appendChild(circle1);
* pattern.appendChild(g);
*
* return [pattern];
* }
* }
*/
color_pattern: [],
color_tiles: undefined,
color_threshold: {},

/**
Expand Down
1 change: 1 addition & 0 deletions src/config/classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default {
legendItemTile: "bb-legend-item-tile",
legendItemHidden: "bb-legend-item-hidden",
legendItemFocused: "bb-legend-item-focused",
colorizePattern: "bb-colorize-pattern",
dragarea: "bb-dragarea",
EXPANDED: "_expanded_",
SELECTED: "_selected_",
Expand Down
5 changes: 5 additions & 0 deletions src/internals/ChartInternal.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ export default class ChartInternal {
$$.clipGrid = $$.appendClip(defs, $$.clipIdForGrid);
$$.clipSubchart = $$.appendClip(defs, $$.clipIdForSubchart);

// set color patterns
if (isFunction(config.color_tiles) && $$.patterns) {
$$.patterns.forEach(p => defs.append(() => p.node));
}

$$.updateSvgSize();

// Set initialized scales to brush and zoom
Expand Down
16 changes: 14 additions & 2 deletions src/internals/color.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
schemeCategory10 as d3SchemeCategory10
} from "d3";
import ChartInternal from "./ChartInternal";
import {notEmpty, extend} from "./util";
import {notEmpty, extend, isFunction, colorizePattern} from "./util";

extend(ChartInternal.prototype, {
generateColor() {
Expand All @@ -16,9 +16,21 @@ extend(ChartInternal.prototype, {
const colors = config.data_colors;
const callback = config.data_color;
const ids = [];
const pattern = notEmpty(config.color_pattern) ?
let pattern = notEmpty(config.color_pattern) ?
config.color_pattern : d3ScaleOrdinal(d3SchemeCategory10).range();

if (isFunction(config.color_tiles)) {
const tiles = config.color_tiles();

// Add background color to patterns
const colorizedPatterns = pattern.map((p, index) =>
colorizePattern(tiles[index % tiles.length], p)
);

pattern = colorizedPatterns.map(p => `url(#${p.id})`);
$$.patterns = colorizedPatterns;
}

return function(d) {
const id = d.id || (d.data && d.data.id) || d;
let color;
Expand Down
11 changes: 7 additions & 4 deletions src/internals/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,13 @@ extend(ChartInternal.prototype, {
name = sanitise(nameFormat(row.name, row.ratio, row.id, row.index));
bgcolor = $$.levelColor ? $$.levelColor(row.value) : color(row.id);

text += `<tr class="${$$.CLASS.tooltipName}${$$.getTargetSelectorSuffix(row.id)}">
<td class="name"><span style="background-color:${bgcolor}"></span>${name}</td>
<td class="value">${value}</td>
</tr>`;
text += `<tr class="${$$.CLASS.tooltipName}${$$.getTargetSelectorSuffix(row.id)}"><td class="name">`;

text += $$.patterns ?
`<svg width="10" height="10"><rect style="fill:${bgcolor}" width="10" height="10"></rect></svg>` :
`<span style="background-image:${bgcolor}"></span>`;

text += `${name}</td><td class="value">${value}</td></tr>`;
}
}

Expand Down
30 changes: 29 additions & 1 deletion src/internals/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import {
event as d3Event,
select as d3Select,
brushSelection as d3BrushSelection
} from "d3";
import CLASS from "../config/classes";
Expand Down Expand Up @@ -237,6 +238,32 @@ const merge = (target, ...objectN) => {
return extend(target, ...objectN);
};

/**
* Set pattern's background color
* (it adds a <rect> element to simulate bg-color)
* @param {SVGPatternElement} pattern
* @param {String} color
* @return {{id: string, node: SVGPatternElement}}
* @private
*/
const colorizePattern = (pattern, color) => {
const suffix = color.replace(/[#\(\)\s,]/g, "");
const id = `${CLASS.colorizePattern}-${suffix}`;
const node = d3Select(pattern.cloneNode(true));

node
.attr("id", id)
.insert("rect", ":first-child")
.attr("width", node.attr("width"))
.attr("height", node.attr("height"))
.style("fill", color);

return {
id,
node: node.node()
};
};

export {
isValue,
isDefined,
Expand Down Expand Up @@ -264,5 +291,6 @@ export {
removeEvent,
getRectSegList,
merge,
capitalize
capitalize,
colorizePattern
};

0 comments on commit bec92dd

Please sign in to comment.