UNPKG

billboard.js

Version:

Re-usable easy interface JavaScript chart library, based on D3 v4+

222 lines (183 loc) 6.05 kB
/** * 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); } };