UNPKG

apexcharts

Version:

A JavaScript Chart Library

569 lines (492 loc) 16.1 kB
// @ts-check import Graphics from './Graphics' import Utils from '../utils/Utils' import { BrowserAPIs } from '../ssr/BrowserAPIs.js' import { Environment } from '../utils/Environment.js' import { SVGNS } from '../svg/math' /** * ApexCharts Fill Class for setting fill options of the paths. * * @module Fill **/ class Fill { /** * @param {import('../types/internal').ChartStateW} w */ constructor(w) { this.w = w /** @type {any} */ this.opts = null this.seriesIndex = 0 /** @type {any} */ this.patternIDs = [] } /** * @param {Record<string, any>} params */ clippedImgArea(params) { const w = this.w const cnf = w.config const svgW = parseInt(String(w.layout.gridWidth), 10) const svgH = parseInt(String(w.layout.gridHeight), 10) const size = svgW > svgH ? svgW : svgH const fillImg = params.image let imgWidth = 0 let imgHeight = 0 if ( typeof params.width === 'undefined' && typeof params.height === 'undefined' ) { if ( cnf.fill.image.width !== undefined && cnf.fill.image.height !== undefined ) { imgWidth = cnf.fill.image.width + 1 imgHeight = cnf.fill.image.height } else { imgWidth = size + 1 imgHeight = size } } else { imgWidth = params.width imgHeight = params.height } const elPattern = BrowserAPIs.createElementNS(SVGNS, 'pattern') Graphics.setAttrs(elPattern, { id: params.patternID, patternUnits: params.patternUnits ? params.patternUnits : 'userSpaceOnUse', width: imgWidth + 'px', height: imgHeight + 'px', }) const elImage = BrowserAPIs.createElementNS(SVGNS, 'image') elPattern.appendChild(elImage) const SVGLib = Environment.isBrowser() ? /** @type {any} */ (window).SVG : /** @type {any} */ (global).SVG elImage.setAttributeNS(SVGLib.xlink, 'href', fillImg) Graphics.setAttrs(elImage, { x: 0, y: 0, preserveAspectRatio: 'none', width: imgWidth + 'px', height: imgHeight + 'px', }) elImage.style.opacity = params.opacity w.dom.elDefs.node.appendChild(elPattern) } /** * @param {Record<string, any>} opts */ getSeriesIndex(opts) { const w = this.w const cType = w.config.chart.type if ( ((cType === 'bar' || cType === 'rangeBar') && w.config.plotOptions.bar.distributed) || cType === 'heatmap' || cType === 'treemap' ) { this.seriesIndex = opts.seriesNumber } else { this.seriesIndex = opts.seriesNumber % w.seriesData.series.length } return this.seriesIndex } /** * The y-axis window a series is plotted against. This is the space the * threshold gradient is measured in, since that gradient spans the plot area. * * @param {number} realIndex */ getSeriesAxisRange(realIndex) { const w = this.w const yaxisIndex = w.globals.seriesYAxisReverseMap?.[realIndex] ?? 0 return { minY: Utils.isNumber(w.globals.minYArr[realIndex]) ? w.globals.minYArr[realIndex] : w.globals.minY, maxY: Utils.isNumber(w.globals.maxYArr[realIndex]) ? w.globals.maxYArr[realIndex] : w.globals.maxY, reversed: !!w.config.yaxis[yaxisIndex]?.reversed, } } /** * Builds the two stops that split a series into an above- and a * below-threshold color. * * The stops are painted into a vertical gradient anchored to the plot area * (see Graphics.drawGradient's verticalUserSpace branch), so the boundary is * positioned over the *axis* range. Deriving it from the data range instead * puts the transition on the wrong value whenever the axis extends past the * data, which is the common case: an explicit yaxis.min/max, a nice scale, or * an axis shared with another series. * * @param {{ minY: number, maxY: number, reversed?: boolean }} axisRange * @param {Record<string, any>} multiColorConfig */ computeColorStops(axisRange, multiColorConfig) { const w = this.w const { threshold, colorAboveThreshold, colorBelowThreshold } = multiColorConfig const { minY, maxY } = axisRange const span = maxY - minY // A flat axis has no interior to split: the series sits wholly on one side // of the threshold. let offset = span === 0 ? (threshold > maxY ? 0 : 100) : ((maxY - threshold) / span) * 100 // A reversed axis puts the low values at the top, mirroring the boundary. if (axisRange.reversed) { offset = 100 - offset } offset = Math.max(0, Math.min(offset, 100)) // fill.opacity may be a per-series array (as elsewhere in fillPath); index // it, or the stop opacity becomes the whole array -> stop-opacity="0.9,0.8", // an invalid value the browser ignores (renders at full opacity). const fillOpacity = Array.isArray(w.config.fill.opacity) ? w.config.fill.opacity[this.seriesIndex] : w.config.fill.opacity const above = { offset, color: colorAboveThreshold, opacity: fillOpacity, } const below = { offset, color: colorBelowThreshold, opacity: fillOpacity, } // Both stops share an offset, giving a hard transition there: the band // before it takes the first color, the band after it the second. Emitting // them in ascending order keeps this off the SVG rule that silently clamps // an out-of-order offset up to its predecessor. return axisRange.reversed ? [below, above] : [above, below] } /** * @param {Record<string, any>} opts */ fillPath(opts) { const w = this.w this.opts = opts const cnf = this.w.config let pathFill let patternFill, gradientFill this.seriesIndex = this.getSeriesIndex(opts) const drawMultiColorLine = cnf.plotOptions.line.colors.colorAboveThreshold && cnf.plotOptions.line.colors.colorBelowThreshold const fillColors = this.getFillColors() let fillColor = fillColors[this.seriesIndex] //override fillcolor if user inputted color with data if (w.seriesData.seriesColors[this.seriesIndex] !== undefined) { fillColor = w.seriesData.seriesColors[this.seriesIndex] } if (typeof fillColor === 'function') { fillColor = fillColor({ seriesIndex: this.seriesIndex, dataPointIndex: opts.dataPointIndex, value: opts.value, w, }) } const fillType = opts.fillType ? opts.fillType : this.getFillType(this.seriesIndex) let fillOpacity = Array.isArray(cnf.fill.opacity) ? cnf.fill.opacity[this.seriesIndex] : cnf.fill.opacity // when line colors needs to be different based on values, we use gradient config to achieve this const useGradient = fillType === 'gradient' || drawMultiColorLine if (opts.color) { fillColor = opts.color } const seriesItem = /** @type {Record<string,any>} */ (w.config.series[this.seriesIndex]) if (seriesItem?.data?.[opts.dataPointIndex]?.fillColor) { fillColor = seriesItem?.data?.[opts.dataPointIndex]?.fillColor } // in case a color is undefined, fallback to white color to prevent runtime error if (!fillColor) { fillColor = '#fff' console.warn('undefined color - ApexCharts') } if (opts.opacity !== undefined && opts.opacity !== null) fillOpacity = opts.opacity let defaultColor = fillColor if (Utils.isCSSVariable(fillColor)) { defaultColor = Utils.applyOpacityToColor(fillColor, fillOpacity) } else if (fillColor.indexOf('rgb') === -1) { if (fillColor.indexOf('#') === -1) { defaultColor = fillColor } else if (fillColor.length < 9) { defaultColor = Utils.hexToRgba(fillColor, fillOpacity) } } else { if (fillColor.indexOf('rgba') > -1) { fillOpacity = Utils.getOpacityFromRGBA(fillColor) } else { defaultColor = Utils.hexToRgba(Utils.rgb2hex(fillColor), fillOpacity) } } // Gradients and patterns require resolved color values (not CSS variables) const resolvedFillColor = Utils.isCSSVariable(fillColor) ? Utils.getThemeColor(fillColor) : fillColor if (fillType === 'pattern') { patternFill = this.handlePatternFill({ fillConfig: opts.fillConfig, patternFill, fillColor: resolvedFillColor, defaultColor, }) } if (useGradient) { const colorStops = cnf.fill.gradient.colorStops ? [...cnf.fill.gradient.colorStops] : [] let type = cnf.fill.gradient.type if (drawMultiColorLine) { colorStops[this.seriesIndex] = this.computeColorStops( this.getSeriesAxisRange(this.seriesIndex), cnf.plotOptions.line.colors, ) type = 'vertical' } gradientFill = this.handleGradientFill({ type, fillConfig: opts.fillConfig, fillColor: resolvedFillColor, fillOpacity, colorStops, i: this.seriesIndex, // Threshold stops are positioned over the axis range, so the gradient // has to be anchored to the plot area to match. This also keeps every // segment of a null-split series on one shared coordinate space. verticalUserSpace: drawMultiColorLine, }) } if (fillType === 'image') { const imgSrc = cnf.fill.image.src const patternID = opts.patternID ? opts.patternID : '' const patternKey = `pattern${w.globals.cuid}${ opts.seriesNumber + 1 }${patternID}` if (this.patternIDs.indexOf(patternKey) === -1) { this.clippedImgArea({ opacity: fillOpacity, image: Array.isArray(imgSrc) ? opts.seriesNumber < imgSrc.length ? imgSrc[opts.seriesNumber] : imgSrc[0] : imgSrc, width: opts.width ? opts.width : undefined, height: opts.height ? opts.height : undefined, patternUnits: opts.patternUnits, patternID: patternKey, }) this.patternIDs.push(patternKey) } pathFill = `url(#${patternKey})` } else if (useGradient) { pathFill = gradientFill } else if (fillType === 'pattern') { pathFill = patternFill } else { pathFill = defaultColor } // override pattern/gradient if opts.solid is true if (opts.solid) { pathFill = defaultColor } return pathFill } /** * @param {number} seriesIndex */ getFillType(seriesIndex) { const w = this.w if (Array.isArray(w.config.fill.type)) { return w.config.fill.type[seriesIndex] } else { return w.config.fill.type } } getFillColors() { const w = this.w const cnf = w.config const opts = this.opts let fillColors = [] if (w.globals.comboCharts) { if ( /** @type {Record<string,any>} */ (w.config.series[this.seriesIndex]).type === 'line' ) { if (Array.isArray(w.globals.stroke.colors)) { fillColors = w.globals.stroke.colors } else { fillColors.push(w.globals.stroke.colors) } } else { if (Array.isArray(w.globals.fill.colors)) { fillColors = w.globals.fill.colors } else { fillColors.push(w.globals.fill.colors) } } } else { if (cnf.chart.type === 'line') { if (Array.isArray(w.globals.stroke.colors)) { fillColors = w.globals.stroke.colors } else { fillColors.push(w.globals.stroke.colors) } } else { if (Array.isArray(w.globals.fill.colors)) { fillColors = w.globals.fill.colors } else { fillColors.push(w.globals.fill.colors) } } } // colors passed in arguments if (typeof opts.fillColors !== 'undefined') { fillColors = [] if (Array.isArray(opts.fillColors)) { fillColors = opts.fillColors.slice() } else { fillColors.push(opts.fillColors) } } return fillColors } /** @param {{fillConfig: any, patternFill: any, fillColor: any, defaultColor: any}} opts */ handlePatternFill({ fillConfig, patternFill, fillColor, defaultColor }) { let fillCnf = this.w.config.fill if (fillConfig) { fillCnf = fillConfig } const opts = this.opts const graphics = new Graphics(this.w) const patternStrokeWidth = Array.isArray(fillCnf.pattern.strokeWidth) ? fillCnf.pattern.strokeWidth[this.seriesIndex] : fillCnf.pattern.strokeWidth const patternLineColor = fillColor if (Array.isArray(fillCnf.pattern.style)) { if (typeof fillCnf.pattern.style[opts.seriesNumber] !== 'undefined') { const pf = graphics.drawPattern( fillCnf.pattern.style[opts.seriesNumber], fillCnf.pattern.width, fillCnf.pattern.height, patternLineColor, patternStrokeWidth, ) patternFill = pf } else { patternFill = defaultColor } } else { patternFill = graphics.drawPattern( fillCnf.pattern.style, fillCnf.pattern.width, fillCnf.pattern.height, patternLineColor, patternStrokeWidth, ) } return patternFill } handleGradientFill( /** @type {any} */ { type, fillColor, fillOpacity, fillConfig, colorStops, i, verticalUserSpace = false, }, ) { let fillCnf = this.w.config.fill if (fillConfig) { fillCnf = { ...fillCnf, ...fillConfig, } } const opts = this.opts const graphics = new Graphics(this.w) const utils = new Utils() type = type || fillCnf.gradient.type let gradientFrom = fillColor let gradientTo let opacityFrom = fillCnf.gradient.opacityFrom === undefined ? fillOpacity : Array.isArray(fillCnf.gradient.opacityFrom) ? fillCnf.gradient.opacityFrom[i] : fillCnf.gradient.opacityFrom if (gradientFrom.indexOf('rgba') > -1) { opacityFrom = Utils.getOpacityFromRGBA(gradientFrom) } let opacityTo = fillCnf.gradient.opacityTo === undefined ? fillOpacity : Array.isArray(fillCnf.gradient.opacityTo) ? fillCnf.gradient.opacityTo[i] : fillCnf.gradient.opacityTo if ( fillCnf.gradient.gradientToColors === undefined || fillCnf.gradient.gradientToColors.length === 0 ) { if (fillCnf.gradient.shade === 'dark') { gradientTo = utils.shadeColor( parseFloat(fillCnf.gradient.shadeIntensity) * -1, fillColor.indexOf('rgb') > -1 ? Utils.rgb2hex(fillColor) : fillColor, ) } else { gradientTo = utils.shadeColor( parseFloat(fillCnf.gradient.shadeIntensity), fillColor.indexOf('rgb') > -1 ? Utils.rgb2hex(fillColor) : fillColor, ) } } else { if (fillCnf.gradient.gradientToColors[opts.seriesNumber]) { const gToColor = fillCnf.gradient.gradientToColors[opts.seriesNumber] gradientTo = gToColor if (gToColor.indexOf('rgba') > -1) { opacityTo = Utils.getOpacityFromRGBA(gToColor) } } else { gradientTo = fillColor } } if (fillCnf.gradient.gradientFrom) { gradientFrom = fillCnf.gradient.gradientFrom } if (fillCnf.gradient.gradientTo) { gradientTo = fillCnf.gradient.gradientTo } if (fillCnf.gradient.inverseColors) { const t = gradientFrom gradientFrom = gradientTo gradientTo = t } if (gradientFrom.indexOf('rgb') > -1) { gradientFrom = Utils.rgb2hex(gradientFrom) } if (gradientTo.indexOf('rgb') > -1) { gradientTo = Utils.rgb2hex(gradientTo) } return graphics.drawGradient( type, gradientFrom, gradientTo, opacityFrom, opacityTo, opts.size, fillCnf.gradient.stops, colorStops, i, verticalUserSpace, ) } } export default Fill