billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
222 lines (183 loc) • 6.05 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {select as d3Select} from "d3-selection";
import {scaleOrdinal as d3ScaleOrdinal} from "d3-scale";
import {document, window} from "../../module/browser";
import {$ARC, $COLOR, $SHAPE} from "../../config/classes";
import {KEY} from "../../module/Cache";
import {notEmpty, isFunction, isObject, isString} from "../../module/util";
/**
* Set pattern's background color
* (it adds a <rect> element to simulate bg-color)
* @param {SVGPatternElement} pattern SVG pattern element
* @param {string} color Color string
* @param {string} id ID to be set
* @returns {{id: string, node: SVGPatternElement}}
* @private
*/
const colorizePattern = (pattern, color, id: string) => {
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()
};
};
// Replacement of d3.schemeCategory10.
// Contained differently depend on d3 version: v4(d3-scale), v5(d3-scale-chromatic)
const schemeCategory10 = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"];
export default {
/**
* Get color pattern from CSS file
* CSS should be defined as: background-image: url("#00c73c;#fa7171; ...");
* @returns {Array}
* @private
*/
getColorFromCss(): string[] {
const cacheKey = KEY.colorPattern;
const {body} = document;
let pattern = body[cacheKey];
if (!pattern) {
const delimiter = ";";
const span = document.createElement("span");
span.className = $COLOR.colorPattern;
span.style.display = "none";
body.appendChild(span);
const content = window.getComputedStyle(span).backgroundImage;
span.parentNode.removeChild(span);
if (content.indexOf(delimiter) > -1) {
pattern = content
.replace(/url[^#]*|["'()]|(\s|%20)/g, "")
.split(delimiter)
.map(v => v.trim().replace(/[\"'\s]/g, ""))
.filter(Boolean);
body[cacheKey] = pattern;
}
}
return pattern;
},
generateColor(): Function {
const $$ = this;
const {config} = $$;
const colors = config.data_colors;
const callback = config.data_color;
const ids: string[] = [];
let pattern = notEmpty(config.color_pattern) ? config.color_pattern :
d3ScaleOrdinal($$.getColorFromCss() || schemeCategory10).range();
const originalColorPattern = pattern;
if (isFunction(config.color_tiles)) {
const tiles = config.color_tiles.bind($$.api)();
// Add background color to patterns
const colorizedPatterns = pattern.map((p, index) => {
const color = p.replace(/[#\(\)\s,]/g, "");
const id = `${$$.state.datetimeId}-pattern-${color}-${index}`;
return colorizePattern(tiles[index % tiles.length], p, id);
});
pattern = colorizedPatterns.map(p => `url(#${p.id})`);
$$.patterns = colorizedPatterns;
}
return function(d) {
const id: string = d.id || d.data?.id || d;
const isLine = $$.isTypeOf(id, ["line", "spline", "step"]) || !config.data_types[id];
let color;
// if callback function is provided
if (isFunction(colors[id])) {
color = colors[id].bind($$.api)(d);
// if specified, choose that color
} else if (colors[id]) {
color = colors[id];
// if not specified, choose from pattern
} else {
if (ids.indexOf(id) < 0) {
ids.push(id);
}
color = isLine ? originalColorPattern[ids.indexOf(id) % originalColorPattern.length] :
pattern[ids.indexOf(id) % pattern.length];
colors[id] = color;
}
return isFunction(callback) ?
callback.bind($$.api)(color, d) : color;
};
},
generateLevelColor(): Function | null {
const $$ = this;
const {config} = $$;
const colors = config.color_pattern;
const threshold = config.color_threshold;
const asValue = threshold.unit === "value";
const max = threshold.max || 100;
const values = threshold.values &&
threshold.values.length ? threshold.values : [];
return notEmpty(threshold) ? function(value) {
const v = asValue ? value : (value * 100 / max);
let color = colors[colors.length - 1];
for (let i = 0, l = values.length; i < l; i++) {
if (v <= values[i]) {
color = colors[i];
break;
}
}
return color;
} : null;
},
/**
* Append data backgound color filter definition
* @private
*/
generateDataLabelBackgroundColorFilter(): void {
const $$ = this;
const {$el, config, state} = $$;
const backgroundColors = config.data_labels_backgroundColors;
if (backgroundColors) {
let ids: string[] = [];
if (isString(backgroundColors)) {
ids.push("");
} else if (isObject(backgroundColors)) {
ids = Object.keys(backgroundColors);
}
ids.forEach(v => {
const id = `${state.datetimeId}-labels-bg${$$.getTargetSelectorSuffix(v)}`;
$el.defs.append("filter")
.attr("x", "0")
.attr("y", "0")
.attr("width", "1")
.attr("height", "1")
.attr("id", id)
.html(`<feFlood flood-color="${v === "" ? backgroundColors : backgroundColors[v]}" /><feComposite in="SourceGraphic"/>`);
});
}
},
/**
* Set the data over color.
* When is out, will restate in its previous color value
* @param {boolean} isOver true: set overed color, false: restore
* @param {number|object} d target index or data object for Arc type
* @private
*/
setOverColor(isOver: boolean, d): void {
const $$ = this;
const {config, $el: {main}} = $$;
const onover = config.color_onover;
let color = isOver ? onover : $$.color;
if (isObject(color)) {
color = ({id}) => (id in onover ? onover[id] : $$.color(id));
} else if (isString(color)) {
color = () => onover;
} else if (isFunction(onover)) {
color = color.bind($$.api);
}
main.selectAll(
isObject(d) ?
// when is Arc type
`.${$ARC.arc}${$$.getTargetSelectorSuffix(d.id)}` :
`.${$SHAPE.shape}-${d}`
).style("fill", color);
}
};