UNPKG

billboard.js

Version:

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

973 lines (972 loc) • 37.9 kB
/*! * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license * * billboard.js, JavaScript chart library * https://naver.github.io/billboard.js/ * * @version 4.1.1 */ import { document as doc, window as win } from "../module/browser.js"; import { mergeObj } from "../module/util/object.js"; //#region src/canvas/CanvasTheme.ts /** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ const SVG_NS = "http://www.w3.org/2000/svg"; /** * Parse CSS numeric value. * @param {string|number} value CSS value * @param {number} fallback Fallback value * @returns {number} Parsed number * @private */ function toNumber(value, fallback) { const parsed = typeof value === "number" ? value : parseFloat(value ?? ""); return Number.isFinite(parsed) ? parsed : fallback; } /** * Resolve effective SVG paint opacity from element opacity and fill-opacity. * @param {object} value Computed opacity values * @param {number} fallback Fallback opacity * @returns {number} Effective opacity * @private */ function toPaintOpacity(value, fallback) { const result = toNumber(value.opacity, 1) * toNumber(value["fill-opacity"], 1); return Number.isFinite(result) ? result : fallback; } /** * Parse CSS color value. * @param {string} value CSS color value * @param {string} fallback Fallback value * @returns {string} CSS color * @private */ function toColor(value, fallback) { const color = value?.trim(); return color && !/^(none|transparent|rgba\(0,\s*0,\s*0,\s*0\))$/i.test(color) ? color : fallback; } /** * Parse CSS color value only when it is different from an ignored baseline. * @param {string} value CSS color value * @param {string} ignored Ignored baseline color * @returns {string|undefined} CSS color * @private */ function toOptionalColor(value, ignored) { const color = toColor(value, ""); const ignoredColor = ignored ? toColor(ignored, "") : ""; return color && color !== ignoredColor ? color : void 0; } /** * Parse CSS numeric value only when it is different from an ignored baseline. * @param {string|number} value CSS value * @param {number} ignored Ignored baseline value * @returns {number|undefined} Parsed number * @private */ function toOptionalNumber(value, ignored) { const parsed = typeof value === "number" ? value : parseFloat(value ?? ""); const ignoredParsed = typeof ignored === "number" ? ignored : parseFloat(ignored ?? ""); return Number.isFinite(parsed) && (!Number.isFinite(ignoredParsed) || parsed !== ignoredParsed) ? parsed : void 0; } /** * Parse CSS dash array value. * @param {string|Array} value CSS stroke-dasharray value * @returns {Array} Dash array * @private */ function toDashArray(value) { if (Array.isArray(value)) return value.filter((v) => Number.isFinite(v) && v > 0); return (value || "").split(/[,\s]+/).map((v) => parseFloat(v)).filter((v) => Number.isFinite(v) && v > 0); } /** * Get CSS font shorthand usable by canvas. * @param {CSSStyleDeclaration} style Computed style * @param {string} fallback Fallback value * @returns {string} Font shorthand * @private */ function toFont(style, fallback) { const font = style.getPropertyValue("font").trim(); if (font) return font; const fontSize = style.getPropertyValue("font-size").trim(); const fontFamily = style.getPropertyValue("font-family").trim(); if (!fontSize || !fontFamily) return fallback; const fontStyle = style.getPropertyValue("font-style").trim(); const fontVariant = style.getPropertyValue("font-variant").trim(); const fontWeight = style.getPropertyValue("font-weight").trim(); const lineHeight = style.getPropertyValue("line-height").trim(); const size = lineHeight && lineHeight !== "normal" ? `${fontSize}/${lineHeight}` : fontSize; return [ fontStyle && fontStyle !== "normal" ? fontStyle : "", fontVariant && fontVariant !== "normal" ? fontVariant : "", fontWeight && !/^(normal|400)$/.test(fontWeight) ? fontWeight : "", size, fontFamily ].filter(Boolean).join(" "); } /** * Get style declaration value by CSS property name. * @param {object} style Style declaration object * @param {string} prop CSS property name * @returns {string|number|Array|undefined} Style value * @private */ function getStyleValue(style, prop) { const camelProp = prop.replace(/-([a-z])/g, (all, chr) => chr.toUpperCase()); return style[prop] ?? style[camelProp]; } /** * Read color from selector style declaration. * @param {object} style Style declaration object * @param {string} prop CSS property name * @returns {string|undefined} Color value * @private */ function readColorValue(style, prop) { const value = getStyleValue(style, prop); return (value == null ? "" : toColor(String(value), "")) || void 0; } /** * Read numeric value from selector style declaration. * @param {object} style Style declaration object * @param {string} prop CSS property name * @returns {number|undefined} Numeric value * @private */ function readNumberValue(style, prop) { const value = getStyleValue(style, prop); const parsed = typeof value === "number" ? value : parseFloat(String(value ?? "")); return Number.isFinite(parsed) ? parsed : void 0; } /** * Read paint opacity from selector style declaration. * @param {object} style Style declaration object * @returns {number|undefined} Paint opacity * @private */ function readPaintOpacityValue(style) { const opacity = readNumberValue(style, "opacity"); const fillOpacity = readNumberValue(style, "fill-opacity"); return opacity === void 0 && fillOpacity === void 0 ? void 0 : (opacity ?? 1) * (fillOpacity ?? 1); } /** * Read dash array from selector style declaration. * @param {object} style Style declaration object * @returns {Array|undefined} Dash array * @private */ function readDashArrayValue(style) { const value = getStyleValue(style, "stroke-dasharray"); const dashArray = value == null ? [] : toDashArray(value); return dashArray.length ? dashArray : void 0; } /** * Read font shorthand from selector style declaration. * @param {object} style Style declaration object * @returns {string|undefined} Font shorthand * @private */ function readFontValue(style) { const font = getStyleValue(style, "font"); if (font != null && String(font).trim()) return String(font).trim(); const fontSize = getStyleValue(style, "font-size"); const fontFamily = getStyleValue(style, "font-family"); if (fontSize == null || fontFamily == null) return; const size = String(fontSize).trim(); const family = String(fontFamily).trim(); if (!size || !family) return; const fontStyle = String(getStyleValue(style, "font-style") ?? "").trim(); const fontVariant = String(getStyleValue(style, "font-variant") ?? "").trim(); const fontWeight = String(getStyleValue(style, "font-weight") ?? "").trim(); const lineHeight = String(getStyleValue(style, "line-height") ?? "").trim(); return [ fontStyle && fontStyle !== "normal" ? fontStyle : "", fontVariant && fontVariant !== "normal" ? fontVariant : "", fontWeight && !/^(normal|400)$/.test(fontWeight) ? fontWeight : "", lineHeight && lineHeight !== "normal" ? `${size}/${lineHeight}` : size, family ].filter(Boolean).join(" "); } /** * Merge selector style values to a canvas theme section. * @param {object} target Theme override target * @param {string} section Theme section name * @param {object} values Theme section values * @private */ function mergeThemeSection(target, section, values) { const override = Object.keys(values).reduce((acc, key) => { values[key] !== void 0 && (acc[key] = values[key]); return acc; }, {}); if (Object.keys(override).length) target[section] = mergeObj(target[section] ?? {}, override); } /** * Normalize a supported SVG selector for canvas theme matching. * @param {string} selector SVG selector * @returns {Array} Normalized selector list * @private */ function normalizeThemeSelectors(selector) { return selector.split(",").map((value) => value.trim().replace(/\s*>\s*/g, " ").replace(/\s+/g, " ").replace(/^(?:svg)?\.bb\s+/, "").replace(/(^|\s)(?:g|path|rect|line|text|circle)\./g, "$1.")).filter(Boolean); } /** * Get own keys from a style object whose values are set. * @param {object} values Style values * @returns {Array} Defined keys * @private */ function getDefinedKeys(values) { return Object.keys(values).filter((key) => values[key] !== void 0); } /** * Copy the defined entries of an object into a target of the same shape. * @param {object} target Object to copy into * @param {object} source Object to copy from * @param {Set} skip Keys to leave untouched * @returns {object} The target object * @private */ function assignDefined(target, source, skip) { getDefinedKeys(source).forEach((key) => { if (!skip?.has(key)) target[key] = source[key]; }); return target; } /** * Check whether a selector is simple enough for virtual optional grid line matching. * @param {string} selector Normalized SVG selector * @returns {boolean} Whether selector can be matched * @private */ function isSimpleGridLineSelector(selector) { const tokens = selector.split(" "); return tokens.every((token, index) => token === "line" || token === "text" ? index === tokens.length - 1 : /^(\.[A-Za-z_-][\w-]*)+$/.test(token)); } /** * Get optional grid line selector target. * @param {string} selector Normalized SVG selector * @returns {string|null} Selector target * @private */ function getGridLineSelectorTarget(selector) { const tokens = selector.split(" "); const target = tokens[tokens.length - 1]; return target === "line" || target === "text" ? target : null; } /** * Split custom class value. * @param {string} className Custom class value * @returns {Array} Class names * @private */ function getClassNames(className) { return (className || "").split(/\s+/).filter(Boolean); } /** * Read optional grid line or label style from selector style declaration. * @param {object} style Style declaration object * @param {string} target Selector target * @returns {object} Grid line style * @private */ function getGridLineSelectorStyle(style, target) { return assignDefined({}, target === "text" ? { labelColor: readColorValue(style, "fill"), labelFont: readFontValue(style) } : { lineColor: readColorValue(style, "stroke"), lineWidth: readNumberValue(style, "stroke-width"), dashArray: readDashArrayValue(style) }); } /** * Convert selector based optional grid line overrides to ordered matcher entries. * @param {object} selectors Selector style declarations * @returns {Array} Optional grid line selector overrides * @private */ function getGridLineSelectorOverrides(selectors) { const overrides = []; Object.keys(selectors ?? {}).forEach((selector) => { normalizeThemeSelectors(selector).forEach((normalizedSelector) => { const target = getGridLineSelectorTarget(normalizedSelector) || "line"; const style = getGridLineSelectorStyle(selectors[selector], target); if (isSimpleGridLineSelector(normalizedSelector) && getDefinedKeys(style).length) overrides.push({ selector: normalizedSelector, target, style }); }); }); return overrides; } /** * Check whether a virtual optional grid line selector matches a grid line config. * @param {string} selector Normalized SVG selector * @param {string} axis Grid axis * @param {string} className Grid line custom class * @param {string} target Draw target * @returns {boolean} Whether selector matches * @private */ function matchesGridLineSelector(selector, axis, className, target) { if (!isSimpleGridLineSelector(selector)) return false; const selectorTarget = getGridLineSelectorTarget(selector); if (selectorTarget && selectorTarget !== target) return false; else if (!selectorTarget && target === "text") return false; const customClasses = getClassNames(className); const axisLineClass = `bb-${axis}grid-line`; const availableClasses = /* @__PURE__ */ new Set([ "bb-grid", "bb-grid-lines", `bb-${axis}grid-lines`, axisLineClass, ...customClasses ]); const selectorClasses = (selector.match(/\.[A-Za-z_-][\w-]*/g) || []).map((match) => match.slice(1)); return !!selectorClasses.length && selectorClasses.every((cls) => availableClasses.has(cls)) && (selectorClasses.includes(axisLineClass) || customClasses.some((cls) => selectorClasses.includes(cls))); } /** * Apply one selector style declaration to canvas theme override. * @param {object} target Theme override target * @param {string} selector SVG selector * @param {object} style Style declaration object * @private */ function applySelectorStyle(target, selector, style) { switch (selector) { case ".bb-axis path.domain": case ".bb-axis .domain": case ".bb-axis-x path.domain": case ".bb-axis-x .domain": case ".bb-axis-y path.domain": case ".bb-axis-y .domain": case ".bb-axis-y2 path.domain": case ".bb-axis-y2 .domain": mergeThemeSection(target, "axis", { lineColor: readColorValue(style, "stroke"), lineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-axis line": case ".bb-axis .tick line": case ".bb-axis-x line": case ".bb-axis-x .tick line": case ".bb-axis-y line": case ".bb-axis-y .tick line": case ".bb-axis-y2 line": case ".bb-axis-y2 .tick line": mergeThemeSection(target, "axis", { tickColor: readColorValue(style, "stroke"), tickWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-axis text": case ".bb-axis .tick text": { const font = readFontValue(style); mergeThemeSection(target, "axis", { labelColor: readColorValue(style, "fill"), xTickFont: font, yTickFont: font, y2TickFont: font }); break; } case ".bb-axis-x text": case ".bb-axis-x .tick text": mergeThemeSection(target, "axis", { labelColor: readColorValue(style, "fill"), xTickFont: readFontValue(style) }); break; case ".bb-axis-y text": case ".bb-axis-y .tick text": mergeThemeSection(target, "axis", { labelColor: readColorValue(style, "fill"), yTickFont: readFontValue(style) }); break; case ".bb-axis-y2 text": case ".bb-axis-y2 .tick text": mergeThemeSection(target, "axis", { labelColor: readColorValue(style, "fill"), y2TickFont: readFontValue(style) }); break; case ".tick._active_ text": case ".bb-axis .tick._active_ text": case ".bb-axis-x .tick._active_ text": mergeThemeSection(target, "axis", { activeLabelColor: readColorValue(style, "fill") }); break; case ".bb-axis-x-label": case ".bb-axis-x .bb-axis-x-label": mergeThemeSection(target, "axis", { labelFont: readFontValue(style), xLabelColor: readColorValue(style, "fill") }); break; case ".bb-axis-y-label": case ".bb-axis-y .bb-axis-y-label": mergeThemeSection(target, "axis", { labelFont: readFontValue(style), yLabelColor: readColorValue(style, "fill") }); break; case ".bb-axis-y2-label": case ".bb-axis-y2 .bb-axis-y2-label": mergeThemeSection(target, "axis", { labelFont: readFontValue(style), y2LabelColor: readColorValue(style, "fill") }); break; case ".bb-grid line": case ".bb-grid .bb-xgrid": case ".bb-grid .bb-ygrid": case ".bb-xgrid": case ".bb-ygrid": mergeThemeSection(target, "grid", { lineColor: readColorValue(style, "stroke"), lineWidth: readNumberValue(style, "stroke-width"), dashArray: readDashArrayValue(style) }); break; case ".bb-grid text": mergeThemeSection(target, "grid", { labelColor: readColorValue(style, "fill"), labelFont: readFontValue(style) }); break; case ".bb-xgrid-focus line": case ".bb-grid .bb-xgrid-focus line": case ".bb-grid .bb-xgrid-focus": mergeThemeSection(target, "focusGrid", { lineColor: readColorValue(style, "stroke"), lineWidth: readNumberValue(style, "stroke-width"), dashArray: readDashArrayValue(style) }); break; case ".bb-region rect": case ".bb-region": mergeThemeSection(target, "region", { fill: readColorValue(style, "fill"), opacity: readPaintOpacityValue(style) }); break; case ".bb-region text": mergeThemeSection(target, "region", { labelColor: readColorValue(style, "fill"), labelFont: readFontValue(style) }); break; case ".bb-bar": mergeThemeSection(target, "shape", { barOpacity: readPaintOpacityValue(style), barStrokeColor: readColorValue(style, "stroke"), barLineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-bar._expanded_": mergeThemeSection(target, "shape", { barExpandedOpacity: readPaintOpacityValue(style) }); break; case ".bb-candlestick": mergeThemeSection(target, "shape", { candlestickStrokeColor: readColorValue(style, "stroke"), candlestickLineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-candlestick._expanded_": mergeThemeSection(target, "shape", { candlestickExpandedOpacity: readPaintOpacityValue(style) }); break; case ".bb-line": mergeThemeSection(target, "shape", { barConnectLineColor: readColorValue(style, "stroke"), barConnectLineWidth: readNumberValue(style, "stroke-width"), lineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-target.bb-focused .bb-line": case ".bb-target.bb-focused path.bb-line": mergeThemeSection(target, "shape", { lineFocusedWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-target.bb-defocused": mergeThemeSection(target, "shape", { targetDefocusedOpacity: readNumberValue(style, "opacity") }); break; case ".bb-area": mergeThemeSection(target, "shape", { areaOpacity: readPaintOpacityValue(style) }); break; case ".bb-circle": mergeThemeSection(target, "shape", { pointFillColor: readColorValue(style, "fill"), pointStrokeColor: readColorValue(style, "stroke"), pointLineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-selected-circle": mergeThemeSection(target, "selectedPoint", { fill: readColorValue(style, "fill"), stroke: readColorValue(style, "stroke"), lineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-circle._expanded_": mergeThemeSection(target, "focusPoint", { fill: readColorValue(style, "fill"), stroke: readColorValue(style, "stroke"), lineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-zoom-brush": mergeThemeSection(target, "zoomBrush", { fill: readColorValue(style, "fill"), opacity: readPaintOpacityValue(style) }); break; case ".bb-brush .extent": case ".bb-brush .selection": mergeThemeSection(target, "subchartBrush", { fill: readColorValue(style, "fill"), opacity: readPaintOpacityValue(style) }); break; case ".bb-brush .handle--custom": mergeThemeSection(target, "subchartBrush", { handleFill: readColorValue(style, "fill"), handleOpacity: readPaintOpacityValue(style), handleStroke: readColorValue(style, "stroke"), handleLineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-chart-treemaps rect": case ".bb-chart-treemaps": mergeThemeSection(target, "treemap", { stroke: readColorValue(style, "stroke"), lineWidth: readNumberValue(style, "stroke-width") }); break; case ".bb-text.bb-empty": case ".bb-text .bb-empty": case ".bb-empty": mergeThemeSection(target, "emptyLabel", { color: readColorValue(style, "fill"), font: readFontValue(style) }); break; case ".bb-text": mergeThemeSection(target, "label", { color: readColorValue(style, "fill"), font: readFontValue(style) }); break; case ".bb-title": mergeThemeSection(target, "title", { color: readColorValue(style, "fill"), font: readFontValue(style) }); } } /** * Convert selector based canvas theme overrides to renderer theme slots. * @param {object} selectors Selector style declarations * @returns {object} Canvas theme override * @private */ function getSelectorThemeOverride(selectors) { const override = {}; Object.keys(selectors ?? {}).forEach((selector) => { normalizeThemeSelectors(selector).forEach((normalizedSelector) => { applySelectorStyle(override, normalizedSelector, selectors[selector]); }); }); return override; } /** * Resolve user canvas theme override. * @param {object} userOverride Canvas theme override * @returns {object} Canvas theme override * @private */ function getThemeOverride(userOverride) { if (!userOverride) return {}; const { selectors, ...styleOverride } = userOverride; return mergeObj({}, getSelectorThemeOverride(selectors), styleOverride); } /** * Build a stable enough cache key for theme reloads. * @param {HTMLElement} container Chart container * @param {object} userOverride Canvas theme override * @returns {string|null} Cache key * @private */ function getThemeCacheKey(container, userOverride) { try { return JSON.stringify({ className: container.className, style: container.getAttribute("style") || "", override: userOverride || null }); } catch { return null; } } /** * Resolve canvas drawing styles from the existing SVG CSS theme. * @private */ var CanvasTheme = class { style; cacheContainer = null; cacheKey = null; gridLineSelectorOverrides = []; directGridOverrideKeys = /* @__PURE__ */ new Set(); /** * Read theme values from temporary SVG probes. * @param {HTMLElement} container Chart container * @param {object} userOverride Canvas theme override * @private */ load(container, userOverride) { const svg = doc.createElementNS(SVG_NS, "svg"); this.gridLineSelectorOverrides = getGridLineSelectorOverrides(userOverride?.selectors); this.directGridOverrideKeys = new Set(Object.keys(userOverride?.grid ?? {})); svg.setAttribute("class", "bb"); svg.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;width:0;height:0;overflow:hidden"; container.appendChild(svg); const read = (el, props) => { const style = win.getComputedStyle(el); const values = {}; for (const prop of props) values[prop] = prop === "font" ? toFont(style, "") : style.getPropertyValue(prop).trim(); return values; }; const probe = (tag, cls, props) => { const el = doc.createElementNS(SVG_NS, tag); el.setAttribute("class", cls); svg.appendChild(el); const value = read(el, props); svg.removeChild(el); return value; }; const probeIn = (parentCls, tag, cls, props) => { const parent = doc.createElementNS(SVG_NS, "g"); const el = doc.createElementNS(SVG_NS, tag); parent.setAttribute("class", parentCls); cls && el.setAttribute("class", cls); parent.appendChild(el); svg.appendChild(parent); const value = read(el, props); svg.removeChild(parent); return value; }; const probeInNested = (parentCls, groupCls, tag, cls, props) => { const parent = doc.createElementNS(SVG_NS, "g"); const group = doc.createElementNS(SVG_NS, "g"); const el = doc.createElementNS(SVG_NS, tag); parent.setAttribute("class", parentCls); group.setAttribute("class", groupCls); cls && el.setAttribute("class", cls); group.appendChild(el); parent.appendChild(group); svg.appendChild(parent); const value = read(el, props); svg.removeChild(parent); return value; }; const probeTick = (axisCls, tag, props, tickCls = "") => { const axis = doc.createElementNS(SVG_NS, "g"); const tick = doc.createElementNS(SVG_NS, "g"); const el = doc.createElementNS(SVG_NS, tag); axis.setAttribute("class", `bb-axis ${axisCls}`); tick.setAttribute("class", ["tick", tickCls].filter(Boolean).join(" ")); el.textContent = tag === "text" ? "0" : ""; tick.appendChild(el); axis.appendChild(tick); svg.appendChild(axis); const value = read(el, props); svg.removeChild(axis); return value; }; const targetPointPaint = "rgb(1, 2, 3)"; const probeTargetPoint = (cls, props) => { const style = doc.createElement("style"); style.textContent = ` .bb-shapes-canvas-probe .bb-circle { fill: ${targetPointPaint}; stroke: ${targetPointPaint}; } `; doc.head.appendChild(style); try { return probeIn("bb-shapes bb-shapes-canvas-probe", "circle", cls, props); } finally { style.remove(); } }; const axisLine = probeIn("bb-axis bb-axis-x", "path", "domain", ["stroke", "stroke-width"]); const tickLine = probeTick("bb-axis-x", "line", ["stroke", "stroke-width"]); const xAxisText = probeTick("bb-axis-x", "text", ["fill", "font"]); const activeAxisText = probeTick("bb-axis-x", "text", ["fill"], "_active_"); const axisText = probeTick("bb-axis-y", "text", ["fill", "font"]); const y2AxisText = probeTick("bb-axis-y2", "text", ["fill", "font"]); const xAxisLabel = probeIn("bb-axis bb-axis-x", "text", "bb-axis-x-label", ["fill", "font"]); const yAxisLabel = probeIn("bb-axis bb-axis-y", "text", "bb-axis-y-label", ["fill", "font"]); const y2AxisLabel = probeIn("bb-axis bb-axis-y2", "text", "bb-axis-y2-label", ["fill", "font"]); const plainCircle = probe("circle", "", [ "fill", "stroke", "stroke-width" ]); const gridLine = probeIn("bb-grid", "line", "bb-ygrid", [ "stroke", "stroke-width", "stroke-dasharray" ]); const gridText = probeIn("bb-grid", "text", "", ["fill", "font"]); const focusGridLine = probeInNested("bb-grid", "bb-xgrid-focus", "line", "bb-xgrid-focus", [ "stroke", "stroke-width", "stroke-dasharray" ]); const emptyLabel = probe("text", "bb-text bb-empty", ["fill", "font"]); const regionRect = probeIn("bb-region", "rect", "", ["fill", "fill-opacity"]); const regionLabel = probeIn("bb-region", "text", "", ["fill", "font"]); const bar = probe("path", "bb-bar", [ "opacity", "fill-opacity", "stroke", "stroke-width" ]); const barExpanded = probe("path", "bb-bar _expanded_", ["opacity", "fill-opacity"]); const candlestick = probe("path", "bb-candlestick", ["stroke", "stroke-width"]); const candlestickExpanded = probe("path", "bb-candlestick _expanded_", ["opacity", "fill-opacity"]); const line = probe("path", "bb-line", ["stroke-width"]); const focusedLine = probeIn("bb-target bb-focused", "path", "bb-line", ["stroke-width"]); const defocusedTarget = probe("g", "bb-target bb-defocused", ["opacity"]); const area = probe("path", "bb-area", ["opacity", "fill-opacity"]); const point = probe("circle", "bb-circle", [ "fill", "stroke", "stroke-width" ]); const selectedPoint = probe("circle", "bb-selected-circle", [ "fill", "stroke", "stroke-width" ]); const focusPoint = probeTargetPoint("bb-circle _expanded_", [ "fill", "stroke", "stroke-width" ]); const zoomBrush = probe("rect", "bb-zoom-brush", [ "fill", "fill-opacity", "opacity" ]); const subchartBrushExtent = probeIn("bb-brush", "rect", "extent", [ "fill", "fill-opacity", "opacity" ]); const subchartBrushSelection = probeIn("bb-brush", "rect", "selection", [ "fill", "fill-opacity", "opacity" ]); const subchartBrushHandle = probeIn("bb-brush", "path", "handle--custom", [ "fill", "fill-opacity", "opacity", "stroke", "stroke-width" ]); const treemap = probeIn("bb-chart-treemaps", "rect", "", ["stroke", "stroke-width"]); const label = probe("text", "bb-text", ["fill", "font"]); const title = probeIn("", "text", "bb-title", ["fill", "font"]); const zoomBrushFill = toColor(zoomBrush.fill, "#000"); const zoomBrushOpacity = toNumber(zoomBrush["fill-opacity"] || zoomBrush.opacity, .1); const subchartBrushFill = toColor(subchartBrushSelection.fill, toColor(subchartBrushExtent.fill, zoomBrushFill)); const subchartBrushOpacity = toOptionalNumber(subchartBrushSelection["fill-opacity"], 1) ?? toOptionalNumber(subchartBrushSelection.opacity, 1) ?? toOptionalNumber(subchartBrushExtent["fill-opacity"], 1) ?? toOptionalNumber(subchartBrushExtent.opacity, 1) ?? zoomBrushOpacity; const subchartBrushHandleStroke = toColor(subchartBrushHandle.stroke, "transparent"); const defaultStyle = { axis: { lineColor: toColor(axisLine.stroke, "#000"), lineWidth: toNumber(axisLine["stroke-width"], 1), tickColor: toColor(tickLine.stroke, toColor(axisLine.stroke, "#000")), tickWidth: toNumber(tickLine["stroke-width"], toNumber(axisLine["stroke-width"], 1)), labelFont: yAxisLabel.font || axisText.font || "10px sans-serif", xTickFont: xAxisText.font || axisText.font || "10px sans-serif", yTickFont: axisText.font || "10px sans-serif", y2TickFont: y2AxisText.font || axisText.font || "10px sans-serif", labelColor: toColor(axisText.fill, "#000"), xLabelColor: toColor(xAxisLabel.fill, toColor(xAxisText.fill, "#000")), yLabelColor: toColor(yAxisLabel.fill, toColor(axisText.fill, "#000")), y2LabelColor: toColor(y2AxisLabel.fill, toColor(yAxisLabel.fill, toColor(axisText.fill, "#000"))), activeLabelColor: toOptionalColor(activeAxisText.fill, xAxisText.fill) || toColor(axisText.fill, "#000") }, grid: { lineColor: toColor(gridLine.stroke, "#aaa"), lineWidth: toNumber(gridLine["stroke-width"], 1), dashArray: toDashArray(gridLine["stroke-dasharray"]), labelFont: gridText.font || axisText.font || "10px sans-serif", labelColor: toColor(gridText.fill, "#aaa") }, focusGrid: { lineColor: toColor(focusGridLine.stroke, toColor(gridLine.stroke, "#aaa")), lineWidth: toNumber(focusGridLine["stroke-width"], toNumber(gridLine["stroke-width"], 1)), dashArray: toDashArray(focusGridLine["stroke-dasharray"] || gridLine["stroke-dasharray"]) }, emptyLabel: { font: emptyLabel.font || label.font || axisText.font || "20px sans-serif", color: toColor(emptyLabel.fill, "#808080") }, region: { fill: toColor(regionRect.fill, "steelblue"), opacity: toNumber(regionRect["fill-opacity"], .1), labelFont: regionLabel.font || axisText.font || "10px sans-serif", labelColor: toColor(regionLabel.fill, "#000") }, shape: { barOpacity: toPaintOpacity(bar, 1), barExpandedOpacity: toNumber(toPaintOpacity(barExpanded, toPaintOpacity(bar, 1)), toPaintOpacity(bar, 1)), barStrokeColor: toColor(bar.stroke, "#000"), barLineWidth: toNumber(bar["stroke-width"], 0), barConnectLineColor: toColor(line.stroke, "#000"), barConnectLineWidth: toNumber(line["stroke-width"], 1), candlestickStrokeColor: toColor(candlestick.stroke, "#000"), candlestickLineWidth: toNumber(candlestick["stroke-width"], 1), candlestickExpandedOpacity: toNumber(toPaintOpacity(candlestickExpanded, 1), 1), lineWidth: toNumber(line["stroke-width"], 1), lineFocusedWidth: toNumber(focusedLine["stroke-width"], 2), areaOpacity: toPaintOpacity(area, .2), targetDefocusedOpacity: toNumber(defocusedTarget.opacity, .3), pointFillColor: toOptionalColor(point.fill, plainCircle.fill), pointStrokeColor: toOptionalColor(point.stroke, plainCircle.stroke), pointLineWidth: toOptionalNumber(point["stroke-width"], plainCircle["stroke-width"]) }, selectedPoint: { fill: toColor(selectedPoint.fill, "#fff"), stroke: toOptionalColor(selectedPoint.stroke), lineWidth: toNumber(selectedPoint["stroke-width"], 2) }, focusPoint: { fill: toOptionalColor(focusPoint.fill, targetPointPaint), stroke: toOptionalColor(focusPoint.stroke, targetPointPaint), lineWidth: toNumber(focusPoint["stroke-width"], 1) }, zoomBrush: { fill: zoomBrushFill, opacity: zoomBrushOpacity }, subchartBrush: { fill: subchartBrushFill, opacity: subchartBrushOpacity, handleFill: toColor(subchartBrushHandle.fill, "transparent"), handleOpacity: toPaintOpacity(subchartBrushHandle, 1), handleStroke: subchartBrushHandleStroke, handleLineWidth: toNumber(subchartBrushHandle["stroke-width"], subchartBrushHandleStroke === "transparent" ? 0 : 1) }, treemap: { stroke: toColor(treemap.stroke, "#fff"), lineWidth: toNumber(treemap["stroke-width"], 1) }, label: { font: label.font || axisText.font || "10px sans-serif", color: toColor(label.fill, "#000") }, title: { font: title.font || label.font || axisText.font || "14px sans-serif", color: toColor(title.fill, "#000") } }; const themeOverride = getThemeOverride(userOverride); const style = mergeObj(defaultStyle, themeOverride); const userAxisOverride = userOverride?.axis; const hasUserAxisLabelColor = !!userAxisOverride && Object.prototype.hasOwnProperty.call(userAxisOverride, "labelColor"); const hasUserAxisLabelFont = !!userAxisOverride && Object.prototype.hasOwnProperty.call(userAxisOverride, "labelFont"); style.axis.lineWidth = toNumber(style.axis.lineWidth, defaultStyle.axis.lineWidth); style.axis.tickWidth = toNumber(style.axis.tickWidth, style.axis.lineWidth); if (hasUserAxisLabelFont) { const axisOverride = userAxisOverride; const labelFont = style.axis.labelFont; axisOverride.xTickFont === void 0 && (style.axis.xTickFont = labelFont); axisOverride.yTickFont === void 0 && (style.axis.yTickFont = labelFont); axisOverride.y2TickFont === void 0 && (style.axis.y2TickFont = labelFont); } style.axis.labelFont = style.axis.labelFont || defaultStyle.axis.labelFont; style.axis.xTickFont = style.axis.xTickFont || style.axis.labelFont; style.axis.yTickFont = style.axis.yTickFont || style.axis.labelFont; style.axis.y2TickFont = style.axis.y2TickFont || style.axis.yTickFont; style.axis.labelColor = toColor(style.axis.labelColor, defaultStyle.axis.labelColor); if (hasUserAxisLabelColor) { const labelColor = style.axis.labelColor; userAxisOverride.xLabelColor === void 0 && (style.axis.xLabelColor = labelColor); userAxisOverride.yLabelColor === void 0 && (style.axis.yLabelColor = labelColor); userAxisOverride.y2LabelColor === void 0 && (style.axis.y2LabelColor = labelColor); } style.axis.xLabelColor = toColor(style.axis.xLabelColor, style.axis.labelColor); style.axis.yLabelColor = toColor(style.axis.yLabelColor, style.axis.labelColor); style.axis.y2LabelColor = toColor(style.axis.y2LabelColor, style.axis.yLabelColor); style.axis.activeLabelColor = toColor(style.axis.activeLabelColor, style.axis.labelColor); style.grid.lineWidth = toNumber(style.grid.lineWidth, defaultStyle.grid.lineWidth); style.grid.dashArray = toDashArray(style.grid.dashArray); style.focusGrid.lineWidth = toNumber(style.focusGrid.lineWidth, defaultStyle.focusGrid.lineWidth); style.focusGrid.dashArray = toDashArray(style.focusGrid.dashArray); style.region.opacity = toNumber(style.region.opacity, defaultStyle.region.opacity); style.shape.barOpacity = toNumber(style.shape.barOpacity, defaultStyle.shape.barOpacity); style.shape.barExpandedOpacity = toNumber(style.shape.barExpandedOpacity, style.shape.barOpacity); style.shape.barLineWidth = toNumber(style.shape.barLineWidth, defaultStyle.shape.barLineWidth); style.shape.barConnectLineWidth = toNumber(style.shape.barConnectLineWidth, defaultStyle.shape.barConnectLineWidth); style.shape.candlestickLineWidth = toNumber(style.shape.candlestickLineWidth, defaultStyle.shape.candlestickLineWidth); style.shape.candlestickStrokeColor = toColor(style.shape.candlestickStrokeColor, defaultStyle.shape.candlestickStrokeColor); style.shape.candlestickExpandedOpacity = toNumber(style.shape.candlestickExpandedOpacity, defaultStyle.shape.candlestickExpandedOpacity); style.shape.lineWidth = toNumber(style.shape.lineWidth, defaultStyle.shape.lineWidth); style.shape.lineFocusedWidth = toNumber(style.shape.lineFocusedWidth, defaultStyle.shape.lineFocusedWidth); style.shape.areaOpacity = toNumber(style.shape.areaOpacity, defaultStyle.shape.areaOpacity); style.shape.targetDefocusedOpacity = toNumber(style.shape.targetDefocusedOpacity, defaultStyle.shape.targetDefocusedOpacity); style.shape.pointLineWidth = toOptionalNumber(style.shape.pointLineWidth); style.selectedPoint.lineWidth = toNumber(style.selectedPoint.lineWidth, defaultStyle.selectedPoint.lineWidth); style.focusPoint.lineWidth = toNumber(style.focusPoint.lineWidth, defaultStyle.focusPoint.lineWidth); style.zoomBrush.opacity = toNumber(style.zoomBrush.opacity, defaultStyle.zoomBrush.opacity); style.subchartBrush.opacity = toNumber(style.subchartBrush.opacity, defaultStyle.subchartBrush.opacity); style.subchartBrush.handleOpacity = toNumber(style.subchartBrush.handleOpacity, defaultStyle.subchartBrush.handleOpacity); style.subchartBrush.handleLineWidth = toNumber(style.subchartBrush.handleLineWidth, defaultStyle.subchartBrush.handleLineWidth); style.treemap.lineWidth = toNumber(style.treemap.lineWidth, defaultStyle.treemap.lineWidth); this.style = style; this.cacheContainer = container; this.cacheKey = getThemeCacheKey(container, userOverride); svg.remove(); } /** * Reload theme values after resize or option changes. * @param {HTMLElement} container Chart container * @param {object} userOverride Canvas theme override * @private */ reload(container, userOverride) { const key = getThemeCacheKey(container, userOverride); if (key !== null && this.style && this.cacheContainer === container && this.cacheKey === key) return; this.load(container, userOverride); } /** * Resolve selector based style for one configured optional grid line. * @param {string} axis Grid axis * @param {object} line Grid line options * @param {string} line.class Grid line custom class * @param {string} target Draw target * @returns {object|undefined} Grid line style override * @private */ getGridLineStyle(axis, line = {}, target = "line") { const style = {}; for (const override of this.gridLineSelectorOverrides) { if (!matchesGridLineSelector(override.selector, axis, line.class, target)) continue; assignDefined(style, override.style, this.directGridOverrideKeys); } return getDefinedKeys(style).length ? style : void 0; } }; //#endregion export { CanvasTheme as default };