billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
1,167 lines (1,075 loc) • 37.3 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {document, window} from "../module/browser";
import {mergeObj} from "../module/util";
/**
* Resolved canvas drawing style.
* @private
*/
export interface CanvasThemeStyle {
axis: {
lineColor: string,
lineWidth: number,
tickColor: string,
tickWidth: number,
labelFont: string,
xTickFont: string,
yTickFont: string,
y2TickFont: string,
labelColor: string,
xLabelColor: string,
yLabelColor: string,
y2LabelColor: string,
activeLabelColor: string
};
grid: {
lineColor: string,
lineWidth: number,
dashArray: number[],
labelFont: string,
labelColor: string
};
focusGrid: {lineColor: string, lineWidth: number, dashArray: number[]};
emptyLabel: {font: string, color: string};
region: {fill: string, opacity: number, labelFont: string, labelColor: string};
shape: {
barOpacity: number,
barExpandedOpacity: number,
barStrokeColor: string,
barLineWidth: number,
barConnectLineColor: string,
barConnectLineWidth: number,
candlestickStrokeColor: string,
candlestickLineWidth: number,
candlestickExpandedOpacity: number,
lineWidth: number,
lineFocusedWidth: number,
areaOpacity: number,
targetDefocusedOpacity: number,
pointFillColor?: string,
pointStrokeColor?: string,
pointLineWidth?: number
};
selectedPoint: {fill: string, stroke?: string, lineWidth: number};
focusPoint: {fill?: string, stroke?: string, lineWidth: number};
zoomBrush: {fill: string, opacity: number};
subchartBrush: {
fill: string,
opacity: number,
handleFill: string,
handleOpacity: number,
handleStroke: string,
handleLineWidth: number
};
treemap: {stroke: string, lineWidth: number};
label: {font: string, color: string};
title: {font: string, color: string};
}
type CanvasThemeDeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? CanvasThemeDeepPartial<T[P]> : T[P];
};
export type CanvasThemeSelectorStyle = Record<
string,
string | number | number[] | null | undefined
>;
export type CanvasThemeSelectorMap = Record<string, CanvasThemeSelectorStyle>;
export type CanvasThemeOverride = CanvasThemeDeepPartial<CanvasThemeStyle> & {
selectors?: CanvasThemeSelectorMap
};
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: string | number | null | undefined, fallback: number): number {
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: Record<string, string>, fallback: number): number {
const opacity = toNumber(value.opacity, 1);
const fillOpacity = toNumber(value["fill-opacity"], 1);
const result = opacity * fillOpacity;
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: string | null | undefined, fallback: string): string {
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: string | null | undefined,
ignored?: string | null | undefined
): string | undefined {
const color = toColor(value, "");
const ignoredColor = ignored ? toColor(ignored, "") : "";
return color && color !== ignoredColor ? color : undefined;
}
/**
* 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: string | number | null | undefined,
ignored?: string | number | null | undefined
): number | undefined {
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 :
undefined;
}
/**
* Parse CSS dash array value.
* @param {string|Array} value CSS stroke-dasharray value
* @returns {Array} Dash array
* @private
*/
function toDashArray(value: string | number[] | null | undefined): number[] {
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: CSSStyleDeclaration, fallback: string): string {
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: CanvasThemeSelectorStyle,
prop: string
): string | number | number[] | null | undefined {
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: CanvasThemeSelectorStyle, prop: string): string | undefined {
const value = getStyleValue(style, prop);
const color = value == null ? "" : toColor(String(value), "");
return color || undefined;
}
/**
* 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: CanvasThemeSelectorStyle, prop: string): number | undefined {
const value = getStyleValue(style, prop);
const parsed = typeof value === "number" ? value : parseFloat(String(value ?? ""));
return Number.isFinite(parsed) ? parsed : undefined;
}
/**
* Read paint opacity from selector style declaration.
* @param {object} style Style declaration object
* @returns {number|undefined} Paint opacity
* @private
*/
function readPaintOpacityValue(style: CanvasThemeSelectorStyle): number | undefined {
const opacity = readNumberValue(style, "opacity");
const fillOpacity = readNumberValue(style, "fill-opacity");
return opacity === undefined && fillOpacity === undefined ?
undefined :
(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: CanvasThemeSelectorStyle): number[] | undefined {
const value = getStyleValue(style, "stroke-dasharray");
const dashArray = value == null ? [] : toDashArray(value as string | number[]);
return dashArray.length ? dashArray : undefined;
}
/**
* Read font shorthand from selector style declaration.
* @param {object} style Style declaration object
* @returns {string|undefined} Font shorthand
* @private
*/
function readFontValue(style: CanvasThemeSelectorStyle): string | undefined {
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 undefined;
}
const size = String(fontSize).trim();
const family = String(fontFamily).trim();
if (!size || !family) {
return undefined;
}
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: CanvasThemeDeepPartial<CanvasThemeStyle>,
section: keyof CanvasThemeStyle,
values: Record<string, any>
): void {
const override = Object.keys(values).reduce((acc, key) => {
values[key] !== undefined && (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: string): string[] {
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);
}
/**
* 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: CanvasThemeDeepPartial<CanvasThemeStyle>,
selector: string,
style: CanvasThemeSelectorStyle
): void {
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)
});
break;
}
}
/**
* 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?: CanvasThemeSelectorMap
): CanvasThemeDeepPartial<CanvasThemeStyle> {
const override: CanvasThemeDeepPartial<CanvasThemeStyle> = {};
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?: CanvasThemeOverride
): CanvasThemeDeepPartial<CanvasThemeStyle> {
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: HTMLElement,
userOverride?: CanvasThemeOverride
): string | null {
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
*/
export default class CanvasTheme {
public style!: CanvasThemeStyle;
private cacheContainer: HTMLElement | null = null;
private cacheKey: string | null = null;
/**
* Read theme values from temporary SVG probes.
* @param {HTMLElement} container Chart container
* @param {object} userOverride Canvas theme override
* @private
*/
load(container: HTMLElement, userOverride?: CanvasThemeOverride): void {
const svg = document.createElementNS(SVG_NS, "svg");
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: SVGElement, props: string[]): Record<string, string> => {
const style = window.getComputedStyle(el);
const values: Record<string, string> = {};
for (const prop of props) {
values[prop] = prop === "font" ?
toFont(style, "") :
style.getPropertyValue(prop).trim();
}
return values;
};
const probe = (tag: string, cls: string, props: string[]): Record<string, string> => {
const el = document.createElementNS(SVG_NS, tag);
el.setAttribute("class", cls);
svg.appendChild(el);
const value = read(el, props);
svg.removeChild(el);
return value;
};
const probeIn = (
parentCls: string,
tag: string,
cls: string,
props: string[]
): Record<string, string> => {
const parent = document.createElementNS(SVG_NS, "g");
const el = document.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: string,
groupCls: string,
tag: string,
cls: string,
props: string[]
): Record<string, string> => {
const parent = document.createElementNS(SVG_NS, "g");
const group = document.createElementNS(SVG_NS, "g");
const el = document.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: string,
tag: string,
props: string[],
tickCls = ""
): Record<string, string> => {
const axis = document.createElementNS(SVG_NS, "g");
const tick = document.createElementNS(SVG_NS, "g");
const el = document.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: string,
props: string[]
): Record<string, string> => {
const style = document.createElement("style");
style.textContent = `
.bb-shapes-canvas-probe .bb-circle {
fill: ${targetPointPaint};
stroke: ${targetPointPaint};
}
`;
document.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, 0.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"], 0.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, 0.2),
targetDefocusedOpacity: toNumber(defocusedTarget.opacity, 0.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 as
| CanvasThemeDeepPartial<CanvasThemeStyle["axis"]>
| undefined;
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 === undefined && (style.axis.xTickFont = labelFont);
axisOverride.yTickFont === undefined && (style.axis.yTickFont = labelFont);
axisOverride.y2TickFont === undefined && (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 === undefined && (style.axis.xLabelColor = labelColor);
userAxisOverride.yLabelColor === undefined && (style.axis.yLabelColor = labelColor);
userAxisOverride.y2LabelColor === undefined && (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: HTMLElement, userOverride?: CanvasThemeOverride): void {
const key = getThemeCacheKey(container, userOverride);
if (
key !== null &&
this.style &&
this.cacheContainer === container &&
this.cacheKey === key
) {
return;
}
this.load(container, userOverride);
}
}