apexcharts
Version:
A JavaScript Chart Library
542 lines (466 loc) • 14.2 kB
JavaScript
// @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
}
/**
* @param {number[]} data
* @param {Record<string, any>} multiColorConfig
*/
computeColorStops(data, multiColorConfig) {
const w = this.w
let maxPositive = null
let minNegative = null
for (const value of data) {
if (value >= multiColorConfig.threshold) {
if (maxPositive === null || value > maxPositive) {
maxPositive = value
}
} else {
if (minNegative === null || value < minNegative) {
minNegative = value
}
}
}
if (maxPositive === null) {
maxPositive = multiColorConfig.threshold
}
if (minNegative === null) {
minNegative = multiColorConfig.threshold
}
let totalRange =
maxPositive -
multiColorConfig.threshold +
(multiColorConfig.threshold - minNegative)
if (totalRange === 0) {
totalRange = 1
}
const negativePercentage =
((multiColorConfig.threshold - minNegative) / totalRange) * 100
let offset = 100 - negativePercentage
offset = Math.max(0, Math.min(offset, 100))
return [
{
offset: offset,
color: multiColorConfig.colorAboveThreshold,
opacity: w.config.fill.opacity,
},
{
offset: 0,
color: multiColorConfig.colorBelowThreshold,
opacity: w.config.fill.opacity,
},
]
}
/**
* @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(
w.seriesData.series[this.seriesIndex],
cnf.plotOptions.line.colors,
)
type = 'vertical'
}
gradientFill = this.handleGradientFill({
type,
fillConfig: opts.fillConfig,
fillColor: resolvedFillColor,
fillOpacity,
colorStops,
i: this.seriesIndex,
})
}
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,
},
) {
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,
)
}
}
export default Fill