feat:Added piechart in Dashboard

This commit is contained in:
2025-02-22 15:35:48 +05:30
parent 357071b967
commit f7cb1af2c4
384 changed files with 112765 additions and 8 deletions
+779
View File
@@ -0,0 +1,779 @@
import Annotations from './modules/annotations/Annotations'
import Base from './modules/Base'
import CoreUtils from './modules/CoreUtils'
import DataLabels from './modules/DataLabels'
import Defaults from './modules/settings/Defaults'
import Exports from './modules/Exports'
import Grid from './modules/axes/Grid'
import Markers from './modules/Markers'
import Range from './modules/Range'
import Utils from './utils/Utils'
import XAxis from './modules/axes/XAxis'
import YAxis from './modules/axes/YAxis'
import InitCtxVariables from './modules/helpers/InitCtxVariables'
import Destroy from './modules/helpers/Destroy'
import { addResizeListener, removeResizeListener } from './utils/Resize'
import apexCSS from './assets/apexcharts.css'
/**
*
* @module ApexCharts
**/
export default class ApexCharts {
constructor(el, opts) {
this.opts = opts
this.ctx = this
// Pass the user supplied options to the Base Class where these options will be extended with defaults. The returned object from Base Class will become the config object in the entire codebase.
this.w = new Base(opts).init()
this.el = el
this.w.globals.cuid = Utils.randomId()
this.w.globals.chartID = this.w.config.chart.id
? Utils.escapeString(this.w.config.chart.id)
: this.w.globals.cuid
const initCtx = new InitCtxVariables(this)
initCtx.initModules()
this.create = Utils.bind(this.create, this)
this.windowResizeHandler = this._windowResizeHandler.bind(this)
this.parentResizeHandler = this._parentResizeCallback.bind(this)
}
/**
* The primary method user will call to render the chart.
*/
render() {
// main method
return new Promise((resolve, reject) => {
// only draw chart, if element found
if (this.el !== null) {
if (typeof Apex._chartInstances === 'undefined') {
Apex._chartInstances = []
}
if (this.w.config.chart.id) {
Apex._chartInstances.push({
id: this.w.globals.chartID,
group: this.w.config.chart.group,
chart: this,
})
}
// set the locale here
this.setLocale(this.w.config.chart.defaultLocale)
const beforeMount = this.w.config.chart.events.beforeMount
if (typeof beforeMount === 'function') {
beforeMount(this, this.w)
}
this.events.fireEvent('beforeMount', [this, this.w])
window.addEventListener('resize', this.windowResizeHandler)
addResizeListener(this.el.parentNode, this.parentResizeHandler)
// Add CSS if not already added
if (!this.css) {
let rootNode = this.el.getRootNode && this.el.getRootNode()
let inShadowRoot = Utils.is('ShadowRoot', rootNode)
let doc = this.el.ownerDocument
let globalCSS = doc.getElementById('apexcharts-css')
if (inShadowRoot || !globalCSS) {
this.css = document.createElement('style')
this.css.id = 'apexcharts-css'
this.css.textContent = apexCSS
const nonce = this.opts.chart?.nonce || this.w.config.chart.nonce;
if (nonce) {
this.css.setAttribute('nonce', nonce);
}
if (inShadowRoot) {
// We are in Shadow DOM, add to shadow root
rootNode.prepend(this.css)
} else {
// Add to <head> of element's document
doc.head.appendChild(this.css)
}
}
}
let graphData = this.create(this.w.config.series, {})
if (!graphData) return resolve(this)
this.mount(graphData)
.then(() => {
if (typeof this.w.config.chart.events.mounted === 'function') {
this.w.config.chart.events.mounted(this, this.w)
}
this.events.fireEvent('mounted', [this, this.w])
resolve(graphData)
})
.catch((e) => {
reject(e)
// handle error in case no data or element not found
})
} else {
reject(new Error('Element not found'))
}
})
}
create(ser, opts) {
let w = this.w
const initCtx = new InitCtxVariables(this)
initCtx.initModules()
let gl = this.w.globals
gl.noData = false
gl.animationEnded = false
this.responsive.checkResponsiveConfig(opts)
if (w.config.xaxis.convertedCatToNumeric) {
const defaults = new Defaults(w.config)
defaults.convertCatToNumericXaxis(w.config, this.ctx)
}
if (this.el === null) {
gl.animationEnded = true
return null
}
this.core.setupElements()
if (w.config.chart.type === 'treemap') {
w.config.grid.show = false
w.config.yaxis[0].show = false
}
if (gl.svgWidth === 0) {
// if the element is hidden, skip drawing
gl.animationEnded = true
return null
}
const combo = CoreUtils.checkComboSeries(ser)
gl.comboCharts = combo.comboCharts
gl.comboBarCount = combo.comboBarCount
const allSeriesAreEmpty = ser.every((s) => s.data && s.data.length === 0)
if (ser.length === 0 || allSeriesAreEmpty) {
this.series.handleNoData()
}
this.events.setupEventHandlers()
// Handle the data inputted by user and set some of the global variables (for eg, if data is datetime / numeric / category). Don't calculate the range / min / max at this time
this.data.parseData(ser)
// this is a good time to set theme colors first
this.theme.init()
// as markers accepts array, we need to setup global markers for easier access
const markers = new Markers(this)
markers.setGlobalMarkerSize()
// labelFormatters should be called before dimensions as in dimensions we need text labels width
this.formatters.setLabelFormatters()
this.titleSubtitle.draw()
// legend is calculated here before coreCalculations because it affects the plottable area
// if there is some data to show or user collapsed all series, then proceed drawing legend
if (
!gl.noData ||
gl.collapsedSeries.length === gl.series.length ||
w.config.legend.showForSingleSeries
) {
this.legend.init()
}
// check whether in multiple series, all series share the same X
this.series.hasAllSeriesEqualX()
// coreCalculations will give the min/max range and yaxis/axis values. It should be called here to set series variable from config to globals
if (gl.axisCharts) {
this.core.coreCalculations()
if (w.config.xaxis.type !== 'category') {
// as we have minX and maxX values, determine the default DateTimeFormat for time series
this.formatters.setLabelFormatters()
}
this.ctx.toolbar.minX = w.globals.minX
this.ctx.toolbar.maxX = w.globals.maxX
}
// we need to generate yaxis for heatmap separately as we are not showing numerics there, but seriesNames. There are some tweaks which are required for heatmap to align labels correctly which are done in below function
// Also we need to do this before calculating Dimensions plotCoords() method of Dimensions
this.formatters.heatmapLabelFormatters()
// get the largest marker size which will be needed in dimensions calc
const coreUtils = new CoreUtils(this)
coreUtils.getLargestMarkerSize()
// We got plottable area here, next task would be to calculate axis areas
this.dimensions.plotCoords()
const xyRatios = this.core.xySettings()
this.grid.createGridMask()
const elGraph = this.core.plotChartType(ser, xyRatios)
const dataLabels = new DataLabels(this)
dataLabels.bringForward()
if (w.config.dataLabels.background.enabled) {
dataLabels.dataLabelsBackground()
}
// after all the drawing calculations, shift the graphical area (actual charts/bars) excluding legends
this.core.shiftGraphPosition()
const dim = {
plot: {
left: w.globals.translateX,
top: w.globals.translateY,
width: w.globals.gridWidth,
height: w.globals.gridHeight,
},
}
return {
elGraph,
xyRatios,
dimensions: dim,
}
}
mount(graphData = null) {
let me = this
let w = me.w
return new Promise((resolve, reject) => {
// no data to display
if (me.el === null) {
return reject(
new Error('Not enough data to display or target element not found')
)
} else if (graphData === null || w.globals.allSeriesCollapsed) {
me.series.handleNoData()
}
me.grid = new Grid(me)
let elgrid = me.grid.drawGrid()
me.annotations = new Annotations(me)
me.annotations.drawImageAnnos()
me.annotations.drawTextAnnos()
if (w.config.grid.position === 'back') {
if (elgrid) {
w.globals.dom.elGraphical.add(elgrid.el)
}
if (elgrid?.elGridBorders?.node) {
w.globals.dom.elGraphical.add(elgrid.elGridBorders)
}
}
if (Array.isArray(graphData.elGraph)) {
for (let g = 0; g < graphData.elGraph.length; g++) {
w.globals.dom.elGraphical.add(graphData.elGraph[g])
}
} else {
w.globals.dom.elGraphical.add(graphData.elGraph)
}
if (w.config.grid.position === 'front') {
if (elgrid) {
w.globals.dom.elGraphical.add(elgrid.el)
}
if (elgrid?.elGridBorders?.node) {
w.globals.dom.elGraphical.add(elgrid.elGridBorders)
}
}
if (w.config.xaxis.crosshairs.position === 'front') {
me.crosshairs.drawXCrosshairs()
}
if (w.config.yaxis[0].crosshairs.position === 'front') {
me.crosshairs.drawYCrosshairs()
}
if (w.config.chart.type !== 'treemap') {
me.axes.drawAxis(w.config.chart.type, elgrid)
}
let xAxis = new XAxis(this.ctx, elgrid)
let yaxis = new YAxis(this.ctx, elgrid)
if (elgrid !== null) {
xAxis.xAxisLabelCorrections(elgrid.xAxisTickWidth)
yaxis.setYAxisTextAlignments()
w.config.yaxis.map((yaxe, index) => {
if (w.globals.ignoreYAxisIndexes.indexOf(index) === -1) {
yaxis.yAxisTitleRotate(index, yaxe.opposite)
}
})
}
me.annotations.drawAxesAnnotations()
if (!w.globals.noData) {
// draw tooltips at the end
if (w.config.tooltip.enabled && !w.globals.noData) {
me.w.globals.tooltip.drawTooltip(graphData.xyRatios)
}
if (
w.globals.axisCharts &&
(w.globals.isXNumeric ||
w.config.xaxis.convertedCatToNumeric ||
w.globals.isRangeBar)
) {
if (
w.config.chart.zoom.enabled ||
(w.config.chart.selection && w.config.chart.selection.enabled) ||
(w.config.chart.pan && w.config.chart.pan.enabled)
) {
me.zoomPanSelection.init({
xyRatios: graphData.xyRatios,
})
}
} else {
const tools = w.config.chart.toolbar.tools
let toolsArr = [
'zoom',
'zoomin',
'zoomout',
'selection',
'pan',
'reset',
]
toolsArr.forEach((t) => {
tools[t] = false
})
}
if (w.config.chart.toolbar.show && !w.globals.allSeriesCollapsed) {
me.toolbar.createToolbar()
}
}
if (w.globals.memory.methodsToExec.length > 0) {
w.globals.memory.methodsToExec.forEach((fn) => {
fn.method(fn.params, false, fn.context)
})
}
if (!w.globals.axisCharts && !w.globals.noData) {
me.core.resizeNonAxisCharts()
}
resolve(me)
})
}
/**
* Destroy the chart instance by removing all elements which also clean up event listeners on those elements.
*/
destroy() {
window.removeEventListener('resize', this.windowResizeHandler)
removeResizeListener(this.el.parentNode, this.parentResizeHandler)
// remove the chart's instance from the global Apex._chartInstances
const chartID = this.w.config.chart.id
if (chartID) {
Apex._chartInstances.forEach((c, i) => {
if (c.id === Utils.escapeString(chartID)) {
Apex._chartInstances.splice(i, 1)
}
})
}
new Destroy(this.ctx).clear({ isUpdating: false })
}
/**
* Allows users to update Options after the chart has rendered.
*
* @param {object} options - A new config object can be passed which will be merged with the existing config object
* @param {boolean} redraw - should redraw from beginning or should use existing paths and redraw from there
* @param {boolean} animate - should animate or not on updating Options
*/
updateOptions(
options,
redraw = false,
animate = true,
updateSyncedCharts = true,
overwriteInitialConfig = true
) {
const w = this.w
// when called externally, clear some global variables
// fixes apexcharts.js#1488
w.globals.selection = undefined
if (options.series) {
this.series.resetSeries(false, true, false)
if (options.series.length && options.series[0].data) {
options.series = options.series.map((s, i) => {
return this.updateHelpers._extendSeries(s, i)
})
}
// user updated the series via updateOptions() function.
// Hence, we need to reset axis min/max to avoid zooming issues
this.updateHelpers.revertDefaultAxisMinMax()
}
// user has set x-axis min/max externally - hence we need to forcefully set the xaxis min/max
if (options.xaxis) {
options = this.updateHelpers.forceXAxisUpdate(options)
}
if (options.yaxis) {
options = this.updateHelpers.forceYAxisUpdate(options)
}
if (w.globals.collapsedSeriesIndices.length > 0) {
this.series.clearPreviousPaths()
}
/* update theme mode#459 */
if (options.theme) {
options = this.theme.updateThemeOptions(options)
}
return this.updateHelpers._updateOptions(
options,
redraw,
animate,
updateSyncedCharts,
overwriteInitialConfig
)
}
/**
* Allows users to update Series after the chart has rendered.
*
* @param {array} series - New series which will override the existing
*/
updateSeries(newSeries = [], animate = true, overwriteInitialSeries = true) {
this.series.resetSeries(false)
this.updateHelpers.revertDefaultAxisMinMax()
return this.updateHelpers._updateSeries(
newSeries,
animate,
overwriteInitialSeries
)
}
/**
* Allows users to append a new series after the chart has rendered.
*
* @param {array} newSerie - New serie which will be appended to the existing series
*/
appendSeries(newSerie, animate = true, overwriteInitialSeries = true) {
const newSeries = this.w.config.series.slice()
newSeries.push(newSerie)
this.series.resetSeries(false)
this.updateHelpers.revertDefaultAxisMinMax()
return this.updateHelpers._updateSeries(
newSeries,
animate,
overwriteInitialSeries
)
}
/**
* Allows users to append Data to series.
*
* @param {array} newData - New data in the same format as series
*/
appendData(newData, overwriteInitialSeries = true) {
let me = this
me.w.globals.dataChanged = true
me.series.getPreviousPaths()
let newSeries = me.w.config.series.slice()
for (let i = 0; i < newSeries.length; i++) {
if (newData[i] !== null && typeof newData[i] !== 'undefined') {
for (let j = 0; j < newData[i].data.length; j++) {
newSeries[i].data.push(newData[i].data[j])
}
}
}
me.w.config.series = newSeries
if (overwriteInitialSeries) {
me.w.globals.initialSeries = Utils.clone(me.w.config.series)
}
return this.update()
}
update(options) {
return new Promise((resolve, reject) => {
new Destroy(this.ctx).clear({ isUpdating: true })
const graphData = this.create(this.w.config.series, options)
if (!graphData) return resolve(this)
this.mount(graphData)
.then(() => {
if (typeof this.w.config.chart.events.updated === 'function') {
this.w.config.chart.events.updated(this, this.w)
}
this.events.fireEvent('updated', [this, this.w])
this.w.globals.isDirty = true
resolve(this)
})
.catch((e) => {
reject(e)
})
})
}
/**
* Get all charts in the same "group" (including the instance which is called upon) to sync them when user zooms in/out or pan.
*/
getSyncedCharts() {
const chartGroups = this.getGroupedCharts()
let allCharts = [this]
if (chartGroups.length) {
allCharts = []
chartGroups.forEach((ch) => {
allCharts.push(ch)
})
}
return allCharts
}
/**
* Get charts in the same "group" (excluding the instance which is called upon) to perform operations on the other charts of the same group (eg., tooltip hovering)
*/
getGroupedCharts() {
return Apex._chartInstances
.filter((ch) => {
if (ch.group) {
return true
}
})
.map((ch) => (this.w.config.chart.group === ch.group ? ch.chart : this))
}
static getChartByID(id) {
const chartId = Utils.escapeString(id)
if (!Apex._chartInstances) return undefined
const c = Apex._chartInstances.filter((ch) => ch.id === chartId)[0]
return c && c.chart
}
/**
* Allows the user to provide data attrs in the element and the chart will render automatically when this method is called by searching for the elements containing 'data-apexcharts' attribute
*/
static initOnLoad() {
const els = document.querySelectorAll('[data-apexcharts]')
for (let i = 0; i < els.length; i++) {
const el = els[i]
const options = JSON.parse(els[i].getAttribute('data-options'))
const apexChart = new ApexCharts(el, options)
apexChart.render()
}
}
/**
* This static method allows users to call chart methods without necessarily from the
* instance of the chart in case user has assigned chartID to the targeted chart.
* The chartID is used for mapping the instance stored in Apex._chartInstances global variable
*
* This is helpful in cases when you don't have reference of the chart instance
* easily and need to call the method from anywhere.
* For eg, in React/Vue applications when you have many parent/child components,
* and need easy reference to other charts for performing dynamic operations
*
* @param {string} chartID - The unique identifier which will be used to call methods
* on that chart instance
* @param {function} fn - The method name to call
* @param {object} opts - The parameters which are accepted in the original method will be passed here in the same order.
*/
static exec(chartID, fn, ...opts) {
const chart = this.getChartByID(chartID)
if (!chart) return
// turn on the global exec flag to indicate this method was called
chart.w.globals.isExecCalled = true
let ret = null
if (chart.publicMethods.indexOf(fn) !== -1) {
ret = chart[fn](...opts)
}
return ret
}
static merge(target, source) {
return Utils.extend(target, source)
}
toggleSeries(seriesName) {
return this.series.toggleSeries(seriesName)
}
highlightSeriesOnLegendHover(e, targetElement) {
return this.series.toggleSeriesOnHover(e, targetElement)
}
showSeries(seriesName) {
this.series.showSeries(seriesName)
}
hideSeries(seriesName) {
this.series.hideSeries(seriesName)
}
isSeriesHidden(seriesName) {
this.series.isSeriesHidden(seriesName);
}
resetSeries(shouldUpdateChart = true, shouldResetZoom = true) {
this.series.resetSeries(shouldUpdateChart, shouldResetZoom)
}
// Public method to add event listener on chart context
addEventListener(name, handler) {
this.events.addEventListener(name, handler)
}
// Public method to remove event listener on chart context
removeEventListener(name, handler) {
this.events.removeEventListener(name, handler)
}
addXaxisAnnotation(opts, pushToMemory = true, context = undefined) {
let me = this
if (context) {
me = context
}
me.annotations.addXaxisAnnotationExternal(opts, pushToMemory, me)
}
addYaxisAnnotation(opts, pushToMemory = true, context = undefined) {
let me = this
if (context) {
me = context
}
me.annotations.addYaxisAnnotationExternal(opts, pushToMemory, me)
}
addPointAnnotation(opts, pushToMemory = true, context = undefined) {
let me = this
if (context) {
me = context
}
me.annotations.addPointAnnotationExternal(opts, pushToMemory, me)
}
clearAnnotations(context = undefined) {
let me = this
if (context) {
me = context
}
me.annotations.clearAnnotations(me)
}
removeAnnotation(id, context = undefined) {
let me = this
if (context) {
me = context
}
me.annotations.removeAnnotation(me, id)
}
getChartArea() {
const el = this.w.globals.dom.baseEl.querySelector('.apexcharts-inner')
return el
}
getSeriesTotalXRange(minX, maxX) {
return this.coreUtils.getSeriesTotalsXRange(minX, maxX)
}
getHighestValueInSeries(seriesIndex = 0) {
const range = new Range(this.ctx)
return range.getMinYMaxY(seriesIndex).highestY
}
getLowestValueInSeries(seriesIndex = 0) {
const range = new Range(this.ctx)
return range.getMinYMaxY(seriesIndex).lowestY
}
getSeriesTotal() {
return this.w.globals.seriesTotals
}
toggleDataPointSelection(seriesIndex, dataPointIndex) {
return this.updateHelpers.toggleDataPointSelection(
seriesIndex,
dataPointIndex
)
}
zoomX(min, max) {
this.ctx.toolbar.zoomUpdateOptions(min, max)
}
setLocale(localeName) {
this.localization.setCurrentLocaleValues(localeName)
}
dataURI(options) {
const exp = new Exports(this.ctx)
return exp.dataURI(options)
}
exportToCSV(options = {}) {
const exp = new Exports(this.ctx)
return exp.exportToCSV(options)
}
paper() {
return this.w.globals.dom.Paper
}
_parentResizeCallback() {
if (
this.w.globals.animationEnded &&
this.w.config.chart.redrawOnParentResize
) {
this._windowResize()
}
}
/**
* Handle window resize and re-draw the whole chart.
*/
_windowResize() {
clearTimeout(this.w.globals.resizeTimer)
this.w.globals.resizeTimer = window.setTimeout(() => {
this.w.globals.resized = true
this.w.globals.dataChanged = false
// we need to redraw the whole chart on window resize (with a small delay).
this.ctx.update()
}, 150)
}
_windowResizeHandler() {
let { redrawOnWindowResize: redraw } = this.w.config.chart
if (typeof redraw === 'function') {
redraw = redraw()
}
redraw && this._windowResize()
}
}
+601
View File
@@ -0,0 +1,601 @@
@keyframes opaque {
0% {
opacity: 0
}
to {
opacity: 1
}
}
@keyframes resizeanim {
0%,to {
opacity: 0
}
}
.apexcharts-canvas {
position: relative;
user-select: none
}
.apexcharts-canvas ::-webkit-scrollbar {
-webkit-appearance: none;
width: 6px
}
.apexcharts-canvas ::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0,0,0,.5);
box-shadow: 0 0 1px rgba(255,255,255,.5);
-webkit-box-shadow: 0 0 1px rgba(255,255,255,.5)
}
.apexcharts-inner {
position: relative
}
.apexcharts-text tspan {
font-family: inherit
}
.legend-mouseover-inactive {
transition: .15s ease all;
opacity: .2
}
.apexcharts-legend-text {
padding-left: 15px;
margin-left: -15px;
}
.apexcharts-series-collapsed {
opacity: 0
}
.apexcharts-tooltip {
border-radius: 5px;
box-shadow: 2px 2px 6px -4px #999;
cursor: default;
font-size: 14px;
left: 62px;
opacity: 0;
pointer-events: none;
position: absolute;
top: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
white-space: nowrap;
z-index: 12;
transition: .15s ease all
}
.apexcharts-tooltip.apexcharts-active {
opacity: 1;
transition: .15s ease all
}
.apexcharts-tooltip.apexcharts-theme-light {
border: 1px solid #e3e3e3;
background: rgba(255,255,255,.96)
}
.apexcharts-tooltip.apexcharts-theme-dark {
color: #fff;
background: rgba(30,30,30,.8)
}
.apexcharts-tooltip * {
font-family: inherit
}
.apexcharts-tooltip-title {
padding: 6px;
font-size: 15px;
margin-bottom: 4px
}
.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {
background: #eceff1;
border-bottom: 1px solid #ddd
}
.apexcharts-tooltip.apexcharts-theme-dark .apexcharts-tooltip-title {
background: rgba(0,0,0,.7);
border-bottom: 1px solid #333
}
.apexcharts-tooltip-text-goals-value,.apexcharts-tooltip-text-y-value,.apexcharts-tooltip-text-z-value {
display: inline-block;
margin-left: 5px;
font-weight: 600
}
.apexcharts-tooltip-text-goals-label:empty,.apexcharts-tooltip-text-goals-value:empty,.apexcharts-tooltip-text-y-label:empty,.apexcharts-tooltip-text-y-value:empty,.apexcharts-tooltip-text-z-value:empty,.apexcharts-tooltip-title:empty {
display: none
}
.apexcharts-tooltip-text-goals-label,.apexcharts-tooltip-text-goals-value {
padding: 6px 0 5px
}
.apexcharts-tooltip-goals-group,.apexcharts-tooltip-text-goals-label,.apexcharts-tooltip-text-goals-value {
display: flex
}
.apexcharts-tooltip-text-goals-label:not(:empty),.apexcharts-tooltip-text-goals-value:not(:empty) {
margin-top: -6px
}
.apexcharts-tooltip-marker {
width: 12px;
height: 12px;
position: relative;
top: 0;
margin-right: 10px;
border-radius: 50%
}
.apexcharts-tooltip-series-group {
padding: 0 10px;
display: none;
text-align: left;
justify-content: left;
align-items: center
}
.apexcharts-tooltip-series-group.apexcharts-active .apexcharts-tooltip-marker {
opacity: 1
}
.apexcharts-tooltip-series-group.apexcharts-active,.apexcharts-tooltip-series-group:last-child {
padding-bottom: 4px
}
.apexcharts-tooltip-series-group-hidden {
opacity: 0;
height: 0;
line-height: 0;
padding: 0!important
}
.apexcharts-tooltip-y-group {
padding: 6px 0 5px
}
.apexcharts-custom-tooltip,.apexcharts-tooltip-box {
padding: 4px 8px
}
.apexcharts-tooltip-boxPlot {
display: flex;
flex-direction: column-reverse
}
.apexcharts-tooltip-box>div {
margin: 4px 0
}
.apexcharts-tooltip-box span.value {
font-weight: 700
}
.apexcharts-tooltip-rangebar {
padding: 5px 8px
}
.apexcharts-tooltip-rangebar .category {
font-weight: 600;
color: #777
}
.apexcharts-tooltip-rangebar .series-name {
font-weight: 700;
display: block;
margin-bottom: 5px
}
.apexcharts-xaxistooltip,.apexcharts-yaxistooltip {
opacity: 0;
pointer-events: none;
color: #373d3f;
font-size: 13px;
text-align: center;
border-radius: 2px;
position: absolute;
z-index: 10;
background: #eceff1;
border: 1px solid #90a4ae
}
.apexcharts-xaxistooltip {
padding: 9px 10px;
transition: .15s ease all
}
.apexcharts-xaxistooltip.apexcharts-theme-dark {
background: rgba(0,0,0,.7);
border: 1px solid rgba(0,0,0,.5);
color: #fff
}
.apexcharts-xaxistooltip:after,.apexcharts-xaxistooltip:before {
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none
}
.apexcharts-xaxistooltip:after {
border-color: transparent;
border-width: 6px;
margin-left: -6px
}
.apexcharts-xaxistooltip:before {
border-color: transparent;
border-width: 7px;
margin-left: -7px
}
.apexcharts-xaxistooltip-bottom:after,.apexcharts-xaxistooltip-bottom:before {
bottom: 100%
}
.apexcharts-xaxistooltip-top:after,.apexcharts-xaxistooltip-top:before {
top: 100%
}
.apexcharts-xaxistooltip-bottom:after {
border-bottom-color: #eceff1
}
.apexcharts-xaxistooltip-bottom:before {
border-bottom-color: #90a4ae
}
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:after,.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:before {
border-bottom-color: rgba(0,0,0,.5)
}
.apexcharts-xaxistooltip-top:after {
border-top-color: #eceff1
}
.apexcharts-xaxistooltip-top:before {
border-top-color: #90a4ae
}
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:after,.apexcharts-xaxistooltip-top.apexcharts-theme-dark:before {
border-top-color: rgba(0,0,0,.5)
}
.apexcharts-xaxistooltip.apexcharts-active {
opacity: 1;
transition: .15s ease all
}
.apexcharts-yaxistooltip {
padding: 4px 10px
}
.apexcharts-yaxistooltip.apexcharts-theme-dark {
background: rgba(0,0,0,.7);
border: 1px solid rgba(0,0,0,.5);
color: #fff
}
.apexcharts-yaxistooltip:after,.apexcharts-yaxistooltip:before {
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none
}
.apexcharts-yaxistooltip:after {
border-color: transparent;
border-width: 6px;
margin-top: -6px
}
.apexcharts-yaxistooltip:before {
border-color: transparent;
border-width: 7px;
margin-top: -7px
}
.apexcharts-yaxistooltip-left:after,.apexcharts-yaxistooltip-left:before {
left: 100%
}
.apexcharts-yaxistooltip-right:after,.apexcharts-yaxistooltip-right:before {
right: 100%
}
.apexcharts-yaxistooltip-left:after {
border-left-color: #eceff1
}
.apexcharts-yaxistooltip-left:before {
border-left-color: #90a4ae
}
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:after,.apexcharts-yaxistooltip-left.apexcharts-theme-dark:before {
border-left-color: rgba(0,0,0,.5)
}
.apexcharts-yaxistooltip-right:after {
border-right-color: #eceff1
}
.apexcharts-yaxistooltip-right:before {
border-right-color: #90a4ae
}
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:after,.apexcharts-yaxistooltip-right.apexcharts-theme-dark:before {
border-right-color: rgba(0,0,0,.5)
}
.apexcharts-yaxistooltip.apexcharts-active {
opacity: 1
}
.apexcharts-yaxistooltip-hidden {
display: none
}
.apexcharts-xcrosshairs,.apexcharts-ycrosshairs {
pointer-events: none;
opacity: 0;
transition: .15s ease all
}
.apexcharts-xcrosshairs.apexcharts-active,.apexcharts-ycrosshairs.apexcharts-active {
opacity: 1;
transition: .15s ease all
}
.apexcharts-ycrosshairs-hidden {
opacity: 0
}
.apexcharts-selection-rect {
cursor: move
}
.svg_select_boundingRect,.svg_select_points_rot {
pointer-events: none;
opacity: 0;
visibility: hidden
}
.apexcharts-selection-rect+g .svg_select_boundingRect,.apexcharts-selection-rect+g .svg_select_points_rot {
opacity: 0;
visibility: hidden
}
.apexcharts-selection-rect+g .svg_select_points_l,.apexcharts-selection-rect+g .svg_select_points_r {
cursor: ew-resize;
opacity: 1;
visibility: visible
}
.svg_select_points {
fill: #efefef;
stroke: #333;
rx: 2
}
.apexcharts-svg.apexcharts-zoomable.hovering-zoom {
cursor: crosshair
}
.apexcharts-svg.apexcharts-zoomable.hovering-pan {
cursor: move
}
.apexcharts-menu-icon,.apexcharts-pan-icon,.apexcharts-reset-icon,.apexcharts-selection-icon,.apexcharts-toolbar-custom-icon,.apexcharts-zoom-icon,.apexcharts-zoomin-icon,.apexcharts-zoomout-icon {
cursor: pointer;
width: 20px;
height: 20px;
line-height: 24px;
color: #6e8192;
text-align: center
}
.apexcharts-menu-icon svg,.apexcharts-reset-icon svg,.apexcharts-zoom-icon svg,.apexcharts-zoomin-icon svg,.apexcharts-zoomout-icon svg {
fill: #6e8192
}
.apexcharts-selection-icon svg {
fill: #444;
transform: scale(.76)
}
.apexcharts-theme-dark .apexcharts-menu-icon svg,.apexcharts-theme-dark .apexcharts-pan-icon svg,.apexcharts-theme-dark .apexcharts-reset-icon svg,.apexcharts-theme-dark .apexcharts-selection-icon svg,.apexcharts-theme-dark .apexcharts-toolbar-custom-icon svg,.apexcharts-theme-dark .apexcharts-zoom-icon svg,.apexcharts-theme-dark .apexcharts-zoomin-icon svg,.apexcharts-theme-dark .apexcharts-zoomout-icon svg {
fill: #f3f4f5
}
.apexcharts-canvas .apexcharts-reset-zoom-icon.apexcharts-selected svg,.apexcharts-canvas .apexcharts-selection-icon.apexcharts-selected svg,.apexcharts-canvas .apexcharts-zoom-icon.apexcharts-selected svg {
fill: #008ffb
}
.apexcharts-theme-light .apexcharts-menu-icon:hover svg,.apexcharts-theme-light .apexcharts-reset-icon:hover svg,.apexcharts-theme-light .apexcharts-selection-icon:not(.apexcharts-selected):hover svg,.apexcharts-theme-light .apexcharts-zoom-icon:not(.apexcharts-selected):hover svg,.apexcharts-theme-light .apexcharts-zoomin-icon:hover svg,.apexcharts-theme-light .apexcharts-zoomout-icon:hover svg {
fill: #333
}
.apexcharts-menu-icon,.apexcharts-selection-icon {
position: relative
}
.apexcharts-reset-icon {
margin-left: 5px
}
.apexcharts-menu-icon,.apexcharts-reset-icon,.apexcharts-zoom-icon {
transform: scale(.85)
}
.apexcharts-zoomin-icon,.apexcharts-zoomout-icon {
transform: scale(.7)
}
.apexcharts-zoomout-icon {
margin-right: 3px
}
.apexcharts-pan-icon {
transform: scale(.62);
position: relative;
left: 1px;
top: 0
}
.apexcharts-pan-icon svg {
fill: #fff;
stroke: #6e8192;
stroke-width: 2
}
.apexcharts-pan-icon.apexcharts-selected svg {
stroke: #008ffb
}
.apexcharts-pan-icon:not(.apexcharts-selected):hover svg {
stroke: #333
}
.apexcharts-toolbar {
position: absolute;
z-index: 11;
max-width: 176px;
text-align: right;
border-radius: 3px;
padding: 0 6px 2px;
display: flex;
justify-content: space-between;
align-items: center
}
.apexcharts-menu {
background: #fff;
position: absolute;
top: 100%;
border: 1px solid #ddd;
border-radius: 3px;
padding: 3px;
right: 10px;
opacity: 0;
min-width: 110px;
transition: .15s ease all;
pointer-events: none
}
.apexcharts-menu.apexcharts-menu-open {
opacity: 1;
pointer-events: all;
transition: .15s ease all
}
.apexcharts-menu-item {
padding: 6px 7px;
font-size: 12px;
cursor: pointer
}
.apexcharts-theme-light .apexcharts-menu-item:hover {
background: #eee
}
.apexcharts-theme-dark .apexcharts-menu {
background: rgba(0,0,0,.7);
color: #fff
}
@media screen and (min-width:768px) {
.apexcharts-canvas:hover .apexcharts-toolbar {
opacity: 1
}
}
.apexcharts-canvas .apexcharts-element-hidden,.apexcharts-datalabel.apexcharts-element-hidden,.apexcharts-hide .apexcharts-series-points {
opacity: 0
}
.apexcharts-hidden-element-shown {
opacity: 1;
transition: 0.25s ease all;
}
.apexcharts-datalabel,.apexcharts-datalabel-label,.apexcharts-datalabel-value,.apexcharts-datalabels,.apexcharts-pie-label {
cursor: default;
pointer-events: none
}
.apexcharts-pie-label-delay {
opacity: 0;
animation-name: opaque;
animation-duration: .3s;
animation-fill-mode: forwards;
animation-timing-function: ease
}
.apexcharts-radialbar-label {
cursor: pointer;
}
.apexcharts-annotation-rect,.apexcharts-area-series .apexcharts-area,.apexcharts-area-series .apexcharts-series-markers .apexcharts-marker.no-pointer-events,.apexcharts-gridline,.apexcharts-line,.apexcharts-line-series .apexcharts-series-markers .apexcharts-marker.no-pointer-events,.apexcharts-point-annotation-label,.apexcharts-radar-series path,.apexcharts-radar-series polygon,.apexcharts-toolbar svg,.apexcharts-tooltip .apexcharts-marker,.apexcharts-xaxis-annotation-label,.apexcharts-yaxis-annotation-label,.apexcharts-zoom-rect {
pointer-events: none
}
.apexcharts-marker {
transition: .15s ease all
}
.resize-triggers {
animation: 1ms resizeanim;
visibility: hidden;
opacity: 0;
height: 100%;
width: 100%;
overflow: hidden
}
.contract-trigger:before,.resize-triggers,.resize-triggers>div {
content: " ";
display: block;
position: absolute;
top: 0;
left: 0
}
.resize-triggers>div {
height: 100%;
width: 100%;
background: #eee;
overflow: auto
}
.contract-trigger:before {
overflow: hidden;
width: 200%;
height: 200%
}
.apexcharts-bar-goals-markers{
pointer-events: none
}
.apexcharts-bar-shadows{
pointer-events: none
}
.apexcharts-rangebar-goals-markers{
pointer-events: none
}
+5
View File
@@ -0,0 +1,5 @@
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="3.2"/>
<path d="M9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

+4
View File
@@ -0,0 +1,4 @@
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 199 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>

After

Width:  |  Height:  |  Size: 185 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" width="24" height="24">
<defs>
<path id="a" d="M0 0h24v24H0z"/>
</defs>
<clipPath id="b">
<use xlink:href="#a" overflow="visible"/>
</clipPath>
<path clip-path="url(#b)" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2z"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000" height="24" viewBox="0 0 24 24" width="24">
<defs>
<path d="M0 0h24v24H0z" id="a"/>
</defs>
<clipPath id="b">
<use overflow="visible" xlink:href="#a"/>
</clipPath>
<path clip-path="url(#b)" d="M23 5.5V20c0 2.2-1.8 4-4 4h-7.3c-1.08 0-2.1-.43-2.85-1.19L1 14.83s1.26-1.23 1.3-1.25c.22-.19.49-.29.79-.29.22 0 .42.06.6.16.04.01 4.31 2.46 4.31 2.46V4c0-.83.67-1.5 1.5-1.5S11 3.17 11 4v7h1V1.5c0-.83.67-1.5 1.5-1.5S15 .67 15 1.5V11h1V2.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5V11h1V5.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 656 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" height="24" viewBox="0 0 24 24" width="24">
<path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 263 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4V7zm-1-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

+4
View File
@@ -0,0 +1,4 @@
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" height="24" viewBox="0 0 24 24" width="24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
</svg>

After

Width:  |  Height:  |  Size: 265 B

+4
View File
@@ -0,0 +1,4 @@
<svg fill="#6E8192" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 5h2V3c-1.1 0-2 .9-2 2zm0 8h2v-2H3v2zm4 8h2v-2H7v2zM3 9h2V7H3v2zm10-6h-2v2h2V3zm6 0v2h2c0-1.1-.9-2-2-2zM5 21v-2H3c0 1.1.9 2 2 2zm-2-4h2v-2H3v2zM9 3H7v2h2V3zm2 18h2v-2h-2v2zm8-8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2zm0-12h2V7h-2v2zm0 8h2v-2h-2v2zm-4 4h2v-2h-2v2zm0-16h2V3h-2v2z"/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

+3
View File
@@ -0,0 +1,3 @@
<svg stroke="#333" fill="none" height="24" viewBox="0 0 28 24" width="24" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4" width="16" height="16" stroke-width="2.5" stroke-dasharray="4" stroke-dashoffset="2"></rect>
</svg>

After

Width:  |  Height:  |  Size: 231 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" height="24" viewBox="0 0 24 24" width="24">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

+10
View File
@@ -0,0 +1,10 @@
<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M0 0h24v24H0z" id="a"/>
</defs>
<clipPath id="b">
<use overflow="visible" xlink:href="#a"/>
</clipPath>
<path clip-path="url(#b)" d="M15 3l2.3 2.3-2.89 2.87 1.42 1.42L18.7 6.7 21 9V3zM3 9l2.3-2.3 2.87 2.89 1.42-1.42L6.7 5.3 9 3H3zm6 12l-2.3-2.3 2.89-2.87-1.42-1.42L5.3 17.3 3 15v6zm12-6l-2.3 2.3-2.87-2.89-1.42 1.42 2.89 2.87L15 21h6z"/>
<path clip-path="url(#b)" d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 600 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" height="24" viewBox="0 0 24 24" width="24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 5v4h2V5h4V3H5c-1.1 0-2 .9-2 2zm2 10H3v4c0 1.1.9 2 2 2h4v-2H5v-4zm14 4h-4v2h4c1.1 0 2-.9 2-2v-4h-2v4zm0-16h-4v2h4v4h2V5c0-1.1-.9-2-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

+626
View File
@@ -0,0 +1,626 @@
import BarDataLabels from './common/bar/DataLabels'
import BarHelpers from './common/bar/Helpers'
import CoreUtils from '../modules/CoreUtils'
import Utils from '../utils/Utils'
import Filters from '../modules/Filters'
import Graphics from '../modules/Graphics'
import Series from '../modules/Series'
/**
* ApexCharts Bar Class responsible for drawing both Columns and Bars.
*
* @module Bar
**/
class Bar {
constructor(ctx, xyRatios) {
this.ctx = ctx
this.w = ctx.w
const w = this.w
this.barOptions = w.config.plotOptions.bar
this.isHorizontal = this.barOptions.horizontal
this.strokeWidth = w.config.stroke.width
this.isNullValue = false
this.isRangeBar = w.globals.seriesRange.length && this.isHorizontal
this.isVerticalGroupedRangeBar =
!w.globals.isBarHorizontal &&
w.globals.seriesRange.length &&
w.config.plotOptions.bar.rangeBarGroupRows
this.isFunnel = this.barOptions.isFunnel
this.xyRatios = xyRatios
if (this.xyRatios !== null) {
this.xRatio = xyRatios.xRatio
this.yRatio = xyRatios.yRatio
this.invertedXRatio = xyRatios.invertedXRatio
this.invertedYRatio = xyRatios.invertedYRatio
this.baseLineY = xyRatios.baseLineY
this.baseLineInvertedY = xyRatios.baseLineInvertedY
}
this.yaxisIndex = 0
this.seriesLen = 0
this.pathArr = []
const ser = new Series(this.ctx)
this.lastActiveBarSerieIndex = ser.getActiveConfigSeriesIndex('desc', [
'bar',
'column',
])
const barSeriesIndices = ser.getBarSeriesIndices()
const coreUtils = new CoreUtils(this.ctx)
this.stackedSeriesTotals = coreUtils.getStackedSeriesTotals(
this.w.config.series
.map((s, i) => {
return barSeriesIndices.indexOf(i) === -1 ? i : -1
})
.filter((s) => {
return s !== -1
})
)
this.barHelpers = new BarHelpers(this)
}
/** primary draw method which is called on bar object
* @memberof Bar
* @param {array} series - user supplied series values
* @param {int} seriesIndex - the index by which series will be drawn on the svg
* @return {node} element which is supplied to parent chart draw method for appending
**/
draw(series, seriesIndex) {
let w = this.w
let graphics = new Graphics(this.ctx)
const coreUtils = new CoreUtils(this.ctx, w)
series = coreUtils.getLogSeries(series)
this.series = series
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
this.barHelpers.initVariables(series)
let ret = graphics.group({
class: 'apexcharts-bar-series apexcharts-plot-series',
})
if (w.config.dataLabels.enabled) {
if (this.totalItems > this.barOptions.dataLabels.maxItems) {
console.warn(
'WARNING: DataLabels are enabled but there are too many to display. This may cause performance issue when rendering - ApexCharts'
)
}
}
for (let i = 0, bc = 0; i < series.length; i++, bc++) {
let x,
y,
xDivision, // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
yDivision, // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroH, // zeroH is the baseline where 0 meets y axis
zeroW // zeroW is the baseline where 0 meets x axis
let yArrj = [] // hold y values of current iterating series
let xArrj = [] // hold x values of current iterating series
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
// el to which series will be drawn
let elSeries = graphics.group({
class: `apexcharts-series`,
rel: i + 1,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
'data:realIndex': realIndex,
})
this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex)
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1
}
let barHeight = 0
let barWidth = 0
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
this.isReversed =
w.config.yaxis[this.yaxisIndex] &&
w.config.yaxis[this.yaxisIndex].reversed
let initPositions = this.barHelpers.initialPositions()
y = initPositions.y
barHeight = initPositions.barHeight
yDivision = initPositions.yDivision
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
xDivision = initPositions.xDivision
zeroH = initPositions.zeroH
if (!this.horizontal) {
xArrj.push(x + barWidth / 2)
}
// eldatalabels
let elDataLabelsWrap = graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex,
})
w.globals.delayedElements.push({
el: elDataLabelsWrap.node,
})
elDataLabelsWrap.node.classList.add('apexcharts-element-hidden')
let elGoalsMarkers = graphics.group({
class: 'apexcharts-bar-goals-markers',
})
let elBarShadows = graphics.group({
class: 'apexcharts-bar-shadows',
})
w.globals.delayedElements.push({
el: elBarShadows.node,
})
elBarShadows.node.classList.add('apexcharts-element-hidden')
for (let j = 0; j < series[i].length; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)
let paths = null
const pathsParams = {
indexes: {
i,
j,
realIndex,
bc,
},
x,
y,
strokeWidth,
elSeries,
}
if (this.isHorizontal) {
paths = this.drawBarPaths({
...pathsParams,
barHeight,
zeroW,
yDivision,
})
barWidth = this.series[i][j] / this.invertedYRatio
} else {
paths = this.drawColumnPaths({
...pathsParams,
xDivision,
barWidth,
zeroH,
})
barHeight = this.series[i][j] / this.yRatio[this.yaxisIndex]
}
let pathFill = this.barHelpers.getPathFillColor(series, i, j, realIndex)
if (
this.isFunnel &&
this.barOptions.isFunnel3d &&
this.pathArr.length &&
j > 0
) {
const barShadow = this.barHelpers.drawBarShadow({
color:
typeof pathFill === 'string' && pathFill?.indexOf('url') === -1
? pathFill
: Utils.hexToRgba(w.globals.colors[i]),
prevPaths: this.pathArr[this.pathArr.length - 1],
currPaths: paths,
})
if (barShadow) {
elBarShadows.add(barShadow)
}
}
this.pathArr.push(paths)
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition: paths.barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth,
})
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine)
}
y = paths.y
x = paths.x
// push current X
if (j > 0) {
xArrj.push(x + barWidth / 2)
}
yArrj.push(y)
this.renderSeries({
realIndex,
pathFill,
j,
i,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
x,
y,
series,
barHeight: paths.barHeight ? paths.barHeight : barHeight,
barWidth: paths.barWidth ? paths.barWidth : barWidth,
elDataLabelsWrap,
elGoalsMarkers,
elBarShadows,
visibleSeries: this.visibleI,
type: 'bar',
})
}
// push all x val arrays into main xArr
w.globals.seriesXvalues[realIndex] = xArrj
w.globals.seriesYvalues[realIndex] = yArrj
ret.add(elSeries)
}
return ret
}
renderSeries({
realIndex,
pathFill,
lineFill,
j,
i,
groupIndex, // required in grouped-stacked bars
pathFrom,
pathTo,
strokeWidth,
elSeries,
x, // x pos
y, // y pos
y1, // absolute value
y2, // absolute value
series,
barHeight,
barWidth,
barXPosition,
barYPosition,
elDataLabelsWrap,
elGoalsMarkers,
elBarShadows,
visibleSeries,
type,
}) {
const w = this.w
const graphics = new Graphics(this.ctx)
if (!lineFill) {
/* fix apexcharts#341 */
lineFill = this.barOptions.distributed
? w.globals.stroke.colors[j]
: w.globals.stroke.colors[realIndex]
}
if (w.config.series[i].data[j] && w.config.series[i].data[j].strokeColor) {
lineFill = w.config.series[i].data[j].strokeColor
}
if (this.isNullValue) {
pathFill = 'none'
}
let delay =
((j / w.config.chart.animations.animateGradually.delay) *
(w.config.chart.animations.speed / w.globals.dataPoints)) /
2.4
let renderedPath = graphics.renderPaths({
i,
j,
realIndex,
pathFrom,
pathTo,
stroke: lineFill,
strokeWidth,
strokeLineCap: w.config.stroke.lineCap,
fill: pathFill,
animationDelay: delay,
initialSpeed: w.config.chart.animations.speed,
dataChangeSpeed: w.config.chart.animations.dynamicAnimation.speed,
className: `apexcharts-${type}-area`,
})
renderedPath.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
const forecast = w.config.forecastDataPoints
if (forecast.count > 0) {
if (j >= w.globals.dataPoints - forecast.count) {
renderedPath.node.setAttribute('stroke-dasharray', forecast.dashArray)
renderedPath.node.setAttribute('stroke-width', forecast.strokeWidth)
renderedPath.node.setAttribute('fill-opacity', forecast.fillOpacity)
}
}
if (typeof y1 !== 'undefined' && typeof y2 !== 'undefined') {
renderedPath.attr('data-range-y1', y1)
renderedPath.attr('data-range-y2', y2)
}
const filters = new Filters(this.ctx)
filters.setSelectionFilter(renderedPath, realIndex, j)
elSeries.add(renderedPath)
let barDataLabels = new BarDataLabels(this)
let dataLabelsObj = barDataLabels.handleBarDataLabels({
x,
y,
y1,
y2,
i,
j,
series,
realIndex,
groupIndex,
barHeight,
barWidth,
barXPosition,
barYPosition,
renderedPath,
visibleSeries,
})
if (dataLabelsObj.dataLabels !== null) {
elDataLabelsWrap.add(dataLabelsObj.dataLabels)
}
if (dataLabelsObj.totalDataLabels) {
elDataLabelsWrap.add(dataLabelsObj.totalDataLabels)
}
elSeries.add(elDataLabelsWrap)
if (elGoalsMarkers) {
elSeries.add(elGoalsMarkers)
}
if (elBarShadows) {
elSeries.add(elBarShadows)
}
return elSeries
}
drawBarPaths({
indexes,
barHeight,
strokeWidth,
zeroW,
x,
y,
yDivision,
elSeries,
}) {
let w = this.w
let i = indexes.i
let j = indexes.j
let barYPosition
if (w.globals.isXNumeric) {
y =
(w.globals.seriesX[i][j] - w.globals.minX) / this.invertedXRatio -
barHeight
barYPosition = y + barHeight * this.visibleI
} else {
if (w.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
let nonZeroColumns = 0
let zeroEncounters = 0
w.globals.seriesPercent.forEach((_s, _si) => {
if (_s[j]) {
nonZeroColumns++
}
if (_si < i && _s[j] === 0) {
zeroEncounters++
}
})
if (nonZeroColumns > 0) {
barHeight = (this.seriesLen * barHeight) / nonZeroColumns
}
barYPosition = y + barHeight * this.visibleI
barYPosition -= barHeight * zeroEncounters
} else {
barYPosition = y + barHeight * this.visibleI
}
}
if (this.isFunnel) {
zeroW =
zeroW -
(this.barHelpers.getXForValue(this.series[i][j], zeroW) - zeroW) / 2
}
x = this.barHelpers.getXForValue(this.series[i][j], zeroW)
const paths = this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1: zeroW,
x2: x,
strokeWidth,
series: this.series,
realIndex: indexes.realIndex,
i,
j,
w,
})
if (!w.globals.isXNumeric) {
y = y + yDivision
}
this.barHelpers.barBackground({
j,
i,
y1: barYPosition - barHeight * this.visibleI,
y2: barHeight * this.seriesLen,
elSeries,
})
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
x1: zeroW,
x,
y,
goalX: this.barHelpers.getGoalValues('x', zeroW, null, i, j),
barYPosition,
barHeight,
}
}
drawColumnPaths({
indexes,
x,
y,
xDivision,
barWidth,
zeroH,
strokeWidth,
elSeries,
}) {
let w = this.w
let realIndex = indexes.realIndex
let i = indexes.i
let j = indexes.j
let bc = indexes.bc
let barXPosition
if (w.globals.isXNumeric) {
const xForNumericX = this.getBarXForNumericXAxis({
x,
j,
realIndex,
barWidth,
})
x = xForNumericX.x
barXPosition = xForNumericX.barXPosition
} else {
if (w.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
const { nonZeroColumns, zeroEncounters } =
this.barHelpers.getZeroValueEncounters({ i, j })
if (nonZeroColumns > 0) {
barWidth = (this.seriesLen * barWidth) / nonZeroColumns
}
barXPosition = x + barWidth * this.visibleI
barXPosition -= barWidth * zeroEncounters
} else {
barXPosition = x + barWidth * this.visibleI
}
}
y = this.barHelpers.getYForValue(this.series[i][j], zeroH)
const paths = this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1: zeroH,
y2: y,
strokeWidth,
series: this.series,
realIndex: indexes.realIndex,
i,
j,
w,
})
if (!w.globals.isXNumeric) {
x = x + xDivision
}
this.barHelpers.barBackground({
bc,
j,
i,
x1: barXPosition - strokeWidth / 2 - barWidth * this.visibleI,
x2: barWidth * this.seriesLen + strokeWidth / 2,
elSeries,
})
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
x,
y,
goalY: this.barHelpers.getGoalValues('y', null, zeroH, i, j),
barXPosition,
barWidth,
}
}
getBarXForNumericXAxis({ x, barWidth, realIndex, j }) {
const w = this.w
let sxI = realIndex
if (!w.globals.seriesX[realIndex].length) {
sxI = w.globals.maxValsInArrayIndex
}
if (w.globals.seriesX[sxI][j]) {
x =
(w.globals.seriesX[sxI][j] - w.globals.minX) / this.xRatio -
(barWidth * this.seriesLen) / 2
}
return {
barXPosition: x + barWidth * this.visibleI,
x,
}
}
/** getPreviousPath is a common function for bars/columns which is used to get previous paths when data changes.
* @memberof Bar
* @param {int} realIndex - current iterating i
* @param {int} j - current iterating series's j index
* @return {string} pathFrom is the string which will be appended in animations
**/
getPreviousPath(realIndex, j) {
let w = this.w
let pathFrom
for (let pp = 0; pp < w.globals.previousPaths.length; pp++) {
let gpp = w.globals.previousPaths[pp]
if (
gpp.paths &&
gpp.paths.length > 0 &&
parseInt(gpp.realIndex, 10) === parseInt(realIndex, 10)
) {
if (typeof w.globals.previousPaths[pp].paths[j] !== 'undefined') {
pathFrom = w.globals.previousPaths[pp].paths[j].d
}
}
}
return pathFrom
}
}
export default Bar
+535
View File
@@ -0,0 +1,535 @@
import CoreUtils from '../modules/CoreUtils'
import Bar from './Bar'
import Graphics from '../modules/Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts BarStacked Class responsible for drawing both Stacked Columns and Bars.
*
* @module BarStacked
* The whole calculation for stacked bar/column is different from normal bar/column,
* hence it makes sense to derive a new class for it extending most of the props of Parent Bar
**/
class BarStacked extends Bar {
draw(series, seriesIndex) {
let w = this.w
this.graphics = new Graphics(this.ctx)
this.bar = new Bar(this.ctx, this.xyRatios)
const coreUtils = new CoreUtils(this.ctx, w)
series = coreUtils.getLogSeries(series)
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
this.barHelpers.initVariables(series)
if (w.config.chart.stackType === '100%') {
series = w.globals.seriesPercent.slice()
}
this.series = series
this.barHelpers.initializeStackedPrevVars(this)
let ret = this.graphics.group({
class: 'apexcharts-bar-series apexcharts-plot-series',
})
let x = 0
let y = 0
for (let i = 0, bc = 0; i < series.length; i++, bc++) {
let xDivision // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
let yDivision // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
let zeroH // zeroH is the baseline where 0 meets y axis
let zeroW // zeroW is the baseline where 0 meets x axis
let groupIndex = -1 // groupIndex is the index of group buckets (group1, group2, ...)
this.groupCtx = this
w.globals.seriesGroups.forEach((group, gIndex) => {
if (group.indexOf(w.config.series[i].name) > -1) {
groupIndex = gIndex
}
})
if (groupIndex !== -1) {
this.groupCtx = this[w.globals.seriesGroups[groupIndex]]
}
let xArrValues = []
let yArrValues = []
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
this.isReversed =
w.config.yaxis[this.yaxisIndex] &&
w.config.yaxis[this.yaxisIndex].reversed
// el to which series will be drawn
let elSeries = this.graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
rel: i + 1,
'data:realIndex': realIndex,
})
this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex)
// eldatalabels
let elDataLabelsWrap = this.graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex,
})
let elGoalsMarkers = this.graphics.group({
class: 'apexcharts-bar-goals-markers',
})
let barHeight = 0
let barWidth = 0
let initPositions = this.initialPositions(
x,
y,
xDivision,
yDivision,
zeroH,
zeroW
)
y = initPositions.y
barHeight = initPositions.barHeight
yDivision = initPositions.yDivision
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
xDivision = initPositions.xDivision
zeroH = initPositions.zeroH
w.globals.barHeight = barHeight
w.globals.barWidth = barWidth
this.barHelpers.initializeStackedXYVars(this)
// where all stack bar disappear after collapsing the first series
if (
this.groupCtx.prevY.length === 1 &&
this.groupCtx.prevY[0].every((val) => isNaN(val))
) {
this.groupCtx.prevY[0] = this.groupCtx.prevY[0].map((val) => zeroH)
this.groupCtx.prevYF[0] = this.groupCtx.prevYF[0].map((val) => 0)
}
for (let j = 0; j < w.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)
const commonPathOpts = {
indexes: { i, j, realIndex, bc },
strokeWidth,
x,
y,
elSeries,
groupIndex,
seriesGroup: w.globals.seriesGroups[groupIndex],
}
let paths = null
if (this.isHorizontal) {
paths = this.drawStackedBarPaths({
...commonPathOpts,
zeroW,
barHeight,
yDivision,
})
barWidth = this.series[i][j] / this.invertedYRatio
} else {
paths = this.drawStackedColumnPaths({
...commonPathOpts,
xDivision,
barWidth,
zeroH,
})
barHeight = this.series[i][j] / this.yRatio[this.yaxisIndex]
}
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition: paths.barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth,
})
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine)
}
y = paths.y
x = paths.x
xArrValues.push(x)
yArrValues.push(y)
let pathFill = this.barHelpers.getPathFillColor(series, i, j, realIndex)
elSeries = this.renderSeries({
realIndex,
pathFill,
j,
i,
groupIndex,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
x,
y,
series,
barHeight,
barWidth,
elDataLabelsWrap,
elGoalsMarkers,
type: 'bar',
visibleSeries: 0,
})
}
// push all x val arrays into main xArr
w.globals.seriesXvalues[realIndex] = xArrValues
w.globals.seriesYvalues[realIndex] = yArrValues
// push all current y values array to main PrevY Array
this.groupCtx.prevY.push(this.groupCtx.yArrj)
this.groupCtx.prevYF.push(this.groupCtx.yArrjF)
this.groupCtx.prevYVal.push(this.groupCtx.yArrjVal)
this.groupCtx.prevX.push(this.groupCtx.xArrj)
this.groupCtx.prevXF.push(this.groupCtx.xArrjF)
this.groupCtx.prevXVal.push(this.groupCtx.xArrjVal)
ret.add(elSeries)
}
return ret
}
initialPositions(x, y, xDivision, yDivision, zeroH, zeroW) {
let w = this.w
let barHeight, barWidth
if (this.isHorizontal) {
// height divided into equal parts
yDivision = w.globals.gridHeight / w.globals.dataPoints
barHeight = yDivision
barHeight =
(barHeight * parseInt(w.config.plotOptions.bar.barHeight, 10)) / 100
if (String(w.config.plotOptions.bar.barHeight).indexOf('%') === -1) {
barHeight = parseInt(w.config.plotOptions.bar.barHeight, 10)
}
zeroW =
this.baseLineInvertedY +
w.globals.padHorizontal +
(this.isReversed ? w.globals.gridWidth : 0) -
(this.isReversed ? this.baseLineInvertedY * 2 : 0)
// initial y position is half of barHeight * half of number of Bars
y = (yDivision - barHeight) / 2
} else {
// width divided into equal parts
xDivision = w.globals.gridWidth / w.globals.dataPoints
barWidth = xDivision
if (w.globals.isXNumeric && w.globals.dataPoints > 1) {
// the check (w.globals.dataPoints > 1) fixes apexcharts.js #1617
xDivision = w.globals.minXDiff / this.xRatio
barWidth = (xDivision * parseInt(this.barOptions.columnWidth, 10)) / 100
} else {
barWidth =
(barWidth * parseInt(w.config.plotOptions.bar.columnWidth, 10)) / 100
}
if (String(w.config.plotOptions.bar.columnWidth).indexOf('%') === -1) {
barWidth = parseInt(w.config.plotOptions.bar.columnWidth, 10)
}
zeroH =
w.globals.gridHeight -
this.baseLineY[this.yaxisIndex] -
(this.isReversed ? w.globals.gridHeight : 0) +
(this.isReversed ? this.baseLineY[this.yaxisIndex] * 2 : 0)
// initial x position is one third of barWidth
x = w.globals.padHorizontal + (xDivision - barWidth) / 2
}
return {
x,
y,
yDivision,
xDivision,
barHeight: w.globals.seriesGroups?.length
? barHeight / w.globals.seriesGroups.length
: barHeight,
barWidth: w.globals.seriesGroups?.length
? barWidth / w.globals.seriesGroups.length
: barWidth,
zeroH,
zeroW,
}
}
drawStackedBarPaths({
indexes,
barHeight,
strokeWidth,
zeroW,
x,
y,
groupIndex,
seriesGroup,
yDivision,
elSeries,
}) {
let w = this.w
let barYPosition = y + (groupIndex !== -1 ? groupIndex * barHeight : 0)
let barXPosition
let i = indexes.i
let j = indexes.j
let prevBarW = 0
for (let k = 0; k < this.groupCtx.prevXF.length; k++) {
prevBarW = prevBarW + this.groupCtx.prevXF[k][j]
}
let gsi = i // an index to keep track of the series inside a group
if (seriesGroup) {
gsi = seriesGroup.indexOf(w.config.series[i].name)
}
if (gsi > 0) {
let bXP = zeroW
if (this.groupCtx.prevXVal[gsi - 1][j] < 0) {
bXP =
this.series[i][j] >= 0
? this.groupCtx.prevX[gsi - 1][j] +
prevBarW -
(this.isReversed ? prevBarW : 0) * 2
: this.groupCtx.prevX[gsi - 1][j]
} else if (this.groupCtx.prevXVal[gsi - 1][j] >= 0) {
bXP =
this.series[i][j] >= 0
? this.groupCtx.prevX[gsi - 1][j]
: this.groupCtx.prevX[gsi - 1][j] -
prevBarW +
(this.isReversed ? prevBarW : 0) * 2
}
barXPosition = bXP
} else {
// the first series will not have prevX values
barXPosition = zeroW
}
if (this.series[i][j] === null) {
x = barXPosition
} else {
x =
barXPosition +
this.series[i][j] / this.invertedYRatio -
(this.isReversed ? this.series[i][j] / this.invertedYRatio : 0) * 2
}
const paths = this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1: barXPosition,
x2: x,
strokeWidth,
series: this.series,
realIndex: indexes.realIndex,
seriesGroup,
i,
j,
w,
})
this.barHelpers.barBackground({
j,
i,
y1: barYPosition,
y2: barHeight,
elSeries,
})
y = y + yDivision
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
goalX: this.barHelpers.getGoalValues('x', zeroW, null, i, j),
barYPosition,
x,
y,
}
}
drawStackedColumnPaths({
indexes,
x,
y,
xDivision,
barWidth,
zeroH,
groupIndex,
seriesGroup,
elSeries,
}) {
let w = this.w
let i = indexes.i
let j = indexes.j
let bc = indexes.bc
if (w.globals.isXNumeric) {
let seriesVal = w.globals.seriesX[i][j]
if (!seriesVal) seriesVal = 0
x = (seriesVal - w.globals.minX) / this.xRatio - barWidth / 2
if (w.globals.seriesGroups.length) {
x =
(seriesVal - w.globals.minX) / this.xRatio -
(barWidth / 2) * w.globals.seriesGroups.length
}
}
let barXPosition = x + (groupIndex !== -1 ? groupIndex * barWidth : 0)
let barYPosition
let prevBarH = 0
for (let k = 0; k < this.groupCtx.prevYF.length; k++) {
// fix issue #1215
// in case where this.groupCtx.prevYF[k][j] is NaN, use 0 instead
prevBarH =
prevBarH +
(!isNaN(this.groupCtx.prevYF[k][j]) ? this.groupCtx.prevYF[k][j] : 0)
}
let gsi = i // an index to keep track of the series inside a group
if (seriesGroup) {
gsi = seriesGroup.indexOf(w.config.series[i].name)
}
if (
(gsi > 0 && !w.globals.isXNumeric) ||
(gsi > 0 &&
w.globals.isXNumeric &&
w.globals.seriesX[i - 1][j] === w.globals.seriesX[i][j])
) {
let bYP
let prevYValue
const p = Math.min(this.yRatio.length + 1, i + 1)
if (
this.groupCtx.prevY[gsi - 1] !== undefined &&
this.groupCtx.prevY[gsi - 1].length
) {
for (let ii = 1; ii < p; ii++) {
if (!isNaN(this.groupCtx.prevY[gsi - ii]?.[j])) {
// find the previous available value to give prevYValue
prevYValue = this.groupCtx.prevY[gsi - ii][j]
// if found it, break the loop
break
}
}
}
for (let ii = 1; ii < p; ii++) {
// find the previous available value(non-NaN) to give bYP
if (this.groupCtx.prevYVal[gsi - ii]?.[j] < 0) {
bYP =
this.series[i][j] >= 0
? prevYValue - prevBarH + (this.isReversed ? prevBarH : 0) * 2
: prevYValue
// found it? break the loop
break
} else if (this.groupCtx.prevYVal[gsi - ii]?.[j] >= 0) {
bYP =
this.series[i][j] >= 0
? prevYValue
: prevYValue + prevBarH - (this.isReversed ? prevBarH : 0) * 2
// found it? break the loop
break
}
}
if (typeof bYP === 'undefined') bYP = w.globals.gridHeight
// if this.prevYF[0] is all 0 resulted from line #486
// AND every arr starting from the second only contains NaN
if (
this.groupCtx.prevYF[0]?.every((val) => val === 0) &&
this.groupCtx.prevYF
.slice(1, gsi)
.every((arr) => arr.every((val) => isNaN(val)))
) {
barYPosition = zeroH
} else {
// Nothing special
barYPosition = bYP
}
} else {
// the first series will not have prevY values, also if the prev index's series X doesn't matches the current index's series X, then start from zero
barYPosition = zeroH
}
if (this.series[i][j]) {
y =
barYPosition -
this.series[i][j] / this.yRatio[this.yaxisIndex] +
(this.isReversed
? this.series[i][j] / this.yRatio[this.yaxisIndex]
: 0) *
2
} else {
// fixes #3610
y = barYPosition
}
const paths = this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1: barYPosition,
y2: y,
yRatio: this.yRatio[this.yaxisIndex],
strokeWidth: this.strokeWidth,
series: this.series,
seriesGroup,
realIndex: indexes.realIndex,
i,
j,
w,
})
this.barHelpers.barBackground({
bc,
j,
i,
x1: barXPosition,
x2: barWidth,
elSeries,
})
x = x + xDivision
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
goalY: this.barHelpers.getGoalValues('y', null, zeroH, i, j),
barXPosition,
x: w.globals.isXNumeric ? x - xDivision : x,
y,
}
}
}
export default BarStacked
+429
View File
@@ -0,0 +1,429 @@
import CoreUtils from '../modules/CoreUtils'
import Bar from './Bar'
import Fill from '../modules/Fill'
import Graphics from '../modules/Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts BoxCandleStick Class responsible for drawing both Stacked Columns and Bars.
*
* @module BoxCandleStick
**/
class BoxCandleStick extends Bar {
draw(series, ctype, seriesIndex) {
let w = this.w
let graphics = new Graphics(this.ctx)
let type = w.globals.comboCharts ? ctype : w.config.chart.type
let fill = new Fill(this.ctx)
this.candlestickOptions = this.w.config.plotOptions.candlestick
this.boxOptions = this.w.config.plotOptions.boxPlot
this.isHorizontal = w.config.plotOptions.bar.horizontal
const coreUtils = new CoreUtils(this.ctx, w)
series = coreUtils.getLogSeries(series)
this.series = series
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
this.barHelpers.initVariables(series)
let ret = graphics.group({
class: `apexcharts-${type}-series apexcharts-plot-series`
})
for (let i = 0; i < series.length; i++) {
this.isBoxPlot =
w.config.chart.type === 'boxPlot' ||
w.config.series[i].type === 'boxPlot'
let x,
y,
xDivision, // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
yDivision, // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroH, // zeroH is the baseline where 0 meets y axis
zeroW // zeroW is the baseline where 0 meets x axis
let yArrj = [] // hold y values of current iterating series
let xArrj = [] // hold x values of current iterating series
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
// el to which series will be drawn
let elSeries = graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
rel: i + 1,
'data:realIndex': realIndex
})
this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex)
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1
}
let barHeight = 0
let barWidth = 0
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
let initPositions = this.barHelpers.initialPositions()
y = initPositions.y
barHeight = initPositions.barHeight
yDivision = initPositions.yDivision
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
xDivision = initPositions.xDivision
zeroH = initPositions.zeroH
xArrj.push(x + barWidth / 2)
// eldatalabels
let elDataLabelsWrap = graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex
})
for (let j = 0; j < w.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)
let paths = null
const pathsParams = {
indexes: {
i,
j,
realIndex
},
x,
y,
strokeWidth,
elSeries
}
if (this.isHorizontal) {
paths = this.drawHorizontalBoxPaths({
...pathsParams,
yDivision,
barHeight,
zeroW
})
} else {
paths = this.drawVerticalBoxPaths({
...pathsParams,
xDivision,
barWidth,
zeroH
})
}
y = paths.y
x = paths.x
// push current X
if (j > 0) {
xArrj.push(x + barWidth / 2)
}
yArrj.push(y)
paths.pathTo.forEach((pathTo, pi) => {
let lineFill =
!this.isBoxPlot && this.candlestickOptions.wick.useFillColor
? paths.color[pi]
: w.globals.stroke.colors[i]
let pathFill = fill.fillPath({
seriesNumber: realIndex,
dataPointIndex: j,
color: paths.color[pi],
value: series[i][j]
})
this.renderSeries({
realIndex,
pathFill,
lineFill,
j,
i,
pathFrom: paths.pathFrom,
pathTo,
strokeWidth,
elSeries,
x,
y,
series,
barHeight,
barWidth,
elDataLabelsWrap,
visibleSeries: this.visibleI,
type: w.config.chart.type
})
})
}
// push all x val arrays into main xArr
w.globals.seriesXvalues[realIndex] = xArrj
w.globals.seriesYvalues[realIndex] = yArrj
ret.add(elSeries)
}
return ret
}
drawVerticalBoxPaths({
indexes,
x,
y,
xDivision,
barWidth,
zeroH,
strokeWidth
}) {
let w = this.w
let graphics = new Graphics(this.ctx)
let i = indexes.i
let j = indexes.j
let isPositive = true
let colorPos = w.config.plotOptions.candlestick.colors.upward
let colorNeg = w.config.plotOptions.candlestick.colors.downward
let color = ''
if (this.isBoxPlot) {
color = [this.boxOptions.colors.lower, this.boxOptions.colors.upper]
}
const yRatio = this.yRatio[this.yaxisIndex]
let realIndex = indexes.realIndex
const ohlc = this.getOHLCValue(realIndex, j)
let l1 = zeroH
let l2 = zeroH
if (ohlc.o > ohlc.c) {
isPositive = false
}
let y1 = Math.min(ohlc.o, ohlc.c)
let y2 = Math.max(ohlc.o, ohlc.c)
let m = ohlc.m
if (w.globals.isXNumeric) {
x =
(w.globals.seriesX[realIndex][j] - w.globals.minX) / this.xRatio -
barWidth / 2
}
let barXPosition = x + barWidth * this.visibleI
if (
typeof this.series[i][j] === 'undefined' ||
this.series[i][j] === null
) {
y1 = zeroH
y2 = zeroH
} else {
y1 = zeroH - y1 / yRatio
y2 = zeroH - y2 / yRatio
l1 = zeroH - ohlc.h / yRatio
l2 = zeroH - ohlc.l / yRatio
m = zeroH - ohlc.m / yRatio
}
let pathTo = graphics.move(barXPosition, zeroH)
let pathFrom = graphics.move(barXPosition + barWidth / 2, y1)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(realIndex, j, true)
}
if (this.isBoxPlot) {
pathTo = [
graphics.move(barXPosition, y1) +
graphics.line(barXPosition + barWidth / 2, y1) +
graphics.line(barXPosition + barWidth / 2, l1) +
graphics.line(barXPosition + barWidth / 4, l1) +
graphics.line(barXPosition + barWidth - barWidth / 4, l1) +
graphics.line(barXPosition + barWidth / 2, l1) +
graphics.line(barXPosition + barWidth / 2, y1) +
graphics.line(barXPosition + barWidth, y1) +
graphics.line(barXPosition + barWidth, m) +
graphics.line(barXPosition, m) +
graphics.line(barXPosition, y1 + strokeWidth / 2),
graphics.move(barXPosition, m) +
graphics.line(barXPosition + barWidth, m) +
graphics.line(barXPosition + barWidth, y2) +
graphics.line(barXPosition + barWidth / 2, y2) +
graphics.line(barXPosition + barWidth / 2, l2) +
graphics.line(barXPosition + barWidth - barWidth / 4, l2) +
graphics.line(barXPosition + barWidth / 4, l2) +
graphics.line(barXPosition + barWidth / 2, l2) +
graphics.line(barXPosition + barWidth / 2, y2) +
graphics.line(barXPosition, y2) +
graphics.line(barXPosition, m) +
'z'
]
} else {
// candlestick
pathTo = [
graphics.move(barXPosition, y2) +
graphics.line(barXPosition + barWidth / 2, y2) +
graphics.line(barXPosition + barWidth / 2, l1) +
graphics.line(barXPosition + barWidth / 2, y2) +
graphics.line(barXPosition + barWidth, y2) +
graphics.line(barXPosition + barWidth, y1) +
graphics.line(barXPosition + barWidth / 2, y1) +
graphics.line(barXPosition + barWidth / 2, l2) +
graphics.line(barXPosition + barWidth / 2, y1) +
graphics.line(barXPosition, y1) +
graphics.line(barXPosition, y2 - strokeWidth / 2)
]
}
pathFrom = pathFrom + graphics.move(barXPosition, y1)
if (!w.globals.isXNumeric) {
x = x + xDivision
}
return {
pathTo,
pathFrom,
x,
y: y2,
barXPosition,
color: this.isBoxPlot ? color : isPositive ? [colorPos] : [colorNeg]
}
}
drawHorizontalBoxPaths({
indexes,
x,
y,
yDivision,
barHeight,
zeroW,
strokeWidth
}) {
let w = this.w
let graphics = new Graphics(this.ctx)
let i = indexes.i
let j = indexes.j
let color = this.boxOptions.colors.lower
if (this.isBoxPlot) {
color = [this.boxOptions.colors.lower, this.boxOptions.colors.upper]
}
const yRatio = this.invertedYRatio
let realIndex = indexes.realIndex
const ohlc = this.getOHLCValue(realIndex, j)
let l1 = zeroW
let l2 = zeroW
let x1 = Math.min(ohlc.o, ohlc.c)
let x2 = Math.max(ohlc.o, ohlc.c)
let m = ohlc.m
if (w.globals.isXNumeric) {
y =
(w.globals.seriesX[realIndex][j] - w.globals.minX) /
this.invertedXRatio -
barHeight / 2
}
let barYPosition = y + barHeight * this.visibleI
if (
typeof this.series[i][j] === 'undefined' ||
this.series[i][j] === null
) {
x1 = zeroW
x2 = zeroW
} else {
x1 = zeroW + x1 / yRatio
x2 = zeroW + x2 / yRatio
l1 = zeroW + ohlc.h / yRatio
l2 = zeroW + ohlc.l / yRatio
m = zeroW + ohlc.m / yRatio
}
let pathTo = graphics.move(zeroW, barYPosition)
let pathFrom = graphics.move(x1, barYPosition + barHeight / 2)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(realIndex, j, true)
}
pathTo = [
graphics.move(x1, barYPosition) +
graphics.line(x1, barYPosition + barHeight / 2) +
graphics.line(l1, barYPosition + barHeight / 2) +
graphics.line(l1, barYPosition + barHeight / 2 - barHeight / 4) +
graphics.line(l1, barYPosition + barHeight / 2 + barHeight / 4) +
graphics.line(l1, barYPosition + barHeight / 2) +
graphics.line(x1, barYPosition + barHeight / 2) +
graphics.line(x1, barYPosition + barHeight) +
graphics.line(m, barYPosition + barHeight) +
graphics.line(m, barYPosition) +
graphics.line(x1 + strokeWidth / 2, barYPosition),
graphics.move(m, barYPosition) +
graphics.line(m, barYPosition + barHeight) +
graphics.line(x2, barYPosition + barHeight) +
graphics.line(x2, barYPosition + barHeight / 2) +
graphics.line(l2, barYPosition + barHeight / 2) +
graphics.line(l2, barYPosition + barHeight - barHeight / 4) +
graphics.line(l2, barYPosition + barHeight / 4) +
graphics.line(l2, barYPosition + barHeight / 2) +
graphics.line(x2, barYPosition + barHeight / 2) +
graphics.line(x2, barYPosition) +
graphics.line(m, barYPosition) +
'z'
]
pathFrom = pathFrom + graphics.move(x1, barYPosition)
if (!w.globals.isXNumeric) {
y = y + yDivision
}
return {
pathTo,
pathFrom,
x: x2,
y,
barYPosition,
color
}
}
getOHLCValue(i, j) {
const w = this.w
return {
o: this.isBoxPlot
? w.globals.seriesCandleH[i][j]
: w.globals.seriesCandleO[i][j],
h: this.isBoxPlot
? w.globals.seriesCandleO[i][j]
: w.globals.seriesCandleH[i][j],
m: w.globals.seriesCandleM[i][j],
l: this.isBoxPlot
? w.globals.seriesCandleC[i][j]
: w.globals.seriesCandleL[i][j],
c: this.isBoxPlot
? w.globals.seriesCandleL[i][j]
: w.globals.seriesCandleC[i][j]
}
}
}
export default BoxCandleStick
+240
View File
@@ -0,0 +1,240 @@
import Animations from '../modules/Animations'
import Graphics from '../modules/Graphics'
import Fill from '../modules/Fill'
import Utils from '../utils/Utils'
import Helpers from './common/treemap/Helpers'
import Filters from '../modules/Filters'
/**
* ApexCharts HeatMap Class.
* @module HeatMap
**/
export default class HeatMap {
constructor(ctx, xyRatios) {
this.ctx = ctx
this.w = ctx.w
this.xRatio = xyRatios.xRatio
this.yRatio = xyRatios.yRatio
this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation
this.helpers = new Helpers(ctx)
this.rectRadius = this.w.config.plotOptions.heatmap.radius
this.strokeWidth = this.w.config.stroke.show
? this.w.config.stroke.width
: 0
}
draw(series) {
let w = this.w
const graphics = new Graphics(this.ctx)
let ret = graphics.group({
class: 'apexcharts-heatmap',
})
ret.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
// width divided into equal parts
let xDivision = w.globals.gridWidth / w.globals.dataPoints
let yDivision = w.globals.gridHeight / w.globals.series.length
let y1 = 0
let rev = false
this.negRange = this.helpers.checkColorRange()
let heatSeries = series.slice()
if (w.config.yaxis[0].reversed) {
rev = true
heatSeries.reverse()
}
for (
let i = rev ? 0 : heatSeries.length - 1;
rev ? i < heatSeries.length : i >= 0;
rev ? i++ : i--
) {
// el to which series will be drawn
let elSeries = graphics.group({
class: `apexcharts-series apexcharts-heatmap-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[i]),
rel: i + 1,
'data:realIndex': i,
})
this.ctx.series.addCollapsedClassToSeries(elSeries, i)
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow
const filters = new Filters(this.ctx)
filters.dropShadow(elSeries, shadow, i)
}
let x1 = 0
let shadeIntensity = w.config.plotOptions.heatmap.shadeIntensity
for (let j = 0; j < heatSeries[i].length; j++) {
let heatColor = this.helpers.getShadeColor(
w.config.chart.type,
i,
j,
this.negRange
)
let color = heatColor.color
let heatColorProps = heatColor.colorProps
if (w.config.fill.type === 'image') {
const fill = new Fill(this.ctx)
color = fill.fillPath({
seriesNumber: i,
dataPointIndex: j,
opacity: w.globals.hasNegs
? heatColorProps.percent < 0
? 1 - (1 + heatColorProps.percent / 100)
: shadeIntensity + heatColorProps.percent / 100
: heatColorProps.percent / 100,
patternID: Utils.randomId(),
width: w.config.fill.image.width
? w.config.fill.image.width
: xDivision,
height: w.config.fill.image.height
? w.config.fill.image.height
: yDivision,
})
}
let radius = this.rectRadius
let rect = graphics.drawRect(x1, y1, xDivision, yDivision, radius)
rect.attr({
cx: x1,
cy: y1,
})
rect.node.classList.add('apexcharts-heatmap-rect')
elSeries.add(rect)
rect.attr({
fill: color,
i,
index: i,
j,
val: series[i][j],
'stroke-width': this.strokeWidth,
stroke: w.config.plotOptions.heatmap.useFillColorAsStroke
? color
: w.globals.stroke.colors[0],
color,
})
this.helpers.addListeners(rect)
if (w.config.chart.animations.enabled && !w.globals.dataChanged) {
let speed = 1
if (!w.globals.resized) {
speed = w.config.chart.animations.speed
}
this.animateHeatMap(rect, x1, y1, xDivision, yDivision, speed)
}
if (w.globals.dataChanged) {
let speed = 1
if (this.dynamicAnim.enabled && w.globals.shouldAnimate) {
speed = this.dynamicAnim.speed
let colorFrom =
w.globals.previousPaths[i] &&
w.globals.previousPaths[i][j] &&
w.globals.previousPaths[i][j].color
if (!colorFrom) colorFrom = 'rgba(255, 255, 255, 0)'
this.animateHeatColor(
rect,
Utils.isColorHex(colorFrom)
? colorFrom
: Utils.rgb2hex(colorFrom),
Utils.isColorHex(color) ? color : Utils.rgb2hex(color),
speed
)
}
}
let formatter = w.config.dataLabels.formatter
let formattedText = formatter(w.globals.series[i][j], {
value: w.globals.series[i][j],
seriesIndex: i,
dataPointIndex: j,
w,
})
let dataLabels = this.helpers.calculateDataLabels({
text: formattedText,
x: x1 + xDivision / 2,
y: y1 + yDivision / 2,
i,
j,
colorProps: heatColorProps,
series: heatSeries,
})
if (dataLabels !== null) {
elSeries.add(dataLabels)
}
x1 = x1 + xDivision
}
y1 = y1 + yDivision
ret.add(elSeries)
}
// adjust yaxis labels for heatmap
let yAxisScale = w.globals.yAxisScale[0].result.slice()
if (w.config.yaxis[0].reversed) {
yAxisScale.unshift('')
} else {
yAxisScale.push('')
}
w.globals.yAxisScale[0].result = yAxisScale
return ret
}
animateHeatMap(el, x, y, width, height, speed) {
const animations = new Animations(this.ctx)
animations.animateRect(
el,
{
x: x + width / 2,
y: y + height / 2,
width: 0,
height: 0,
},
{
x,
y,
width,
height,
},
speed,
() => {
animations.animationCompleted(el)
}
)
}
animateHeatColor(el, colorFrom, colorTo, speed) {
el.attr({
fill: colorFrom,
})
.animate(speed)
.attr({
fill: colorTo,
})
}
}
+929
View File
@@ -0,0 +1,929 @@
import CoreUtils from '../modules/CoreUtils'
import Graphics from '../modules/Graphics'
import Fill from '../modules/Fill'
import DataLabels from '../modules/DataLabels'
import Markers from '../modules/Markers'
import Scatter from './Scatter'
import Utils from '../utils/Utils'
import Helpers from './common/line/Helpers'
import { svgPath, spline } from '../libs/monotone-cubic'
/**
* ApexCharts Line Class responsible for drawing Line / Area / RangeArea Charts.
* This class is also responsible for generating values for Bubble/Scatter charts, so need to rename it to Axis Charts to avoid confusions
* @module Line
**/
class Line {
constructor(ctx, xyRatios, isPointsChart) {
this.ctx = ctx
this.w = ctx.w
this.xyRatios = xyRatios
this.pointsChart =
!(
this.w.config.chart.type !== 'bubble' &&
this.w.config.chart.type !== 'scatter'
) || isPointsChart
this.scatter = new Scatter(this.ctx)
this.noNegatives = this.w.globals.minX === Number.MAX_VALUE
this.lineHelpers = new Helpers(this)
this.markers = new Markers(this.ctx)
this.prevSeriesY = []
this.categoryAxisCorrection = 0
this.yaxisIndex = 0
}
draw(series, ctype, seriesIndex, seriesRangeEnd) {
let w = this.w
let graphics = new Graphics(this.ctx)
let type = w.globals.comboCharts ? ctype : w.config.chart.type
let ret = graphics.group({
class: `apexcharts-${type}-series apexcharts-plot-series`,
})
const coreUtils = new CoreUtils(this.ctx, w)
this.yRatio = this.xyRatios.yRatio
this.zRatio = this.xyRatios.zRatio
this.xRatio = this.xyRatios.xRatio
this.baseLineY = this.xyRatios.baseLineY
series = coreUtils.getLogSeries(series)
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
// push all series in an array, so we can draw in reverse order (for stacked charts)
let allSeries = []
for (let i = 0; i < series.length; i++) {
series = this.lineHelpers.sameValueSeriesFix(i, series)
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
this._initSerieVariables(series, i, realIndex)
let yArrj = [] // hold y values of current iterating series
let y2Arrj = [] // holds y2 values in range-area charts
let xArrj = [] // hold x values of current iterating series
let x = w.globals.padHorizontal + this.categoryAxisCorrection
let y = 1
let linePaths = []
let areaPaths = []
this.ctx.series.addCollapsedClassToSeries(this.elSeries, realIndex)
if (w.globals.isXNumeric && w.globals.seriesX.length > 0) {
x = (w.globals.seriesX[realIndex][0] - w.globals.minX) / this.xRatio
}
xArrj.push(x)
let pX = x
let pY
let pY2
let prevX = pX
let prevY = this.zeroY
let prevY2 = this.zeroY
let lineYPosition = 0
// the first value in the current series is not null or undefined
let firstPrevY = this.lineHelpers.determineFirstPrevY({
i,
series,
prevY,
lineYPosition,
})
prevY = firstPrevY.prevY
if (w.config.stroke.curve === 'monotonCubic' && series[i][0] === null) {
// we have to discard the y position if 1st dataPoint is null as it causes issues with monotoneCubic path creation
yArrj.push(null)
} else {
yArrj.push(prevY)
}
pY = prevY
// y2 are needed for range-area charts
let firstPrevY2
if (type === 'rangeArea') {
firstPrevY2 = this.lineHelpers.determineFirstPrevY({
i,
series: seriesRangeEnd,
prevY: prevY2,
lineYPosition,
})
prevY2 = firstPrevY2.prevY
pY2 = prevY2
y2Arrj.push(prevY2)
}
let pathsFrom = this._calculatePathsFrom({
type,
series,
i,
realIndex,
prevX,
prevY,
prevY2,
})
const iteratingOpts = {
type,
series,
realIndex,
i,
x,
y,
pX,
pY,
pathsFrom,
linePaths,
areaPaths,
seriesIndex,
lineYPosition,
xArrj,
yArrj,
y2Arrj,
seriesRangeEnd,
}
let paths = this._iterateOverDataPoints({
...iteratingOpts,
iterations: type === 'rangeArea' ? series[i].length - 1 : undefined,
isRangeStart: true,
})
if (type === 'rangeArea') {
let pathsFrom2 = this._calculatePathsFrom({
series: seriesRangeEnd,
i,
realIndex,
prevX,
prevY: prevY2,
})
let rangePaths = this._iterateOverDataPoints({
...iteratingOpts,
series: seriesRangeEnd,
pY: pY2,
pathsFrom: pathsFrom2,
iterations: seriesRangeEnd[i].length - 1,
isRangeStart: false,
})
paths.linePaths[0] = rangePaths.linePath + paths.linePath
paths.pathFromLine = rangePaths.pathFromLine + paths.pathFromLine
}
this._handlePaths({ type, realIndex, i, paths })
this.elSeries.add(this.elPointsMain)
this.elSeries.add(this.elDataLabelsWrap)
allSeries.push(this.elSeries)
}
if (typeof w.config.series[0]?.zIndex !== 'undefined') {
allSeries.sort(
(a, b) =>
Number(a.node.getAttribute('zIndex')) -
Number(b.node.getAttribute('zIndex'))
)
}
if (w.config.chart.stacked) {
for (let s = allSeries.length; s > 0; s--) {
ret.add(allSeries[s - 1])
}
} else {
for (let s = 0; s < allSeries.length; s++) {
ret.add(allSeries[s])
}
}
return ret
}
_initSerieVariables(series, i, realIndex) {
const w = this.w
const graphics = new Graphics(this.ctx)
// width divided into equal parts
this.xDivision =
w.globals.gridWidth /
(w.globals.dataPoints - (w.config.xaxis.tickPlacement === 'on' ? 1 : 0))
this.strokeWidth = Array.isArray(w.config.stroke.width)
? w.config.stroke.width[realIndex]
: w.config.stroke.width
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
this.isReversed =
w.config.yaxis[this.yaxisIndex] &&
w.config.yaxis[this.yaxisIndex].reversed
// zeroY is the 0 value in y series which can be used in negative charts
this.zeroY =
w.globals.gridHeight -
this.baseLineY[this.yaxisIndex] -
(this.isReversed ? w.globals.gridHeight : 0) +
(this.isReversed ? this.baseLineY[this.yaxisIndex] * 2 : 0)
this.areaBottomY = this.zeroY
if (
this.zeroY > w.globals.gridHeight ||
w.config.plotOptions.area.fillTo === 'end'
) {
this.areaBottomY = w.globals.gridHeight
}
this.categoryAxisCorrection = this.xDivision / 2
// el to which series will be drawn
this.elSeries = graphics.group({
class: `apexcharts-series`,
zIndex:
typeof w.config.series[realIndex].zIndex !== 'undefined'
? w.config.series[realIndex].zIndex
: realIndex,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
})
// points
this.elPointsMain = graphics.group({
class: 'apexcharts-series-markers-wrap',
'data:realIndex': realIndex,
})
// eldatalabels
this.elDataLabelsWrap = graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex,
})
let longestSeries = series[i].length === w.globals.dataPoints
this.elSeries.attr({
'data:longestSeries': longestSeries,
rel: i + 1,
'data:realIndex': realIndex,
})
this.appendPathFrom = true
}
_calculatePathsFrom({ type, series, i, realIndex, prevX, prevY, prevY2 }) {
const w = this.w
const graphics = new Graphics(this.ctx)
let linePath, areaPath, pathFromLine, pathFromArea
if (series[i][0] === null) {
// when the first value itself is null, we need to move the pointer to a location where a null value is not found
for (let s = 0; s < series[i].length; s++) {
if (series[i][s] !== null) {
prevX = this.xDivision * s
prevY = this.zeroY - series[i][s] / this.yRatio[this.yaxisIndex]
linePath = graphics.move(prevX, prevY)
areaPath = graphics.move(prevX, this.areaBottomY)
break
}
}
} else {
linePath = graphics.move(prevX, prevY)
if (type === 'rangeArea') {
linePath = graphics.move(prevX, prevY2) + graphics.line(prevX, prevY)
}
areaPath =
graphics.move(prevX, this.areaBottomY) + graphics.line(prevX, prevY)
}
pathFromLine = graphics.move(-1, this.zeroY) + graphics.line(-1, this.zeroY)
pathFromArea = graphics.move(-1, this.zeroY) + graphics.line(-1, this.zeroY)
if (w.globals.previousPaths.length > 0) {
const pathFrom = this.lineHelpers.checkPreviousPaths({
pathFromLine,
pathFromArea,
realIndex,
})
pathFromLine = pathFrom.pathFromLine
pathFromArea = pathFrom.pathFromArea
}
return {
prevX,
prevY,
linePath,
areaPath,
pathFromLine,
pathFromArea,
}
}
_handlePaths({ type, realIndex, i, paths }) {
const w = this.w
const graphics = new Graphics(this.ctx)
const fill = new Fill(this.ctx)
// push all current y values array to main PrevY Array
this.prevSeriesY.push(paths.yArrj)
// push all x val arrays into main xArr
w.globals.seriesXvalues[realIndex] = paths.xArrj
w.globals.seriesYvalues[realIndex] = paths.yArrj
const forecast = w.config.forecastDataPoints
if (forecast.count > 0 && type !== 'rangeArea') {
const forecastCutoff =
w.globals.seriesXvalues[realIndex][
w.globals.seriesXvalues[realIndex].length - forecast.count - 1
]
const elForecastMask = graphics.drawRect(
forecastCutoff,
0,
w.globals.gridWidth,
w.globals.gridHeight,
0
)
w.globals.dom.elForecastMask.appendChild(elForecastMask.node)
const elNonForecastMask = graphics.drawRect(
0,
0,
forecastCutoff,
w.globals.gridHeight,
0
)
w.globals.dom.elNonForecastMask.appendChild(elNonForecastMask.node)
}
// these elements will be shown after area path animation completes
if (!this.pointsChart) {
w.globals.delayedElements.push({
el: this.elPointsMain.node,
index: realIndex,
})
}
const defaultRenderedPathOptions = {
i,
realIndex,
animationDelay: i,
initialSpeed: w.config.chart.animations.speed,
dataChangeSpeed: w.config.chart.animations.dynamicAnimation.speed,
className: `apexcharts-${type}`,
}
if (type === 'area') {
let pathFill = fill.fillPath({
seriesNumber: realIndex,
})
for (let p = 0; p < paths.areaPaths.length; p++) {
let renderedPath = graphics.renderPaths({
...defaultRenderedPathOptions,
pathFrom: paths.pathFromArea,
pathTo: paths.areaPaths[p],
stroke: 'none',
strokeWidth: 0,
strokeLineCap: null,
fill: pathFill,
})
this.elSeries.add(renderedPath)
}
}
if (w.config.stroke.show && !this.pointsChart) {
let lineFill = null
if (type === 'line') {
lineFill = fill.fillPath({
seriesNumber: realIndex,
i,
})
} else {
if (w.config.stroke.fill.type === 'solid') {
lineFill = w.globals.stroke.colors[realIndex]
} else {
const prevFill = w.config.fill
w.config.fill = w.config.stroke.fill
lineFill = fill.fillPath({
seriesNumber: realIndex,
i,
})
w.config.fill = prevFill
}
}
// range-area paths are drawn using linePaths
for (let p = 0; p < paths.linePaths.length; p++) {
let pathFill = lineFill
if (type === 'rangeArea') {
pathFill = fill.fillPath({
seriesNumber: realIndex,
})
}
const linePathCommonOpts = {
...defaultRenderedPathOptions,
pathFrom: paths.pathFromLine,
pathTo: paths.linePaths[p],
stroke: lineFill,
strokeWidth: this.strokeWidth,
strokeLineCap: w.config.stroke.lineCap,
fill: type === 'rangeArea' ? pathFill : 'none',
}
let renderedPath = graphics.renderPaths(linePathCommonOpts)
this.elSeries.add(renderedPath)
renderedPath.attr('fill-rule', `evenodd`)
if (forecast.count > 0 && type !== 'rangeArea') {
let renderedForecastPath = graphics.renderPaths(linePathCommonOpts)
renderedForecastPath.node.setAttribute(
'stroke-dasharray',
forecast.dashArray
)
if (forecast.strokeWidth) {
renderedForecastPath.node.setAttribute(
'stroke-width',
forecast.strokeWidth
)
}
this.elSeries.add(renderedForecastPath)
renderedForecastPath.attr(
'clip-path',
`url(#forecastMask${w.globals.cuid})`
)
renderedPath.attr(
'clip-path',
`url(#nonForecastMask${w.globals.cuid})`
)
}
}
}
}
_iterateOverDataPoints({
type,
series,
iterations,
realIndex,
i,
x,
y,
pX,
pY,
pathsFrom,
linePaths,
areaPaths,
seriesIndex,
lineYPosition,
xArrj,
yArrj,
y2Arrj,
isRangeStart,
seriesRangeEnd,
}) {
const w = this.w
let graphics = new Graphics(this.ctx)
let yRatio = this.yRatio
let { prevY, linePath, areaPath, pathFromLine, pathFromArea } = pathsFrom
const minY = Utils.isNumber(w.globals.minYArr[realIndex])
? w.globals.minYArr[realIndex]
: w.globals.minY
if (!iterations) {
iterations =
w.globals.dataPoints > 1
? w.globals.dataPoints - 1
: w.globals.dataPoints
}
const getY = (_y, lineYPos) => {
return (
lineYPos -
_y / yRatio[this.yaxisIndex] +
(this.isReversed ? _y / yRatio[this.yaxisIndex] : 0) * 2
)
}
let y2 = y
let stackSeries =
(w.config.chart.stacked && !w.globals.comboCharts) ||
(w.config.chart.stacked &&
w.globals.comboCharts &&
(!this.w.config.chart.stackOnlyBar ||
this.w.config.series[realIndex]?.type === 'bar'))
for (let j = 0; j < iterations; j++) {
const isNull =
typeof series[i][j + 1] === 'undefined' || series[i][j + 1] === null
if (w.globals.isXNumeric) {
let sX = w.globals.seriesX[realIndex][j + 1]
if (typeof w.globals.seriesX[realIndex][j + 1] === 'undefined') {
/* fix #374 */
sX = w.globals.seriesX[realIndex][iterations - 1]
}
x = (sX - w.globals.minX) / this.xRatio
} else {
x = x + this.xDivision
}
if (stackSeries) {
if (
i > 0 &&
w.globals.collapsedSeries.length < w.config.series.length - 1
) {
// a collapsed series in a stacked bar chart may provide wrong result for the next series, hence find the prevIndex of prev series which is not collapsed - fixes apexcharts.js#1372
const prevIndex = (pi) => {
let pii = pi
for (let cpi = 0; cpi < w.globals.series.length; cpi++) {
if (w.globals.collapsedSeriesIndices.indexOf(pi) > -1) {
pii--
break
}
}
return pii >= 0 ? pii : 0
}
lineYPosition = this.prevSeriesY[prevIndex(i - 1)][j + 1]
} else {
// the first series will not have prevY values
lineYPosition = this.zeroY
}
} else {
lineYPosition = this.zeroY
}
if (isNull) {
y = getY(minY, lineYPosition)
} else {
y = getY(series[i][j + 1], lineYPosition)
if (type === 'rangeArea') {
y2 = getY(seriesRangeEnd[i][j + 1], lineYPosition)
}
}
// push current X
xArrj.push(x)
// push current Y that will be used as next series's bottom position
if (isNull && w.config.stroke.curve === 'smooth') {
yArrj.push(null)
} else {
yArrj.push(y)
}
y2Arrj.push(y2)
let pointsPos = this.lineHelpers.calculatePoints({
series,
x,
y,
realIndex,
i,
j,
prevY,
})
let calculatedPaths = this._createPaths({
type,
series,
i,
realIndex,
j,
x,
y,
y2,
xArrj,
yArrj,
y2Arrj,
pX,
pY,
linePath,
areaPath,
linePaths,
areaPaths,
seriesIndex,
isRangeStart,
})
areaPaths = calculatedPaths.areaPaths
linePaths = calculatedPaths.linePaths
pX = calculatedPaths.pX
pY = calculatedPaths.pY
areaPath = calculatedPaths.areaPath
linePath = calculatedPaths.linePath
if (
this.appendPathFrom &&
!(w.config.stroke.curve === 'monotoneCubic' && type === 'rangeArea')
) {
pathFromLine = pathFromLine + graphics.line(x, this.zeroY)
pathFromArea = pathFromArea + graphics.line(x, this.zeroY)
}
this.handleNullDataPoints(series, pointsPos, i, j, realIndex)
this._handleMarkersAndLabels({
type,
pointsPos,
i,
j,
realIndex,
isRangeStart,
})
}
return {
yArrj,
xArrj,
pathFromArea,
areaPaths,
pathFromLine,
linePaths,
linePath,
areaPath,
}
}
_handleMarkersAndLabels({ type, pointsPos, isRangeStart, i, j, realIndex }) {
const w = this.w
let dataLabels = new DataLabels(this.ctx)
if (!this.pointsChart) {
if (w.globals.series[i].length > 1) {
this.elPointsMain.node.classList.add('apexcharts-element-hidden')
}
let elPointsWrap = this.markers.plotChartMarkers(
pointsPos,
realIndex,
j + 1
)
if (elPointsWrap !== null) {
this.elPointsMain.add(elPointsWrap)
}
} else {
// scatter / bubble chart points creation
this.scatter.draw(this.elSeries, j, {
realIndex,
pointsPos,
zRatio: this.zRatio,
elParent: this.elPointsMain,
})
}
let drawnLabels = dataLabels.drawDataLabel({
type,
isRangeStart,
pos: pointsPos,
i: realIndex,
j: j + 1,
})
if (drawnLabels !== null) {
this.elDataLabelsWrap.add(drawnLabels)
}
}
_createPaths({
type,
series,
i,
realIndex,
j,
x,
y,
xArrj,
yArrj,
y2,
y2Arrj,
pX,
pY,
linePath,
areaPath,
linePaths,
areaPaths,
seriesIndex,
isRangeStart,
}) {
let w = this.w
let graphics = new Graphics(this.ctx)
let curve = w.config.stroke.curve
const areaBottomY = this.areaBottomY
if (Array.isArray(w.config.stroke.curve)) {
if (Array.isArray(seriesIndex)) {
curve = w.config.stroke.curve[seriesIndex[i]]
} else {
curve = w.config.stroke.curve[i]
}
}
if (
type === 'rangeArea' &&
(w.globals.hasNullValues || w.config.forecastDataPoints.count > 0) &&
curve === 'monotoneCubic'
) {
curve = 'straight'
}
if (curve === 'monotoneCubic') {
const shouldRenderMonotone =
type === 'rangeArea'
? xArrj.length === w.globals.dataPoints
: j === series[i].length - 2
const smoothInputs = xArrj
.map((_, i) => {
return [xArrj[i], yArrj[i]]
})
.filter((_) => _[1] !== null)
if (shouldRenderMonotone && smoothInputs.length > 1) {
const points = spline.points(smoothInputs)
linePath += svgPath(points)
if (series[i][0] === null) {
// if the first dataPoint is null, we use the linePath directly
areaPath = linePath
} else {
// else, we append the areaPath
areaPath += svgPath(points)
}
if (type === 'rangeArea' && isRangeStart) {
// draw the line to connect y with y2; then draw the other end of range
linePath += graphics.line(
xArrj[xArrj.length - 1],
y2Arrj[y2Arrj.length - 1]
)
const xArrjInversed = xArrj.slice().reverse()
const y2ArrjInversed = y2Arrj.slice().reverse()
const smoothInputsY2 = xArrjInversed.map((_, i) => {
return [xArrjInversed[i], y2ArrjInversed[i]]
})
const pointsY2 = spline.points(smoothInputsY2)
linePath += svgPath(pointsY2)
// in range area, we don't have separate line and area path
areaPath = linePath
} else {
areaPath +=
graphics.line(
smoothInputs[smoothInputs.length - 1][0],
areaBottomY
) +
graphics.line(smoothInputs[0][0], areaBottomY) +
graphics.move(smoothInputs[0][0], smoothInputs[0][1]) +
'z'
}
linePaths.push(linePath)
areaPaths.push(areaPath)
}
} else if (curve === 'smooth') {
let length = (x - pX) * 0.35
if (w.globals.hasNullValues) {
if (series[i][j] !== null) {
if (series[i][j + 1] !== null) {
linePath =
graphics.move(pX, pY) +
graphics.curve(pX + length, pY, x - length, y, x + 1, y)
areaPath =
graphics.move(pX + 1, pY) +
graphics.curve(pX + length, pY, x - length, y, x + 1, y) +
graphics.line(x, areaBottomY) +
graphics.line(pX, areaBottomY) +
'z'
} else {
linePath = graphics.move(pX, pY)
areaPath = graphics.move(pX, pY) + 'z'
}
}
linePaths.push(linePath)
areaPaths.push(areaPath)
} else {
linePath =
linePath + graphics.curve(pX + length, pY, x - length, y, x, y)
areaPath =
areaPath + graphics.curve(pX + length, pY, x - length, y, x, y)
}
pX = x
pY = y
if (j === series[i].length - 2) {
// last loop, close path
areaPath =
areaPath +
graphics.curve(pX, pY, x, y, x, areaBottomY) +
graphics.move(x, y) +
'z'
if (type === 'rangeArea' && isRangeStart) {
linePath =
linePath +
graphics.curve(pX, pY, x, y, x, y2) +
graphics.move(x, y2) +
'z'
} else {
if (!w.globals.hasNullValues) {
linePaths.push(linePath)
areaPaths.push(areaPath)
}
}
}
} else {
if (series[i][j + 1] === null) {
linePath = linePath + graphics.move(x, y)
const numericOrCatX = w.globals.isXNumeric
? (w.globals.seriesX[realIndex][j] - w.globals.minX) / this.xRatio
: x - this.xDivision
areaPath =
areaPath +
graphics.line(numericOrCatX, areaBottomY) +
graphics.move(x, y) +
'z'
}
if (series[i][j] === null) {
linePath = linePath + graphics.move(x, y)
areaPath = areaPath + graphics.move(x, areaBottomY)
}
if (curve === 'stepline') {
linePath =
linePath + graphics.line(x, null, 'H') + graphics.line(null, y, 'V')
areaPath =
areaPath + graphics.line(x, null, 'H') + graphics.line(null, y, 'V')
} else if (curve === 'straight') {
linePath = linePath + graphics.line(x, y)
areaPath = areaPath + graphics.line(x, y)
}
if (j === series[i].length - 2) {
// last loop, close path
areaPath =
areaPath + graphics.line(x, areaBottomY) + graphics.move(x, y) + 'z'
if (type === 'rangeArea' && isRangeStart) {
linePath =
linePath + graphics.line(x, y2) + graphics.move(x, y2) + 'z'
} else {
linePaths.push(linePath)
areaPaths.push(areaPath)
}
}
}
return {
linePaths,
areaPaths,
pX,
pY,
linePath,
areaPath,
}
}
handleNullDataPoints(series, pointsPos, i, j, realIndex) {
const w = this.w
if (
(series[i][j] === null && w.config.markers.showNullDataPoints) ||
series[i].length === 1
) {
// fixes apexcharts.js#1282, #1252
let elPointsWrap = this.markers.plotChartMarkers(
pointsPos,
realIndex,
j + 1,
this.strokeWidth - w.config.markers.strokeWidth / 2,
true
)
if (elPointsWrap !== null) {
this.elPointsMain.add(elPointsWrap)
}
}
}
}
export default Line
+1057
View File
File diff suppressed because it is too large Load Diff
+524
View File
@@ -0,0 +1,524 @@
import Fill from '../modules/Fill'
import Graphics from '../modules/Graphics'
import Markers from '../modules/Markers'
import DataLabels from '../modules/DataLabels'
import Filters from '../modules/Filters'
import Utils from '../utils/Utils'
import Helpers from './common/circle/Helpers'
import CoreUtils from '../modules/CoreUtils'
/**
* ApexCharts Radar Class for Spider/Radar Charts.
* @module Radar
**/
class Radar {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.chartType = this.w.config.chart.type
this.initialAnim = this.w.config.chart.animations.enabled
this.dynamicAnim =
this.initialAnim &&
this.w.config.chart.animations.dynamicAnimation.enabled
this.animDur = 0
const w = this.w
this.graphics = new Graphics(this.ctx)
this.lineColorArr =
w.globals.stroke.colors !== undefined
? w.globals.stroke.colors
: w.globals.colors
this.defaultSize =
w.globals.svgHeight < w.globals.svgWidth
? w.globals.gridHeight + w.globals.goldenPadding * 1.5
: w.globals.gridWidth
this.isLog = w.config.yaxis[0].logarithmic
this.coreUtils = new CoreUtils(this.ctx)
this.maxValue = this.isLog
? this.coreUtils.getLogVal(w.globals.maxY, 0)
: w.globals.maxY
this.minValue = this.isLog
? this.coreUtils.getLogVal(this.w.globals.minY, 0)
: w.globals.minY
this.polygons = w.config.plotOptions.radar.polygons
this.strokeWidth = w.config.stroke.show ? w.config.stroke.width : 0
this.size =
this.defaultSize / 2.1 - this.strokeWidth - w.config.chart.dropShadow.blur
if (w.config.xaxis.labels.show) {
this.size = this.size - w.globals.xAxisLabelsWidth / 1.75
}
if (w.config.plotOptions.radar.size !== undefined) {
this.size = w.config.plotOptions.radar.size
}
this.dataRadiusOfPercent = []
this.dataRadius = []
this.angleArr = []
this.yaxisLabelsTextsPos = []
}
draw(series) {
let w = this.w
const fill = new Fill(this.ctx)
const allSeries = []
const dataLabels = new DataLabels(this.ctx)
if (series.length) {
this.dataPointsLen = series[w.globals.maxValsInArrayIndex].length
}
this.disAngle = (Math.PI * 2) / this.dataPointsLen
let halfW = w.globals.gridWidth / 2
let halfH = w.globals.gridHeight / 2
let translateX = halfW + w.config.plotOptions.radar.offsetX
let translateY = halfH + w.config.plotOptions.radar.offsetY
let ret = this.graphics.group({
class: 'apexcharts-radar-series apexcharts-plot-series',
transform: `translate(${translateX || 0}, ${translateY || 0})`
})
let dataPointsPos = []
let elPointsMain = null
let elDataPointsMain = null
this.yaxisLabels = this.graphics.group({
class: 'apexcharts-yaxis'
})
series.forEach((s, i) => {
let longestSeries = s.length === w.globals.dataPoints
// el to which series will be drawn
let elSeries = this.graphics.group().attr({
class: `apexcharts-series`,
'data:longestSeries': longestSeries,
seriesName: Utils.escapeString(w.globals.seriesNames[i]),
rel: i + 1,
'data:realIndex': i
})
this.dataRadiusOfPercent[i] = []
this.dataRadius[i] = []
this.angleArr[i] = []
s.forEach((dv, j) => {
const range = Math.abs(this.maxValue - this.minValue)
dv = dv + Math.abs(this.minValue)
if (this.isLog) {
dv = this.coreUtils.getLogVal(dv, 0)
}
this.dataRadiusOfPercent[i][j] = dv / range
this.dataRadius[i][j] = this.dataRadiusOfPercent[i][j] * this.size
this.angleArr[i][j] = j * this.disAngle
})
dataPointsPos = this.getDataPointsPos(
this.dataRadius[i],
this.angleArr[i]
)
const paths = this.createPaths(dataPointsPos, {
x: 0,
y: 0
})
// points
elPointsMain = this.graphics.group({
class: 'apexcharts-series-markers-wrap apexcharts-element-hidden'
})
// datapoints
elDataPointsMain = this.graphics.group({
class: `apexcharts-datalabels`,
'data:realIndex': i
})
w.globals.delayedElements.push({
el: elPointsMain.node,
index: i
})
const defaultRenderedPathOptions = {
i,
realIndex: i,
animationDelay: i,
initialSpeed: w.config.chart.animations.speed,
dataChangeSpeed: w.config.chart.animations.dynamicAnimation.speed,
className: `apexcharts-radar`,
shouldClipToGrid: false,
bindEventsOnPaths: false,
stroke: w.globals.stroke.colors[i],
strokeLineCap: w.config.stroke.lineCap
}
let pathFrom = null
if (w.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(i)
}
for (let p = 0; p < paths.linePathsTo.length; p++) {
let renderedLinePath = this.graphics.renderPaths({
...defaultRenderedPathOptions,
pathFrom: pathFrom === null ? paths.linePathsFrom[p] : pathFrom,
pathTo: paths.linePathsTo[p],
strokeWidth: Array.isArray(this.strokeWidth)
? this.strokeWidth[i]
: this.strokeWidth,
fill: 'none',
drawShadow: false
})
elSeries.add(renderedLinePath)
let pathFill = fill.fillPath({
seriesNumber: i
})
let renderedAreaPath = this.graphics.renderPaths({
...defaultRenderedPathOptions,
pathFrom: pathFrom === null ? paths.areaPathsFrom[p] : pathFrom,
pathTo: paths.areaPathsTo[p],
strokeWidth: 0,
fill: pathFill,
drawShadow: false
})
if (w.config.chart.dropShadow.enabled) {
const filters = new Filters(this.ctx)
const shadow = w.config.chart.dropShadow
filters.dropShadow(
renderedAreaPath,
Object.assign({}, shadow, { noUserSpaceOnUse: true }),
i
)
}
elSeries.add(renderedAreaPath)
}
s.forEach((sj, j) => {
let markers = new Markers(this.ctx)
let opts = markers.getMarkerConfig({
cssClass: 'apexcharts-marker',
seriesIndex: i,
dataPointIndex: j
})
let point = this.graphics.drawMarker(
dataPointsPos[j].x,
dataPointsPos[j].y,
opts
)
point.attr('rel', j)
point.attr('j', j)
point.attr('index', i)
point.node.setAttribute('default-marker-size', opts.pSize)
let elPointsWrap = this.graphics.group({
class: 'apexcharts-series-markers'
})
if (elPointsWrap) {
elPointsWrap.add(point)
}
elPointsMain.add(elPointsWrap)
elSeries.add(elPointsMain)
const dataLabelsConfig = w.config.dataLabels
if (dataLabelsConfig.enabled) {
let text = dataLabelsConfig.formatter(w.globals.series[i][j], {
seriesIndex: i,
dataPointIndex: j,
w
})
dataLabels.plotDataLabelsText({
x: dataPointsPos[j].x,
y: dataPointsPos[j].y,
text,
textAnchor: 'middle',
i,
j: i,
parent: elDataPointsMain,
offsetCorrection: false,
dataLabelsConfig: {
...dataLabelsConfig
}
})
}
elSeries.add(elDataPointsMain)
})
allSeries.push(elSeries)
})
this.drawPolygons({
parent: ret
})
if (w.config.xaxis.labels.show) {
const xaxisTexts = this.drawXAxisTexts()
ret.add(xaxisTexts)
}
allSeries.forEach((elS) => {
ret.add(elS)
})
ret.add(this.yaxisLabels)
return ret
}
drawPolygons(opts) {
const w = this.w
const { parent } = opts
const helpers = new Helpers(this.ctx)
const yaxisTexts = w.globals.yAxisScale[0].result.reverse()
const layers = yaxisTexts.length
let radiusSizes = []
let layerDis = this.size / (layers - 1)
for (let i = 0; i < layers; i++) {
radiusSizes[i] = layerDis * i
}
radiusSizes.reverse()
let polygonStrings = []
let lines = []
radiusSizes.forEach((radiusSize, r) => {
const polygon = Utils.getPolygonPos(radiusSize, this.dataPointsLen)
let string = ''
polygon.forEach((p, i) => {
if (r === 0) {
const line = this.graphics.drawLine(
p.x,
p.y,
0,
0,
Array.isArray(this.polygons.connectorColors)
? this.polygons.connectorColors[i]
: this.polygons.connectorColors
)
lines.push(line)
}
if (i === 0) {
this.yaxisLabelsTextsPos.push({
x: p.x,
y: p.y
})
}
string += p.x + ',' + p.y + ' '
})
polygonStrings.push(string)
})
polygonStrings.forEach((p, i) => {
const strokeColors = this.polygons.strokeColors
const strokeWidth = this.polygons.strokeWidth
const polygon = this.graphics.drawPolygon(
p,
Array.isArray(strokeColors) ? strokeColors[i] : strokeColors,
Array.isArray(strokeWidth) ? strokeWidth[i] : strokeWidth,
w.globals.radarPolygons.fill.colors[i]
)
parent.add(polygon)
})
lines.forEach((l) => {
parent.add(l)
})
if (w.config.yaxis[0].show) {
this.yaxisLabelsTextsPos.forEach((p, i) => {
const yText = helpers.drawYAxisTexts(p.x, p.y, i, yaxisTexts[i])
this.yaxisLabels.add(yText)
})
}
}
drawXAxisTexts() {
const w = this.w
const xaxisLabelsConfig = w.config.xaxis.labels
let elXAxisWrap = this.graphics.group({
class: 'apexcharts-xaxis'
})
let polygonPos = Utils.getPolygonPos(this.size, this.dataPointsLen)
w.globals.labels.forEach((label, i) => {
let formatter = w.config.xaxis.labels.formatter
let dataLabels = new DataLabels(this.ctx)
if (polygonPos[i]) {
let textPos = this.getTextPos(polygonPos[i], this.size)
let text = formatter(label, {
seriesIndex: -1,
dataPointIndex: i,
w
})
dataLabels.plotDataLabelsText({
x: textPos.newX,
y: textPos.newY,
text,
textAnchor: textPos.textAnchor,
i,
j: i,
parent: elXAxisWrap,
color:
Array.isArray(xaxisLabelsConfig.style.colors) &&
xaxisLabelsConfig.style.colors[i]
? xaxisLabelsConfig.style.colors[i]
: '#a8a8a8',
dataLabelsConfig: {
textAnchor: textPos.textAnchor,
dropShadow: { enabled: false },
...xaxisLabelsConfig
},
offsetCorrection: false
})
}
})
return elXAxisWrap
}
createPaths(pos, origin) {
let linePathsTo = []
let linePathsFrom = []
let areaPathsTo = []
let areaPathsFrom = []
if (pos.length) {
linePathsFrom = [this.graphics.move(origin.x, origin.y)]
areaPathsFrom = [this.graphics.move(origin.x, origin.y)]
let linePathTo = this.graphics.move(pos[0].x, pos[0].y)
let areaPathTo = this.graphics.move(pos[0].x, pos[0].y)
pos.forEach((p, i) => {
linePathTo += this.graphics.line(p.x, p.y)
areaPathTo += this.graphics.line(p.x, p.y)
if (i === pos.length - 1) {
linePathTo += 'Z'
areaPathTo += 'Z'
}
})
linePathsTo.push(linePathTo)
areaPathsTo.push(areaPathTo)
}
return {
linePathsFrom,
linePathsTo,
areaPathsFrom,
areaPathsTo
}
}
getTextPos(pos, polygonSize) {
let limit = 10
let textAnchor = 'middle'
let newX = pos.x
let newY = pos.y
if (Math.abs(pos.x) >= limit) {
if (pos.x > 0) {
textAnchor = 'start'
newX += 10
} else if (pos.x < 0) {
textAnchor = 'end'
newX -= 10
}
} else {
textAnchor = 'middle'
}
if (Math.abs(pos.y) >= polygonSize - limit) {
if (pos.y < 0) {
newY -= 10
} else if (pos.y > 0) {
newY += 10
}
}
return {
textAnchor,
newX,
newY
}
}
getPreviousPath(realIndex) {
let w = this.w
let pathFrom = null
for (let pp = 0; pp < w.globals.previousPaths.length; pp++) {
let gpp = w.globals.previousPaths[pp]
if (
gpp.paths.length > 0 &&
parseInt(gpp.realIndex, 10) === parseInt(realIndex, 10)
) {
if (typeof w.globals.previousPaths[pp].paths[0] !== 'undefined') {
pathFrom = w.globals.previousPaths[pp].paths[0].d
}
}
}
return pathFrom
}
getDataPointsPos(
dataRadiusArr,
angleArr,
dataPointsLen = this.dataPointsLen
) {
dataRadiusArr = dataRadiusArr || []
angleArr = angleArr || []
let dataPointsPosArray = []
for (let j = 0; j < dataPointsLen; j++) {
let curPointPos = {}
curPointPos.x = dataRadiusArr[j] * Math.sin(angleArr[j])
curPointPos.y = -dataRadiusArr[j] * Math.cos(angleArr[j])
dataPointsPosArray.push(curPointPos)
}
return dataPointsPosArray
}
}
export default Radar
+537
View File
@@ -0,0 +1,537 @@
import Pie from './Pie'
import Utils from '../utils/Utils'
import Fill from '../modules/Fill'
import Graphics from '../modules/Graphics'
import Filters from '../modules/Filters'
/**
* ApexCharts Radial Class for drawing Circle / Semi Circle Charts.
* @module Radial
**/
class Radial extends Pie {
constructor(ctx) {
super(ctx)
this.ctx = ctx
this.w = ctx.w
this.animBeginArr = [0]
this.animDur = 0
const w = this.w
this.startAngle = w.config.plotOptions.radialBar.startAngle
this.endAngle = w.config.plotOptions.radialBar.endAngle
this.totalAngle = Math.abs(
w.config.plotOptions.radialBar.endAngle -
w.config.plotOptions.radialBar.startAngle
)
this.trackStartAngle = w.config.plotOptions.radialBar.track.startAngle
this.trackEndAngle = w.config.plotOptions.radialBar.track.endAngle
this.barLabels = this.w.config.plotOptions.radialBar.barLabels
this.donutDataLabels = this.w.config.plotOptions.radialBar.dataLabels
this.radialDataLabels = this.donutDataLabels // make a copy for easy reference
if (!this.trackStartAngle) this.trackStartAngle = this.startAngle
if (!this.trackEndAngle) this.trackEndAngle = this.endAngle
if (this.endAngle === 360) this.endAngle = 359.99
this.margin = parseInt(w.config.plotOptions.radialBar.track.margin, 10)
this.onBarLabelClick = this.onBarLabelClick.bind(this)
}
draw(series) {
let w = this.w
const graphics = new Graphics(this.ctx)
let ret = graphics.group({
class: 'apexcharts-radialbar',
})
if (w.globals.noData) return ret
let elSeries = graphics.group()
let centerY = this.defaultSize / 2
let centerX = w.globals.gridWidth / 2
let size = this.defaultSize / 2.05
if (!w.config.chart.sparkline.enabled) {
size = size - w.config.stroke.width - w.config.chart.dropShadow.blur
}
let colorArr = w.globals.fill.colors
if (w.config.plotOptions.radialBar.track.show) {
let elTracks = this.drawTracks({
size,
centerX,
centerY,
colorArr,
series,
})
elSeries.add(elTracks)
}
let elG = this.drawArcs({
size,
centerX,
centerY,
colorArr,
series,
})
let totalAngle = 360
if (w.config.plotOptions.radialBar.startAngle < 0) {
totalAngle = this.totalAngle
}
let angleRatio = (360 - totalAngle) / 360
w.globals.radialSize = size - size * angleRatio
if (this.radialDataLabels.value.show) {
let offset = Math.max(
this.radialDataLabels.value.offsetY,
this.radialDataLabels.name.offsetY
)
w.globals.radialSize += offset * angleRatio
}
elSeries.add(elG.g)
if (w.config.plotOptions.radialBar.hollow.position === 'front') {
elG.g.add(elG.elHollow)
if (elG.dataLabels) {
elG.g.add(elG.dataLabels)
}
}
ret.add(elSeries)
return ret
}
drawTracks(opts) {
let w = this.w
const graphics = new Graphics(this.ctx)
let g = graphics.group({
class: 'apexcharts-tracks',
})
let filters = new Filters(this.ctx)
let fill = new Fill(this.ctx)
let strokeWidth = this.getStrokeWidth(opts)
opts.size = opts.size - strokeWidth / 2
for (let i = 0; i < opts.series.length; i++) {
let elRadialBarTrack = graphics.group({
class: 'apexcharts-radialbar-track apexcharts-track',
})
g.add(elRadialBarTrack)
elRadialBarTrack.attr({
rel: i + 1,
})
opts.size = opts.size - strokeWidth - this.margin
const trackConfig = w.config.plotOptions.radialBar.track
let pathFill = fill.fillPath({
seriesNumber: 0,
size: opts.size,
fillColors: Array.isArray(trackConfig.background)
? trackConfig.background[i]
: trackConfig.background,
solid: true,
})
let startAngle = this.trackStartAngle
let endAngle = this.trackEndAngle
if (Math.abs(endAngle) + Math.abs(startAngle) >= 360)
endAngle = 360 - Math.abs(this.startAngle) - 0.1
let elPath = graphics.drawPath({
d: '',
stroke: pathFill,
strokeWidth:
(strokeWidth * parseInt(trackConfig.strokeWidth, 10)) / 100,
fill: 'none',
strokeOpacity: trackConfig.opacity,
classes: 'apexcharts-radialbar-area',
})
if (trackConfig.dropShadow.enabled) {
const shadow = trackConfig.dropShadow
filters.dropShadow(elPath, shadow)
}
elRadialBarTrack.add(elPath)
elPath.attr('id', 'apexcharts-radialbarTrack-' + i)
this.animatePaths(elPath, {
centerX: opts.centerX,
centerY: opts.centerY,
endAngle,
startAngle,
size: opts.size,
i,
totalItems: 2,
animBeginArr: 0,
dur: 0,
isTrack: true,
easing: w.globals.easing,
})
}
return g
}
drawArcs(opts) {
let w = this.w
// size, donutSize, centerX, centerY, colorArr, lineColorArr, sectorAngleArr, series
let graphics = new Graphics(this.ctx)
let fill = new Fill(this.ctx)
let filters = new Filters(this.ctx)
let g = graphics.group()
let strokeWidth = this.getStrokeWidth(opts)
opts.size = opts.size - strokeWidth / 2
let hollowFillID = w.config.plotOptions.radialBar.hollow.background
let hollowSize =
opts.size -
strokeWidth * opts.series.length -
this.margin * opts.series.length -
(strokeWidth *
parseInt(w.config.plotOptions.radialBar.track.strokeWidth, 10)) /
100 /
2
let hollowRadius = hollowSize - w.config.plotOptions.radialBar.hollow.margin
if (w.config.plotOptions.radialBar.hollow.image !== undefined) {
hollowFillID = this.drawHollowImage(opts, g, hollowSize, hollowFillID)
}
let elHollow = this.drawHollow({
size: hollowRadius,
centerX: opts.centerX,
centerY: opts.centerY,
fill: hollowFillID ? hollowFillID : 'transparent',
})
if (w.config.plotOptions.radialBar.hollow.dropShadow.enabled) {
const shadow = w.config.plotOptions.radialBar.hollow.dropShadow
filters.dropShadow(elHollow, shadow)
}
let shown = 1
if (!this.radialDataLabels.total.show && w.globals.series.length > 1) {
shown = 0
}
let dataLabels = null
if (this.radialDataLabels.show) {
dataLabels = this.renderInnerDataLabels(this.radialDataLabels, {
hollowSize,
centerX: opts.centerX,
centerY: opts.centerY,
opacity: shown,
})
}
if (w.config.plotOptions.radialBar.hollow.position === 'back') {
g.add(elHollow)
if (dataLabels) {
g.add(dataLabels)
}
}
let reverseLoop = false
if (w.config.plotOptions.radialBar.inverseOrder) {
reverseLoop = true
}
for (
let i = reverseLoop ? opts.series.length - 1 : 0;
reverseLoop ? i >= 0 : i < opts.series.length;
reverseLoop ? i-- : i++
) {
let elRadialBarArc = graphics.group({
class: `apexcharts-series apexcharts-radial-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[i]),
})
g.add(elRadialBarArc)
elRadialBarArc.attr({
rel: i + 1,
'data:realIndex': i,
})
this.ctx.series.addCollapsedClassToSeries(elRadialBarArc, i)
opts.size = opts.size - strokeWidth - this.margin
let pathFill = fill.fillPath({
seriesNumber: i,
size: opts.size,
value: opts.series[i],
})
let startAngle = this.startAngle
let prevStartAngle
// if data exceeds 100, make it 100
const dataValue =
Utils.negToZero(opts.series[i] > 100 ? 100 : opts.series[i]) / 100
let endAngle = Math.round(this.totalAngle * dataValue) + this.startAngle
let prevEndAngle
if (w.globals.dataChanged) {
prevStartAngle = this.startAngle
prevEndAngle =
Math.round(
(this.totalAngle * Utils.negToZero(w.globals.previousPaths[i])) /
100
) + prevStartAngle
}
const currFullAngle = Math.abs(endAngle) + Math.abs(startAngle)
if (currFullAngle >= 360) {
endAngle = endAngle - 0.01
}
const prevFullAngle = Math.abs(prevEndAngle) + Math.abs(prevStartAngle)
if (prevFullAngle >= 360) {
prevEndAngle = prevEndAngle - 0.01
}
let angle = endAngle - startAngle
const dashArray = Array.isArray(w.config.stroke.dashArray)
? w.config.stroke.dashArray[i]
: w.config.stroke.dashArray
let elPath = graphics.drawPath({
d: '',
stroke: pathFill,
strokeWidth,
fill: 'none',
fillOpacity: w.config.fill.opacity,
classes: 'apexcharts-radialbar-area apexcharts-radialbar-slice-' + i,
strokeDashArray: dashArray,
})
Graphics.setAttrs(elPath.node, {
'data:angle': angle,
'data:value': opts.series[i],
})
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow
filters.dropShadow(elPath, shadow, i)
}
filters.setSelectionFilter(elPath, 0, i)
this.addListeners(elPath, this.radialDataLabels)
elRadialBarArc.add(elPath)
elPath.attr({
index: 0,
j: i,
})
if (this.barLabels.enabled) {
let barStartCords = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
opts.size,
startAngle
)
let text = this.barLabels.formatter(w.globals.seriesNames[i], {
seriesIndex: i,
w,
})
let classes = ['apexcharts-radialbar-label']
if (!this.barLabels.onClick) {
classes.push('apexcharts-no-click')
}
let textColor = this.barLabels.useSeriesColors
? w.globals.colors[i]
: w.config.chart.foreColor
if (!textColor) {
textColor = w.config.chart.foreColor
}
const x = barStartCords.x - this.barLabels.margin
const y = barStartCords.y
let elText = graphics.drawText({
x,
y,
text,
textAnchor: 'end',
dominantBaseline: 'middle',
fontFamily: this.barLabels.fontFamily,
fontWeight: this.barLabels.fontWeight,
fontSize: this.barLabels.fontSize,
foreColor: textColor,
cssClass: classes.join(' '),
})
elText.on('click', this.onBarLabelClick)
elText.attr({
rel: i + 1,
})
if (startAngle !== 0) {
elText.attr({
'transform-origin': `${x} ${y}`,
transform: `rotate(${startAngle} 0 0)`,
})
}
elRadialBarArc.add(elText)
}
let dur = 0
if (this.initialAnim && !w.globals.resized && !w.globals.dataChanged) {
dur = w.config.chart.animations.speed
}
if (w.globals.dataChanged) {
dur = w.config.chart.animations.dynamicAnimation.speed
}
this.animDur = dur / (opts.series.length * 1.2) + this.animDur
this.animBeginArr.push(this.animDur)
this.animatePaths(elPath, {
centerX: opts.centerX,
centerY: opts.centerY,
endAngle,
startAngle,
prevEndAngle,
prevStartAngle,
size: opts.size,
i,
totalItems: 2,
animBeginArr: this.animBeginArr,
dur,
shouldSetPrevPaths: true,
easing: w.globals.easing,
})
}
return {
g,
elHollow,
dataLabels,
}
}
drawHollow(opts) {
const graphics = new Graphics(this.ctx)
let circle = graphics.drawCircle(opts.size * 2)
circle.attr({
class: 'apexcharts-radialbar-hollow',
cx: opts.centerX,
cy: opts.centerY,
r: opts.size,
fill: opts.fill,
})
return circle
}
drawHollowImage(opts, g, hollowSize, hollowFillID) {
const w = this.w
let fill = new Fill(this.ctx)
let randID = Utils.randomId()
let hollowFillImg = w.config.plotOptions.radialBar.hollow.image
if (w.config.plotOptions.radialBar.hollow.imageClipped) {
fill.clippedImgArea({
width: hollowSize,
height: hollowSize,
image: hollowFillImg,
patternID: `pattern${w.globals.cuid}${randID}`,
})
hollowFillID = `url(#pattern${w.globals.cuid}${randID})`
} else {
const imgWidth = w.config.plotOptions.radialBar.hollow.imageWidth
const imgHeight = w.config.plotOptions.radialBar.hollow.imageHeight
if (imgWidth === undefined && imgHeight === undefined) {
let image = w.globals.dom.Paper.image(hollowFillImg).loaded(function (
loader
) {
this.move(
opts.centerX -
loader.width / 2 +
w.config.plotOptions.radialBar.hollow.imageOffsetX,
opts.centerY -
loader.height / 2 +
w.config.plotOptions.radialBar.hollow.imageOffsetY
)
})
g.add(image)
} else {
let image = w.globals.dom.Paper.image(hollowFillImg).loaded(function (
loader
) {
this.move(
opts.centerX -
imgWidth / 2 +
w.config.plotOptions.radialBar.hollow.imageOffsetX,
opts.centerY -
imgHeight / 2 +
w.config.plotOptions.radialBar.hollow.imageOffsetY
)
this.size(imgWidth, imgHeight)
})
g.add(image)
}
}
return hollowFillID
}
getStrokeWidth(opts) {
const w = this.w
return (
(opts.size *
(100 - parseInt(w.config.plotOptions.radialBar.hollow.size, 10))) /
100 /
(opts.series.length + 1) -
this.margin
)
}
onBarLabelClick(e) {
let seriesIndex = parseInt(e.target.getAttribute('rel'), 10) - 1
const legendClick = this.barLabels.onClick
const w = this.w
if (legendClick) {
legendClick(w.globals.seriesNames[seriesIndex], { w, seriesIndex })
}
}
}
export default Radial
+437
View File
@@ -0,0 +1,437 @@
import Bar from './Bar'
import Graphics from '../modules/Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts RangeBar Class responsible for drawing Range/Timeline Bars.
*
* @module RangeBar
**/
class RangeBar extends Bar {
draw(series, seriesIndex) {
let w = this.w
let graphics = new Graphics(this.ctx)
this.rangeBarOptions = this.w.config.plotOptions.rangeBar
this.series = series
this.seriesRangeStart = w.globals.seriesRangeStart
this.seriesRangeEnd = w.globals.seriesRangeEnd
this.barHelpers.initVariables(series)
let ret = graphics.group({
class: 'apexcharts-rangebar-series apexcharts-plot-series',
})
for (let i = 0; i < series.length; i++) {
let x,
y,
xDivision, // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
yDivision, // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroH, // zeroH is the baseline where 0 meets y axis
zeroW // zeroW is the baseline where 0 meets x axis
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
// el to which series will be drawn
let elSeries = graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
rel: i + 1,
'data:realIndex': realIndex,
})
this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex)
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1
}
let barHeight = 0
let barWidth = 0
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
let initPositions = this.barHelpers.initialPositions()
y = initPositions.y
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
barHeight = initPositions.barHeight
xDivision = initPositions.xDivision
yDivision = initPositions.yDivision
zeroH = initPositions.zeroH
// eldatalabels
let elDataLabelsWrap = graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex,
})
let elGoalsMarkers = graphics.group({
class: 'apexcharts-rangebar-goals-markers',
})
for (let j = 0; j < w.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)
const y1 = this.seriesRangeStart[i][j]
const y2 = this.seriesRangeEnd[i][j]
let paths = null
let barXPosition = null
let barYPosition = null
const params = { x, y, strokeWidth, elSeries }
let seriesLen = this.seriesLen
if (w.config.plotOptions.bar.rangeBarGroupRows) {
seriesLen = 1
}
if (typeof w.config.series[i].data[j] === 'undefined') {
// no data exists for further indexes, hence we need to get out the innr loop.
// As we are iterating over total datapoints, there is a possiblity the series might not have data for j index
break
}
if (this.isHorizontal) {
barYPosition = y + barHeight * this.visibleI
let srty = (yDivision - barHeight * seriesLen) / 2
if (w.config.series[i].data[j].x) {
let positions = this.detectOverlappingBars({
i,
j,
barYPosition,
srty,
barHeight,
yDivision,
initPositions,
})
barHeight = positions.barHeight
barYPosition = positions.barYPosition
}
paths = this.drawRangeBarPaths({
indexes: { i, j, realIndex },
barHeight,
barYPosition,
zeroW,
yDivision,
y1,
y2,
...params,
})
barWidth = paths.barWidth
} else {
if (w.globals.isXNumeric) {
x =
(w.globals.seriesX[i][j] - w.globals.minX) / this.xRatio -
barWidth / 2
}
barXPosition = x + barWidth * this.visibleI
let srtx = (xDivision - barWidth * seriesLen) / 2
if (w.config.series[i].data[j].x) {
let positions = this.detectOverlappingBars({
i,
j,
barXPosition,
srtx,
barWidth,
xDivision,
initPositions,
})
barWidth = positions.barWidth
barXPosition = positions.barXPosition
}
paths = this.drawRangeColumnPaths({
indexes: { i, j, realIndex },
barWidth,
barXPosition,
zeroH,
xDivision,
...params,
})
barHeight = paths.barHeight
}
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth,
})
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine)
}
y = paths.y
x = paths.x
let pathFill = this.barHelpers.getPathFillColor(series, i, j, realIndex)
let lineFill = w.globals.stroke.colors[realIndex]
this.renderSeries({
realIndex,
pathFill,
lineFill,
j,
i,
x,
y,
y1,
y2,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
series,
barHeight,
barWidth,
barXPosition,
barYPosition,
barWidth,
elDataLabelsWrap,
elGoalsMarkers,
visibleSeries: this.visibleI,
type: 'rangebar',
})
}
ret.add(elSeries)
}
return ret
}
detectOverlappingBars({
i,
j,
barYPosition,
barXPosition,
srty,
srtx,
barHeight,
barWidth,
yDivision,
xDivision,
initPositions,
}) {
const w = this.w
let overlaps = []
let rangeName = w.config.series[i].data[j].rangeName
const x = w.config.series[i].data[j].x
const labelX = Array.isArray(x) ? x.join(' ') : x
const rowIndex = w.globals.labels
.map((_) => (Array.isArray(_) ? _.join(' ') : _))
.indexOf(labelX)
const overlappedIndex = w.globals.seriesRange[i].findIndex(
(tx) => tx.x === labelX && tx.overlaps.length > 0
)
if (this.isHorizontal) {
if (w.config.plotOptions.bar.rangeBarGroupRows) {
barYPosition = srty + yDivision * rowIndex
} else {
barYPosition = srty + barHeight * this.visibleI + yDivision * rowIndex
}
if (overlappedIndex > -1 && !w.config.plotOptions.bar.rangeBarOverlap) {
overlaps = w.globals.seriesRange[i][overlappedIndex].overlaps
if (overlaps.indexOf(rangeName) > -1) {
barHeight = initPositions.barHeight / overlaps.length
barYPosition =
barHeight * this.visibleI +
(yDivision * (100 - parseInt(this.barOptions.barHeight, 10))) /
100 /
2 +
barHeight * (this.visibleI + overlaps.indexOf(rangeName)) +
yDivision * rowIndex
}
}
} else {
if (rowIndex > -1) {
if (w.config.plotOptions.bar.rangeBarGroupRows) {
barXPosition = srtx + xDivision * rowIndex
} else {
barXPosition = srtx + barWidth * this.visibleI + xDivision * rowIndex
}
}
if (overlappedIndex > -1 && !w.config.plotOptions.bar.rangeBarOverlap) {
overlaps = w.globals.seriesRange[i][overlappedIndex].overlaps
if (overlaps.indexOf(rangeName) > -1) {
barWidth = initPositions.barWidth / overlaps.length
barXPosition =
barWidth * this.visibleI +
(xDivision * (100 - parseInt(this.barOptions.barWidth, 10))) /
100 /
2 +
barWidth * (this.visibleI + overlaps.indexOf(rangeName)) +
xDivision * rowIndex
}
}
}
return {
barYPosition,
barXPosition,
barHeight,
barWidth,
}
}
drawRangeColumnPaths({
indexes,
x,
xDivision,
barWidth,
barXPosition,
zeroH,
}) {
let w = this.w
let i = indexes.i
let j = indexes.j
const yRatio = this.yRatio[this.yaxisIndex]
let realIndex = indexes.realIndex
const range = this.getRangeValue(realIndex, j)
let y1 = Math.min(range.start, range.end)
let y2 = Math.max(range.start, range.end)
if (
typeof this.series[i][j] === 'undefined' ||
this.series[i][j] === null
) {
y1 = zeroH
} else {
y1 = zeroH - y1 / yRatio
y2 = zeroH - y2 / yRatio
}
const barHeight = Math.abs(y2 - y1)
const paths = this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1,
y2,
strokeWidth: this.strokeWidth,
series: this.seriesRangeEnd,
realIndex: indexes.realIndex,
i: realIndex,
j,
w,
})
if (!w.globals.isXNumeric) {
x = x + xDivision
} else {
const xForNumericXAxis = this.getBarXForNumericXAxis({
x,
j,
realIndex,
barWidth,
})
x = xForNumericXAxis.x
barXPosition = xForNumericXAxis.barXPosition
}
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
barHeight,
x,
y: y2,
goalY: this.barHelpers.getGoalValues('y', null, zeroH, i, j),
barXPosition,
}
}
drawRangeBarPaths({
indexes,
y,
y1,
y2,
yDivision,
barHeight,
barYPosition,
zeroW,
}) {
let w = this.w
const x1 = zeroW + y1 / this.invertedYRatio
const x2 = zeroW + y2 / this.invertedYRatio
const barWidth = Math.abs(x2 - x1)
const paths = this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1,
x2,
strokeWidth: this.strokeWidth,
series: this.seriesRangeEnd,
i: indexes.realIndex,
realIndex: indexes.realIndex,
j: indexes.j,
w,
})
if (!w.globals.isXNumeric) {
y = y + yDivision
}
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
barWidth,
x: x2,
goalX: this.barHelpers.getGoalValues(
'x',
zeroW,
null,
indexes.realIndex,
indexes.j
),
y,
}
}
getRangeValue(i, j) {
const w = this.w
return {
start: w.globals.seriesRangeStart[i][j],
end: w.globals.seriesRangeEnd[i][j],
}
}
}
export default RangeBar
+271
View File
@@ -0,0 +1,271 @@
import Animations from '../modules/Animations'
import Fill from '../modules/Fill'
import Filters from '../modules/Filters'
import Graphics from '../modules/Graphics'
import Markers from '../modules/Markers'
/**
* ApexCharts Scatter Class.
* This Class also handles bubbles chart as currently there is no major difference in drawing them,
* @module Scatter
**/
export default class Scatter {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.initialAnim = this.w.config.chart.animations.enabled
this.dynamicAnim =
this.initialAnim &&
this.w.config.chart.animations.dynamicAnimation.enabled
}
draw(elSeries, j, opts) {
let w = this.w
let graphics = new Graphics(this.ctx)
let realIndex = opts.realIndex
let pointsPos = opts.pointsPos
let zRatio = opts.zRatio
let elPointsMain = opts.elParent
let elPointsWrap = graphics.group({
class: `apexcharts-series-markers apexcharts-series-${w.config.chart.type}`
})
elPointsWrap.attr('clip-path', `url(#gridRectMarkerMask${w.globals.cuid})`)
if (Array.isArray(pointsPos.x)) {
for (let q = 0; q < pointsPos.x.length; q++) {
let dataPointIndex = j + 1
let shouldDraw = true
// a small hack as we have 2 points for the first val to connect it
if (j === 0 && q === 0) dataPointIndex = 0
if (j === 0 && q === 1) dataPointIndex = 1
let radius = 0
let finishRadius = w.globals.markers.size[realIndex]
if (zRatio !== Infinity) {
// means we have a bubble
const bubble = w.config.plotOptions.bubble
finishRadius = w.globals.seriesZ[realIndex][dataPointIndex]
if (bubble.zScaling) {
finishRadius /= zRatio
}
if (bubble.minBubbleRadius && finishRadius < bubble.minBubbleRadius) {
finishRadius = bubble.minBubbleRadius
}
if (bubble.maxBubbleRadius && finishRadius > bubble.maxBubbleRadius) {
finishRadius = bubble.maxBubbleRadius
}
}
if (!w.config.chart.animations.enabled) {
radius = finishRadius
}
let x = pointsPos.x[q]
let y = pointsPos.y[q]
radius = radius || 0
if (
y === null ||
typeof w.globals.series[realIndex][dataPointIndex] === 'undefined'
) {
shouldDraw = false
}
if (shouldDraw) {
const point = this.drawPoint(
x,
y,
radius,
finishRadius,
realIndex,
dataPointIndex,
j
)
elPointsWrap.add(point)
}
elPointsMain.add(elPointsWrap)
}
}
}
drawPoint(x, y, radius, finishRadius, realIndex, dataPointIndex, j) {
const w = this.w
let i = realIndex
let anim = new Animations(this.ctx)
let filters = new Filters(this.ctx)
let fill = new Fill(this.ctx)
let markers = new Markers(this.ctx)
const graphics = new Graphics(this.ctx)
const markerConfig = markers.getMarkerConfig({
cssClass: 'apexcharts-marker',
seriesIndex: i,
dataPointIndex,
finishRadius:
w.config.chart.type === 'bubble' ||
(w.globals.comboCharts &&
w.config.series[realIndex] &&
w.config.series[realIndex].type === 'bubble')
? finishRadius
: null
})
finishRadius = markerConfig.pSize
let pathFillCircle = fill.fillPath({
seriesNumber: realIndex,
dataPointIndex,
color: markerConfig.pointFillColor,
patternUnits: 'objectBoundingBox',
value: w.globals.series[realIndex][j]
})
let el
if (markerConfig.shape === 'circle') {
el = graphics.drawCircle(radius)
} else if (
markerConfig.shape === 'square' ||
markerConfig.shape === 'rect'
) {
el = graphics.drawRect(
0,
0,
markerConfig.width - markerConfig.pointStrokeWidth / 2,
markerConfig.height - markerConfig.pointStrokeWidth / 2,
markerConfig.pRadius
)
}
if (w.config.series[i].data[dataPointIndex]) {
if (w.config.series[i].data[dataPointIndex].fillColor) {
pathFillCircle = w.config.series[i].data[dataPointIndex].fillColor
}
}
el.attr({
x: x - markerConfig.width / 2 - markerConfig.pointStrokeWidth / 2,
y: y - markerConfig.height / 2 - markerConfig.pointStrokeWidth / 2,
cx: x,
cy: y,
fill: pathFillCircle,
'fill-opacity': markerConfig.pointFillOpacity,
stroke: markerConfig.pointStrokeColor,
r: finishRadius,
'stroke-width': markerConfig.pointStrokeWidth,
'stroke-dasharray': markerConfig.pointStrokeDashArray,
'stroke-opacity': markerConfig.pointStrokeOpacity
})
if (w.config.chart.dropShadow.enabled) {
const dropShadow = w.config.chart.dropShadow
filters.dropShadow(el, dropShadow, realIndex)
}
if (this.initialAnim && !w.globals.dataChanged && !w.globals.resized) {
let speed = w.config.chart.animations.speed
anim.animateMarker(
el,
0,
markerConfig.shape === 'circle'
? finishRadius
: { width: markerConfig.width, height: markerConfig.height },
speed,
w.globals.easing,
() => {
window.setTimeout(() => {
anim.animationCompleted(el)
}, 100)
}
)
} else {
w.globals.animationEnded = true
}
if (w.globals.dataChanged && markerConfig.shape === 'circle') {
if (this.dynamicAnim) {
let speed = w.config.chart.animations.dynamicAnimation.speed
let prevX, prevY, prevR
let prevPathJ = null
prevPathJ =
w.globals.previousPaths[realIndex] &&
w.globals.previousPaths[realIndex][j]
if (typeof prevPathJ !== 'undefined' && prevPathJ !== null) {
// series containing less elements will ignore these values and revert to 0
prevX = prevPathJ.x
prevY = prevPathJ.y
prevR =
typeof prevPathJ.r !== 'undefined' ? prevPathJ.r : finishRadius
}
for (let cs = 0; cs < w.globals.collapsedSeries.length; cs++) {
if (w.globals.collapsedSeries[cs].index === realIndex) {
speed = 1
finishRadius = 0
}
}
if (x === 0 && y === 0) finishRadius = 0
anim.animateCircle(
el,
{
cx: prevX,
cy: prevY,
r: prevR
},
{
cx: x,
cy: y,
r: finishRadius
},
speed,
w.globals.easing
)
} else {
el.attr({
r: finishRadius
})
}
}
el.attr({
rel: dataPointIndex,
j: dataPointIndex,
index: realIndex,
'default-marker-size': finishRadius
})
filters.setSelectionFilter(el, realIndex, dataPointIndex)
markers.addEvents(el)
el.node.classList.add('apexcharts-marker')
return el
}
centerTextInBubble(y) {
let w = this.w
y = y + parseInt(w.config.dataLabels.style.fontSize, 10) / 4
return {
y
}
}
}
+353
View File
@@ -0,0 +1,353 @@
import '../libs/Treemap-squared'
import Graphics from '../modules/Graphics'
import Animations from '../modules/Animations'
import Fill from '../modules/Fill'
import Helpers from './common/treemap/Helpers'
import Filters from '../modules/Filters'
import Utils from '../utils/Utils'
/**
* ApexCharts TreemapChart Class.
* @module TreemapChart
**/
export default class TreemapChart {
constructor(ctx, xyRatios) {
this.ctx = ctx
this.w = ctx.w
this.strokeWidth = this.w.config.stroke.width
this.helpers = new Helpers(ctx)
this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation
this.labels = []
}
draw(series) {
let w = this.w
const graphics = new Graphics(this.ctx)
const fill = new Fill(this.ctx)
let ret = graphics.group({
class: 'apexcharts-treemap',
})
if (w.globals.noData) return ret
let ser = []
series.forEach((s) => {
let d = s.map((v) => {
return Math.abs(v)
})
ser.push(d)
})
this.negRange = this.helpers.checkColorRange()
w.config.series.forEach((s, i) => {
s.data.forEach((l) => {
if (!Array.isArray(this.labels[i])) this.labels[i] = []
this.labels[i].push(l.x)
})
})
const nodes = window.TreemapSquared.generate(
ser,
w.globals.gridWidth,
w.globals.gridHeight
)
nodes.forEach((node, i) => {
let elSeries = graphics.group({
class: `apexcharts-series apexcharts-treemap-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[i]),
rel: i + 1,
'data:realIndex': i,
})
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow
const filters = new Filters(this.ctx)
filters.dropShadow(ret, shadow, i)
}
let elDataLabelWrap = graphics.group({
class: 'apexcharts-data-labels',
})
node.forEach((r, j) => {
const x1 = r[0]
const y1 = r[1]
const x2 = r[2]
const y2 = r[3]
let elRect = graphics.drawRect(
x1,
y1,
x2 - x1,
y2 - y1,
w.config.plotOptions.treemap.borderRadius,
'#fff',
1,
this.strokeWidth,
w.config.plotOptions.treemap.useFillColorAsStroke
? color
: w.globals.stroke.colors[i]
)
elRect.attr({
cx: x1,
cy: y1,
index: i,
i,
j,
width: x2 - x1,
height: y2 - y1,
})
let colorProps = this.helpers.getShadeColor(
w.config.chart.type,
i,
j,
this.negRange
)
let color = colorProps.color
if (
typeof w.config.series[i].data[j] !== 'undefined' &&
w.config.series[i].data[j].fillColor
) {
color = w.config.series[i].data[j].fillColor
}
let pathFill = fill.fillPath({
color,
seriesNumber: i,
dataPointIndex: j,
})
elRect.node.classList.add('apexcharts-treemap-rect')
elRect.attr({
fill: pathFill,
})
this.helpers.addListeners(elRect)
let fromRect = {
x: x1 + (x2 - x1) / 2,
y: y1 + (y2 - y1) / 2,
width: 0,
height: 0,
}
let toRect = {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
if (w.config.chart.animations.enabled && !w.globals.dataChanged) {
let speed = 1
if (!w.globals.resized) {
speed = w.config.chart.animations.speed
}
this.animateTreemap(elRect, fromRect, toRect, speed)
}
if (w.globals.dataChanged) {
let speed = 1
if (this.dynamicAnim.enabled && w.globals.shouldAnimate) {
speed = this.dynamicAnim.speed
if (
w.globals.previousPaths[i] &&
w.globals.previousPaths[i][j] &&
w.globals.previousPaths[i][j].rect
) {
fromRect = w.globals.previousPaths[i][j].rect
}
this.animateTreemap(elRect, fromRect, toRect, speed)
}
}
let fontSize = this.getFontSize(r)
let formattedText = w.config.dataLabels.formatter(this.labels[i][j], {
value: w.globals.series[i][j],
seriesIndex: i,
dataPointIndex: j,
w,
})
if (w.config.plotOptions.treemap.dataLabels.format === 'truncate') {
fontSize = parseInt(w.config.dataLabels.style.fontSize, 10)
formattedText = this.truncateLabels(
formattedText,
fontSize,
x1,
y1,
x2,
y2
)
}
let dataLabels = this.helpers.calculateDataLabels({
text: formattedText,
x: (x1 + x2) / 2,
y: (y1 + y2) / 2 + this.strokeWidth / 2 + fontSize / 3,
i,
j,
colorProps,
fontSize,
series,
})
if (w.config.dataLabels.enabled && dataLabels) {
this.rotateToFitLabel(
dataLabels,
fontSize,
formattedText,
x1,
y1,
x2,
y2
)
}
elSeries.add(elRect)
if (dataLabels !== null) {
elSeries.add(dataLabels)
}
})
elSeries.add(elDataLabelWrap)
ret.add(elSeries)
})
return ret
}
// This calculates a font-size based upon
// average label length and the size of the box the label is
// going into. The maximum font size is set in chart config.
getFontSize(coordinates) {
const w = this.w
// total length of labels (i.e [["Italy"],["Spain", "Greece"]] -> 16)
function totalLabelLength(arr) {
let i,
total = 0
if (Array.isArray(arr[0])) {
for (i = 0; i < arr.length; i++) {
total += totalLabelLength(arr[i])
}
} else {
for (i = 0; i < arr.length; i++) {
total += arr[i].length
}
}
return total
}
// count of labels (i.e [["Italy"],["Spain", "Greece"]] -> 3)
function countLabels(arr) {
let i,
total = 0
if (Array.isArray(arr[0])) {
for (i = 0; i < arr.length; i++) {
total += countLabels(arr[i])
}
} else {
for (i = 0; i < arr.length; i++) {
total += 1
}
}
return total
}
let averagelabelsize =
totalLabelLength(this.labels) / countLabels(this.labels)
function fontSize(width, height) {
// the font size should be proportional to the size of the box (and the value)
// otherwise you can end up creating a visual distortion where two boxes of identical
// size have different sized labels, and thus make it look as if the two boxes
// represent different sizes
let area = width * height
let arearoot = Math.pow(area, 0.5)
return Math.min(
arearoot / averagelabelsize,
parseInt(w.config.dataLabels.style.fontSize, 10)
)
}
return fontSize(
coordinates[2] - coordinates[0],
coordinates[3] - coordinates[1]
)
}
rotateToFitLabel(elText, fontSize, text, x1, y1, x2, y2) {
const graphics = new Graphics(this.ctx)
const textRect = graphics.getTextRects(text, fontSize)
//if the label fits better sideways then rotate it
if (
textRect.width + this.w.config.stroke.width + 5 > x2 - x1 &&
textRect.width <= y2 - y1
) {
let labelRotatingCenter = graphics.rotateAroundCenter(elText.node)
elText.node.setAttribute(
'transform',
`rotate(-90 ${labelRotatingCenter.x} ${
labelRotatingCenter.y
}) translate(${textRect.height / 3})`
)
}
}
// This is an alternative label formatting method that uses a
// consistent font size, and trims the edge of long labels
truncateLabels(text, fontSize, x1, y1, x2, y2) {
const graphics = new Graphics(this.ctx)
const textRect = graphics.getTextRects(text, fontSize)
// Determine max width based on ideal orientation of text
const labelMaxWidth =
textRect.width + this.w.config.stroke.width + 5 > x2 - x1 &&
y2 - y1 > x2 - x1
? y2 - y1
: x2 - x1
const truncatedText = graphics.getTextBasedOnMaxWidth({
text: text,
maxWidth: labelMaxWidth,
fontSize: fontSize,
})
// Return empty label when text has been trimmed for very small rects
if (text.length !== truncatedText.length && labelMaxWidth / fontSize < 5) {
return ''
} else {
return truncatedText
}
}
animateTreemap(el, fromRect, toRect, speed) {
const animations = new Animations(this.ctx)
animations.animateRect(
el,
{
x: fromRect.x,
y: fromRect.y,
width: fromRect.width,
height: fromRect.height,
},
{
x: toRect.x,
y: toRect.y,
width: toRect.width,
height: toRect.height,
},
speed,
() => {
animations.animationCompleted(el)
}
)
}
}
+650
View File
@@ -0,0 +1,650 @@
import Graphics from '../../../modules/Graphics'
import DataLabels from '../../../modules/DataLabels'
export default class BarDataLabels {
constructor(barCtx) {
this.w = barCtx.w
this.barCtx = barCtx
this.totalFormatter =
this.w.config.plotOptions.bar.dataLabels.total.formatter
if (!this.totalFormatter) {
this.totalFormatter = this.w.config.dataLabels.formatter
}
}
/** handleBarDataLabels is used to calculate the positions for the data-labels
* It also sets the element's data attr for bars and calls drawCalculatedBarDataLabels()
* After calculating, it also calls the function to draw data labels
* @memberof Bar
* @param {object} {barProps} most of the bar properties used throughout the bar
* drawing function
* @return {object} dataLabels node-element which you can append later
**/
handleBarDataLabels(opts) {
let {
x,
y,
y1,
y2,
i,
j,
realIndex,
groupIndex,
series,
barHeight,
barWidth,
barXPosition,
barYPosition,
visibleSeries,
renderedPath,
} = opts
let w = this.w
let graphics = new Graphics(this.barCtx.ctx)
let strokeWidth = Array.isArray(this.barCtx.strokeWidth)
? this.barCtx.strokeWidth[realIndex]
: this.barCtx.strokeWidth
let bcx = x + parseFloat(barWidth * visibleSeries)
let bcy = y + parseFloat(barHeight * visibleSeries)
if (w.globals.isXNumeric && !w.globals.isBarHorizontal) {
bcx = x + parseFloat(barWidth * (visibleSeries + 1))
bcy = y + parseFloat(barHeight * (visibleSeries + 1)) - strokeWidth
}
let dataLabels = null
let totalDataLabels = null
let dataLabelsX = x
let dataLabelsY = y
let dataLabelsPos = {}
let dataLabelsConfig = w.config.dataLabels
let barDataLabelsConfig = this.barCtx.barOptions.dataLabels
let barTotalDataLabelsConfig = this.barCtx.barOptions.dataLabels.total
if (typeof barYPosition !== 'undefined' && this.barCtx.isRangeBar) {
bcy = barYPosition
dataLabelsY = barYPosition
}
if (
typeof barXPosition !== 'undefined' &&
this.barCtx.isVerticalGroupedRangeBar
) {
bcx = barXPosition
dataLabelsX = barXPosition
}
const offX = dataLabelsConfig.offsetX
const offY = dataLabelsConfig.offsetY
let textRects = {
width: 0,
height: 0,
}
if (w.config.dataLabels.enabled) {
const yLabel = this.barCtx.series[i][j]
textRects = graphics.getTextRects(
w.globals.yLabelFormatters[0](yLabel),
parseFloat(dataLabelsConfig.style.fontSize)
)
}
const params = {
x,
y,
i,
j,
realIndex,
groupIndex: !!groupIndex ? groupIndex : -1,
renderedPath,
bcx,
bcy,
barHeight,
barWidth,
textRects,
strokeWidth,
dataLabelsX,
dataLabelsY,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
offX,
offY,
}
if (this.barCtx.isHorizontal) {
dataLabelsPos = this.calculateBarsDataLabelsPosition(params)
} else {
dataLabelsPos = this.calculateColumnsDataLabelsPosition(params)
}
renderedPath.attr({
cy: dataLabelsPos.bcy,
cx: dataLabelsPos.bcx,
j,
val: series[i][j],
barHeight,
barWidth,
})
dataLabels = this.drawCalculatedDataLabels({
x: dataLabelsPos.dataLabelsX,
y: dataLabelsPos.dataLabelsY,
val: this.barCtx.isRangeBar ? [y1, y2] : series[i][j],
i: realIndex,
j,
barWidth,
barHeight,
textRects,
dataLabelsConfig,
})
if (w.config.chart.stacked && barTotalDataLabelsConfig.enabled) {
totalDataLabels = this.drawTotalDataLabels({
x: dataLabelsPos.totalDataLabelsX,
y: dataLabelsPos.totalDataLabelsY,
barWidth,
barHeight,
realIndex,
textAnchor: dataLabelsPos.totalDataLabelsAnchor,
val: this.getStackedTotalDataLabel({ realIndex, j }),
dataLabelsConfig,
barTotalDataLabelsConfig,
})
}
return {
dataLabels,
totalDataLabels,
}
}
getStackedTotalDataLabel({ realIndex, j }) {
const w = this.w
let val = this.barCtx.stackedSeriesTotals[j]
if (this.totalFormatter) {
val = this.totalFormatter(val, {
...w,
seriesIndex: realIndex,
dataPointIndex: j,
w,
})
}
return val
}
calculateColumnsDataLabelsPosition(opts) {
const w = this.w
let {
i,
j,
realIndex,
groupIndex,
y,
bcx,
barWidth,
barHeight,
textRects,
dataLabelsX,
dataLabelsY,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
strokeWidth,
offX,
offY,
} = opts
let totalDataLabelsY
let totalDataLabelsX
let totalDataLabelsAnchor = 'middle'
barHeight = Math.abs(barHeight)
let vertical =
w.config.plotOptions.bar.dataLabels.orientation === 'vertical'
const { zeroEncounters } = this.barCtx.barHelpers.getZeroValueEncounters({
i,
j,
})
bcx =
bcx - strokeWidth / 2 + (groupIndex !== -1 ? groupIndex * barWidth : 0)
let dataPointsDividedWidth = w.globals.gridWidth / w.globals.dataPoints
if (this.barCtx.isVerticalGroupedRangeBar) {
dataLabelsX = dataLabelsX + barWidth / 2
} else {
if (w.globals.isXNumeric) {
dataLabelsX = bcx - barWidth / 2 + offX
} else {
dataLabelsX = bcx - dataPointsDividedWidth + barWidth / 2 + offX
}
if (
zeroEncounters > 0 &&
w.config.plotOptions.bar.hideZeroBarsWhenGrouped
) {
dataLabelsX = dataLabelsX - barWidth * zeroEncounters
}
}
if (vertical) {
const offsetDLX = 2
dataLabelsX =
dataLabelsX + textRects.height / 2 - strokeWidth / 2 - offsetDLX
}
let valIsNegative = this.barCtx.series[i][j] < 0
let newY = y
if (this.barCtx.isReversed) {
newY = y - barHeight + (valIsNegative ? barHeight * 2 : 0)
y = y - barHeight
}
switch (barDataLabelsConfig.position) {
case 'center':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY - barHeight / 2 + offY
} else {
dataLabelsY = newY + barHeight / 2 - offY
}
} else {
if (valIsNegative) {
dataLabelsY = newY - barHeight / 2 + textRects.height / 2 + offY
} else {
dataLabelsY = newY + barHeight / 2 + textRects.height / 2 - offY
}
}
break
case 'bottom':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY - barHeight + offY
} else {
dataLabelsY = newY + barHeight - offY
}
} else {
if (valIsNegative) {
dataLabelsY =
newY - barHeight + textRects.height + strokeWidth + offY
} else {
dataLabelsY =
newY + barHeight - textRects.height / 2 + strokeWidth - offY
}
}
break
case 'top':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY + offY
} else {
dataLabelsY = newY - offY
}
} else {
if (valIsNegative) {
dataLabelsY = newY - textRects.height / 2 - offY
} else {
dataLabelsY = newY + textRects.height + offY
}
}
break
}
if (
this.barCtx.lastActiveBarSerieIndex === realIndex &&
barTotalDataLabelsConfig.enabled
) {
const ADDITIONAL_OFFX = 18
const graphics = new Graphics(this.barCtx.ctx)
const totalLabeltextRects = graphics.getTextRects(
this.getStackedTotalDataLabel({ realIndex, j }),
dataLabelsConfig.fontSize
)
if (valIsNegative) {
totalDataLabelsY =
newY -
totalLabeltextRects.height / 2 -
offY -
barTotalDataLabelsConfig.offsetY +
ADDITIONAL_OFFX
} else {
totalDataLabelsY =
newY +
totalLabeltextRects.height +
offY +
barTotalDataLabelsConfig.offsetY -
ADDITIONAL_OFFX
}
totalDataLabelsX = dataLabelsX + barTotalDataLabelsConfig.offsetX
}
if (!w.config.chart.stacked) {
if (dataLabelsY < 0) {
dataLabelsY = 0 + strokeWidth
} else if (dataLabelsY + textRects.height / 3 > w.globals.gridHeight) {
dataLabelsY = w.globals.gridHeight - strokeWidth
}
}
return {
bcx,
bcy: y,
dataLabelsX,
dataLabelsY,
totalDataLabelsX,
totalDataLabelsY,
totalDataLabelsAnchor,
}
}
calculateBarsDataLabelsPosition(opts) {
const w = this.w
let {
x,
i,
j,
realIndex,
groupIndex,
bcy,
barHeight,
barWidth,
textRects,
dataLabelsX,
strokeWidth,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
offX,
offY,
} = opts
let dataPointsDividedHeight = w.globals.gridHeight / w.globals.dataPoints
barWidth = Math.abs(barWidth)
bcy = bcy + (groupIndex !== -1 ? groupIndex * barHeight : 0)
let dataLabelsY =
bcy -
(this.barCtx.isRangeBar ? 0 : dataPointsDividedHeight) +
barHeight / 2 +
textRects.height / 2 +
offY -
3
let totalDataLabelsX
let totalDataLabelsY
let totalDataLabelsAnchor = 'start'
let valIsNegative = this.barCtx.series[i][j] < 0
let newX = x
if (this.barCtx.isReversed) {
newX = x + barWidth - (valIsNegative ? barWidth * 2 : 0)
x = w.globals.gridWidth - barWidth
}
switch (barDataLabelsConfig.position) {
case 'center':
if (valIsNegative) {
dataLabelsX = newX + barWidth / 2 - offX
} else {
dataLabelsX =
Math.max(textRects.width / 2, newX - barWidth / 2) + offX
}
break
case 'bottom':
if (valIsNegative) {
dataLabelsX =
newX +
barWidth -
strokeWidth -
Math.round(textRects.width / 2) -
offX
} else {
dataLabelsX =
newX -
barWidth +
strokeWidth +
Math.round(textRects.width / 2) +
offX
}
break
case 'top':
if (valIsNegative) {
dataLabelsX =
newX - strokeWidth + Math.round(textRects.width / 2) - offX
} else {
dataLabelsX =
newX - strokeWidth - Math.round(textRects.width / 2) + offX
}
break
}
if (
this.barCtx.lastActiveBarSerieIndex === realIndex &&
barTotalDataLabelsConfig.enabled
) {
const ADDITIONAL_OFFX = 15
const graphics = new Graphics(this.barCtx.ctx)
const totalLabeltextRects = graphics.getTextRects(
this.getStackedTotalDataLabel({ realIndex, j }),
dataLabelsConfig.fontSize
)
if (valIsNegative) {
totalDataLabelsX =
newX -
strokeWidth +
Math.round(totalLabeltextRects.width / 2) -
offX -
barTotalDataLabelsConfig.offsetX -
ADDITIONAL_OFFX
totalDataLabelsAnchor = 'end'
} else {
totalDataLabelsX =
newX -
strokeWidth -
Math.round(totalLabeltextRects.width / 2) +
offX +
barTotalDataLabelsConfig.offsetX +
ADDITIONAL_OFFX
}
totalDataLabelsY = dataLabelsY + barTotalDataLabelsConfig.offsetY
}
if (!w.config.chart.stacked) {
if (dataLabelsX < 0) {
dataLabelsX = dataLabelsX + textRects.width + strokeWidth
} else if (dataLabelsX + textRects.width / 2 > w.globals.gridWidth) {
dataLabelsX = w.globals.gridWidth - textRects.width - strokeWidth
}
}
return {
bcx: x,
bcy,
dataLabelsX,
dataLabelsY,
totalDataLabelsX,
totalDataLabelsY,
totalDataLabelsAnchor,
}
}
drawCalculatedDataLabels({
x,
y,
val,
i, // = realIndex
j,
textRects,
barHeight,
barWidth,
dataLabelsConfig,
}) {
const w = this.w
let rotate = 'rotate(0)'
if (w.config.plotOptions.bar.dataLabels.orientation === 'vertical')
rotate = `rotate(-90, ${x}, ${y})`
const dataLabels = new DataLabels(this.barCtx.ctx)
const graphics = new Graphics(this.barCtx.ctx)
const formatter = dataLabelsConfig.formatter
let elDataLabelsWrap = null
const isSeriesNotCollapsed =
w.globals.collapsedSeriesIndices.indexOf(i) > -1
if (dataLabelsConfig.enabled && !isSeriesNotCollapsed) {
elDataLabelsWrap = graphics.group({
class: 'apexcharts-data-labels',
transform: rotate,
})
let text = ''
if (typeof val !== 'undefined') {
text = formatter(val, {
...w,
seriesIndex: i,
dataPointIndex: j,
w,
})
}
if (!val && w.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
text = ''
}
let valIsNegative = w.globals.series[i][j] < 0
let position = w.config.plotOptions.bar.dataLabels.position
if (w.config.plotOptions.bar.dataLabels.orientation === 'vertical') {
if (position === 'top') {
if (valIsNegative) dataLabelsConfig.textAnchor = 'end'
else dataLabelsConfig.textAnchor = 'start'
}
if (position === 'center') {
dataLabelsConfig.textAnchor = 'middle'
}
if (position === 'bottom') {
if (valIsNegative) dataLabelsConfig.textAnchor = 'end'
else dataLabelsConfig.textAnchor = 'start'
}
}
if (
this.barCtx.isRangeBar &&
this.barCtx.barOptions.dataLabels.hideOverflowingLabels
) {
// hide the datalabel if it cannot fit into the rect
const txRect = graphics.getTextRects(
text,
parseFloat(dataLabelsConfig.style.fontSize)
)
if (barWidth < txRect.width) {
text = ''
}
}
if (
w.config.chart.stacked &&
this.barCtx.barOptions.dataLabels.hideOverflowingLabels
) {
// if there is not enough space to draw the label in the bar/column rect, check hideOverflowingLabels property to prevent overflowing on wrong rect
// Note: This issue is only seen in stacked charts
if (this.barCtx.isHorizontal) {
if (textRects.width / 1.6 > Math.abs(barWidth)) {
text = ''
}
} else {
if (textRects.height / 1.6 > Math.abs(barHeight)) {
text = ''
}
}
}
let modifiedDataLabelsConfig = {
...dataLabelsConfig,
}
if (this.barCtx.isHorizontal) {
if (val < 0) {
if (dataLabelsConfig.textAnchor === 'start') {
modifiedDataLabelsConfig.textAnchor = 'end'
} else if (dataLabelsConfig.textAnchor === 'end') {
modifiedDataLabelsConfig.textAnchor = 'start'
}
}
}
dataLabels.plotDataLabelsText({
x,
y,
text,
i,
j,
parent: elDataLabelsWrap,
dataLabelsConfig: modifiedDataLabelsConfig,
alwaysDrawDataLabel: true,
offsetCorrection: true,
})
}
return elDataLabelsWrap
}
drawTotalDataLabels({
x,
y,
val,
barWidth,
barHeight,
realIndex,
textAnchor,
barTotalDataLabelsConfig,
}) {
const w = this.w
const graphics = new Graphics(this.barCtx.ctx)
let totalDataLabelText
if (
barTotalDataLabelsConfig.enabled &&
typeof x !== 'undefined' &&
typeof y !== 'undefined' &&
this.barCtx.lastActiveBarSerieIndex === realIndex
) {
totalDataLabelText = graphics.drawText({
x:
x -
(!w.globals.isBarHorizontal && w.globals.seriesGroups.length
? barWidth / w.globals.seriesGroups.length
: 0),
y:
y -
(w.globals.isBarHorizontal && w.globals.seriesGroups.length
? barHeight / w.globals.seriesGroups.length
: 0),
foreColor: barTotalDataLabelsConfig.style.color,
text: val,
textAnchor,
fontFamily: barTotalDataLabelsConfig.style.fontFamily,
fontSize: barTotalDataLabelsConfig.style.fontSize,
fontWeight: barTotalDataLabelsConfig.style.fontWeight,
})
}
return totalDataLabelText
}
}
+699
View File
@@ -0,0 +1,699 @@
import Fill from '../../../modules/Fill'
import Graphics from '../../../modules/Graphics'
import Series from '../../../modules/Series'
import Utils from '../../../utils/Utils'
export default class Helpers {
constructor(barCtx) {
this.w = barCtx.w
this.barCtx = barCtx
}
initVariables(series) {
const w = this.w
this.barCtx.series = series
this.barCtx.totalItems = 0
this.barCtx.seriesLen = 0
this.barCtx.visibleI = -1 // visible Series
this.barCtx.visibleItems = 1 // number of visible bars after user zoomed in/out
for (let sl = 0; sl < series.length; sl++) {
if (series[sl].length > 0) {
this.barCtx.seriesLen = this.barCtx.seriesLen + 1
this.barCtx.totalItems += series[sl].length
}
if (w.globals.isXNumeric) {
// get max visible items
for (let j = 0; j < series[sl].length; j++) {
if (
w.globals.seriesX[sl][j] > w.globals.minX &&
w.globals.seriesX[sl][j] < w.globals.maxX
) {
this.barCtx.visibleItems++
}
}
} else {
this.barCtx.visibleItems = w.globals.dataPoints
}
}
if (this.barCtx.seriesLen === 0) {
// A small adjustment when combo charts are used
this.barCtx.seriesLen = 1
}
this.barCtx.zeroSerieses = []
if (!w.globals.comboCharts) {
this.checkZeroSeries({ series })
}
}
initialPositions() {
let w = this.w
let x, y, yDivision, xDivision, barHeight, barWidth, zeroH, zeroW
let dataPoints = w.globals.dataPoints
if (this.barCtx.isRangeBar) {
// timeline rangebar chart
dataPoints = w.globals.labels.length
}
let seriesLen = this.barCtx.seriesLen
if (w.config.plotOptions.bar.rangeBarGroupRows) {
seriesLen = 1
}
if (this.barCtx.isHorizontal) {
// height divided into equal parts
yDivision = w.globals.gridHeight / dataPoints
barHeight = yDivision / seriesLen
if (w.globals.isXNumeric) {
yDivision = w.globals.gridHeight / this.barCtx.totalItems
barHeight = yDivision / this.barCtx.seriesLen
}
barHeight =
(barHeight * parseInt(this.barCtx.barOptions.barHeight, 10)) / 100
if (String(this.barCtx.barOptions.barHeight).indexOf('%') === -1) {
barHeight = parseInt(this.barCtx.barOptions.barHeight, 10)
}
zeroW =
this.barCtx.baseLineInvertedY +
w.globals.padHorizontal +
(this.barCtx.isReversed ? w.globals.gridWidth : 0) -
(this.barCtx.isReversed ? this.barCtx.baseLineInvertedY * 2 : 0)
if (this.barCtx.isFunnel) {
zeroW = w.globals.gridWidth / 2
}
y = (yDivision - barHeight * this.barCtx.seriesLen) / 2
} else {
// width divided into equal parts
xDivision = w.globals.gridWidth / this.barCtx.visibleItems
if (w.config.xaxis.convertedCatToNumeric) {
xDivision = w.globals.gridWidth / w.globals.dataPoints
}
barWidth =
((xDivision / seriesLen) *
parseInt(this.barCtx.barOptions.columnWidth, 10)) /
100
if (w.globals.isXNumeric) {
// max barwidth should be equal to minXDiff to avoid overlap
let xRatio = this.barCtx.xRatio
if (
w.globals.minXDiff &&
w.globals.minXDiff !== 0.5 &&
w.globals.minXDiff / xRatio > 0
) {
xDivision = w.globals.minXDiff / xRatio
}
barWidth =
((xDivision / seriesLen) *
parseInt(this.barCtx.barOptions.columnWidth, 10)) /
100
if (barWidth < 1) {
barWidth = 1
}
}
if (String(this.barCtx.barOptions.columnWidth).indexOf('%') === -1) {
barWidth = parseInt(this.barCtx.barOptions.columnWidth, 10)
}
zeroH =
w.globals.gridHeight -
this.barCtx.baseLineY[this.barCtx.yaxisIndex] -
(this.barCtx.isReversed ? w.globals.gridHeight : 0) +
(this.barCtx.isReversed
? this.barCtx.baseLineY[this.barCtx.yaxisIndex] * 2
: 0)
x =
w.globals.padHorizontal +
(xDivision - barWidth * this.barCtx.seriesLen) / 2
}
w.globals.barHeight = barHeight
w.globals.barWidth = barWidth
return {
x,
y,
yDivision,
xDivision,
barHeight,
barWidth,
zeroH,
zeroW,
}
}
initializeStackedPrevVars(ctx) {
const w = ctx.w
if (w.globals.hasSeriesGroups) {
w.globals.seriesGroups.forEach((group) => {
if (!ctx[group]) ctx[group] = {}
ctx[group].prevY = []
ctx[group].prevX = []
ctx[group].prevYF = []
ctx[group].prevXF = []
ctx[group].prevYVal = []
ctx[group].prevXVal = []
})
} else {
ctx.prevY = [] // y position on chart (in columns)
ctx.prevX = [] // x position on chart (in horz bars)
ctx.prevYF = [] // starting y and ending y (height) in columns
ctx.prevXF = [] // starting x and ending x (width) in bars
ctx.prevYVal = [] // y values (series[i][j]) in columns
ctx.prevXVal = [] // x values (series[i][j]) in bars
}
}
initializeStackedXYVars(ctx) {
const w = ctx.w
if (w.globals.hasSeriesGroups) {
w.globals.seriesGroups.forEach((group) => {
if (!ctx[group]) ctx[group] = {}
ctx[group].xArrj = []
ctx[group].xArrjF = []
ctx[group].xArrjVal = []
ctx[group].yArrj = []
ctx[group].yArrjF = []
ctx[group].yArrjVal = []
})
} else {
ctx.xArrj = [] // xj indicates x position on graph in bars
ctx.xArrjF = [] // xjF indicates bar's x position + x2 positions in bars
ctx.xArrjVal = [] // x val means the actual series's y values in horizontal/bars
ctx.yArrj = [] // yj indicates y position on graph in columns
ctx.yArrjF = [] // yjF indicates bar's y position + y2 positions in columns
ctx.yArrjVal = [] // y val means the actual series's y values in columns
}
}
getPathFillColor(series, i, j, realIndex) {
const w = this.w
let fill = new Fill(this.barCtx.ctx)
let fillColor = null
let seriesNumber = this.barCtx.barOptions.distributed ? j : i
if (this.barCtx.barOptions.colors.ranges.length > 0) {
const colorRange = this.barCtx.barOptions.colors.ranges
colorRange.map((range) => {
if (series[i][j] >= range.from && series[i][j] <= range.to) {
fillColor = range.color
}
})
}
if (w.config.series[i].data[j] && w.config.series[i].data[j].fillColor) {
fillColor = w.config.series[i].data[j].fillColor
}
let pathFill = fill.fillPath({
seriesNumber: this.barCtx.barOptions.distributed
? seriesNumber
: realIndex,
dataPointIndex: j,
color: fillColor,
value: series[i][j],
fillConfig: w.config.series[i].data[j]?.fill,
fillType: w.config.series[i].data[j]?.fill?.type
? w.config.series[i].data[j]?.fill.type
: Array.isArray(w.config.fill.type)
? w.config.fill.type[i]
: w.config.fill.type,
})
return pathFill
}
getStrokeWidth(i, j, realIndex) {
let strokeWidth = 0
const w = this.w
if (
typeof this.barCtx.series[i][j] === 'undefined' ||
this.barCtx.series[i][j] === null
) {
this.barCtx.isNullValue = true
} else {
this.barCtx.isNullValue = false
}
if (w.config.stroke.show) {
if (!this.barCtx.isNullValue) {
strokeWidth = Array.isArray(this.barCtx.strokeWidth)
? this.barCtx.strokeWidth[realIndex]
: this.barCtx.strokeWidth
}
}
return strokeWidth
}
shouldApplyRadius(realIndex) {
const w = this.w
let applyRadius = false
if (w.config.plotOptions.bar.borderRadius > 0) {
if (w.config.chart.stacked) {
if (w.config.plotOptions.bar.borderRadiusWhenStacked === 'last') {
if (this.barCtx.lastActiveBarSerieIndex === realIndex) {
applyRadius = true
}
} else {
applyRadius = true
}
} else {
applyRadius = true
}
}
return applyRadius
}
barBackground({ j, i, x1, x2, y1, y2, elSeries }) {
const w = this.w
const graphics = new Graphics(this.barCtx.ctx)
const sr = new Series(this.barCtx.ctx)
let activeSeriesIndex = sr.getActiveConfigSeriesIndex()
if (
this.barCtx.barOptions.colors.backgroundBarColors.length > 0 &&
activeSeriesIndex === i
) {
if (j >= this.barCtx.barOptions.colors.backgroundBarColors.length) {
j %= this.barCtx.barOptions.colors.backgroundBarColors.length
}
let bcolor = this.barCtx.barOptions.colors.backgroundBarColors[j]
let rect = graphics.drawRect(
typeof x1 !== 'undefined' ? x1 : 0,
typeof y1 !== 'undefined' ? y1 : 0,
typeof x2 !== 'undefined' ? x2 : w.globals.gridWidth,
typeof y2 !== 'undefined' ? y2 : w.globals.gridHeight,
this.barCtx.barOptions.colors.backgroundBarRadius,
bcolor,
this.barCtx.barOptions.colors.backgroundBarOpacity
)
elSeries.add(rect)
rect.node.classList.add('apexcharts-backgroundBar')
}
}
getColumnPaths({
barWidth,
barXPosition,
y1,
y2,
strokeWidth,
seriesGroup,
realIndex,
i,
j,
w,
}) {
const graphics = new Graphics(this.barCtx.ctx)
strokeWidth = Array.isArray(strokeWidth)
? strokeWidth[realIndex]
: strokeWidth
if (!strokeWidth) strokeWidth = 0
let bW = barWidth
let bXP = barXPosition
if (w.config.series[realIndex].data[j]?.columnWidthOffset) {
bXP =
barXPosition - w.config.series[realIndex].data[j].columnWidthOffset / 2
bW = barWidth + w.config.series[realIndex].data[j].columnWidthOffset
}
const x1 = bXP
const x2 = bXP + bW
// append tiny pixels to avoid exponentials (which cause issues in border-radius)
y1 += 0.001
y2 += 0.001
let pathTo = graphics.move(x1, y1)
let pathFrom = graphics.move(x1, y1)
const sl = graphics.line(x2 - strokeWidth, y1)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, false)
}
pathTo =
pathTo +
graphics.line(x1, y2) +
graphics.line(x2 - strokeWidth, y2) +
graphics.line(x2 - strokeWidth, y1) +
(w.config.plotOptions.bar.borderRadiusApplication === 'around'
? ' Z'
: ' z')
// the lines in pathFrom are repeated to equal it to the points of pathTo
// this is to avoid weird animation (bug in svg.js)
pathFrom =
pathFrom +
graphics.line(x1, y1) +
sl +
sl +
sl +
sl +
sl +
graphics.line(x1, y1) +
(w.config.plotOptions.bar.borderRadiusApplication === 'around'
? ' Z'
: ' z')
if (this.shouldApplyRadius(realIndex)) {
pathTo = graphics.roundPathCorners(
pathTo,
w.config.plotOptions.bar.borderRadius
)
}
if (w.config.chart.stacked) {
let _ctx = this.barCtx
if (w.globals.hasSeriesGroups && seriesGroup) {
_ctx = this.barCtx[seriesGroup]
}
_ctx.yArrj.push(y2)
_ctx.yArrjF.push(Math.abs(y1 - y2))
_ctx.yArrjVal.push(this.barCtx.series[i][j])
}
return {
pathTo,
pathFrom,
}
}
getBarpaths({
barYPosition,
barHeight,
x1,
x2,
strokeWidth,
seriesGroup,
realIndex,
i,
j,
w,
}) {
const graphics = new Graphics(this.barCtx.ctx)
strokeWidth = Array.isArray(strokeWidth)
? strokeWidth[realIndex]
: strokeWidth
if (!strokeWidth) strokeWidth = 0
let bYP = barYPosition
let bH = barHeight
if (w.config.series[realIndex].data[j]?.barHeightOffset) {
bYP =
barYPosition - w.config.series[realIndex].data[j].barHeightOffset / 2
bH = barHeight + w.config.series[realIndex].data[j].barHeightOffset
}
const y1 = bYP
const y2 = bYP + bH
// append tiny pixels to avoid exponentials (which cause issues in border-radius)
x1 += 0.001
x2 += 0.001
let pathTo = graphics.move(x1, y1)
let pathFrom = graphics.move(x1, y1)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, false)
}
const sl = graphics.line(x1, y2 - strokeWidth)
pathTo =
pathTo +
graphics.line(x2, y1) +
graphics.line(x2, y2 - strokeWidth) +
sl +
(w.config.plotOptions.bar.borderRadiusApplication === 'around'
? ' Z'
: ' z')
pathFrom =
pathFrom +
graphics.line(x1, y1) +
sl +
sl +
sl +
sl +
sl +
graphics.line(x1, y1) +
(w.config.plotOptions.bar.borderRadiusApplication === 'around'
? ' Z'
: ' z')
if (this.shouldApplyRadius(realIndex)) {
pathTo = graphics.roundPathCorners(
pathTo,
w.config.plotOptions.bar.borderRadius
)
}
if (w.config.chart.stacked) {
let _ctx = this.barCtx
if (w.globals.hasSeriesGroups && seriesGroup) {
_ctx = this.barCtx[seriesGroup]
}
_ctx.xArrj.push(x2)
_ctx.xArrjF.push(Math.abs(x1 - x2))
_ctx.xArrjVal.push(this.barCtx.series[i][j])
}
return {
pathTo,
pathFrom,
}
}
checkZeroSeries({ series }) {
let w = this.w
for (let zs = 0; zs < series.length; zs++) {
let total = 0
for (
let zsj = 0;
zsj < series[w.globals.maxValsInArrayIndex].length;
zsj++
) {
total += series[zs][zsj]
}
if (total === 0) {
this.barCtx.zeroSerieses.push(zs)
}
}
}
getXForValue(value, zeroW, zeroPositionForNull = true) {
let xForVal = zeroPositionForNull ? zeroW : null
if (typeof value !== 'undefined' && value !== null) {
xForVal =
zeroW +
value / this.barCtx.invertedYRatio -
(this.barCtx.isReversed ? value / this.barCtx.invertedYRatio : 0) * 2
}
return xForVal
}
getYForValue(value, zeroH, zeroPositionForNull = true) {
let yForVal = zeroPositionForNull ? zeroH : null
if (typeof value !== 'undefined' && value !== null) {
yForVal =
zeroH -
value / this.barCtx.yRatio[this.barCtx.yaxisIndex] +
(this.barCtx.isReversed
? value / this.barCtx.yRatio[this.barCtx.yaxisIndex]
: 0) *
2
}
return yForVal
}
getGoalValues(type, zeroW, zeroH, i, j) {
const w = this.w
let goals = []
const pushGoal = (value, attrs) => {
goals.push({
[type]:
type === 'x'
? this.getXForValue(value, zeroW, false)
: this.getYForValue(value, zeroH, false),
attrs,
})
}
if (
w.globals.seriesGoals[i] &&
w.globals.seriesGoals[i][j] &&
Array.isArray(w.globals.seriesGoals[i][j])
) {
w.globals.seriesGoals[i][j].forEach((goal) => {
pushGoal(goal.value, goal)
})
}
if (this.barCtx.barOptions.isDumbbell && w.globals.seriesRange.length) {
let colors = this.barCtx.barOptions.dumbbellColors
? this.barCtx.barOptions.dumbbellColors
: w.globals.colors
const commonAttrs = {
strokeHeight: type === 'x' ? 0 : w.globals.markers.size[i],
strokeWidth: type === 'x' ? w.globals.markers.size[i] : 0,
strokeDashArray: 0,
strokeLineCap: 'round',
strokeColor: Array.isArray(colors[i]) ? colors[i][0] : colors[i],
}
pushGoal(w.globals.seriesRangeStart[i][j], commonAttrs)
pushGoal(w.globals.seriesRangeEnd[i][j], {
...commonAttrs,
strokeColor: Array.isArray(colors[i]) ? colors[i][1] : colors[i],
})
}
return goals
}
drawGoalLine({
barXPosition,
barYPosition,
goalX,
goalY,
barWidth,
barHeight,
}) {
let graphics = new Graphics(this.barCtx.ctx)
const lineGroup = graphics.group({
className: 'apexcharts-bar-goals-groups',
})
lineGroup.node.classList.add('apexcharts-element-hidden')
this.barCtx.w.globals.delayedElements.push({
el: lineGroup.node,
})
lineGroup.attr(
'clip-path',
`url(#gridRectMarkerMask${this.barCtx.w.globals.cuid})`
)
let line = null
if (this.barCtx.isHorizontal) {
if (Array.isArray(goalX)) {
goalX.forEach((goal) => {
let sHeight =
typeof goal.attrs.strokeHeight !== 'undefined'
? goal.attrs.strokeHeight
: barHeight / 2
let y = barYPosition + sHeight + barHeight / 2
line = graphics.drawLine(
goal.x,
y - sHeight * 2,
goal.x,
y,
goal.attrs.strokeColor ? goal.attrs.strokeColor : undefined,
goal.attrs.strokeDashArray,
goal.attrs.strokeWidth ? goal.attrs.strokeWidth : 2,
goal.attrs.strokeLineCap
)
lineGroup.add(line)
})
}
} else {
if (Array.isArray(goalY)) {
goalY.forEach((goal) => {
let sWidth =
typeof goal.attrs.strokeWidth !== 'undefined'
? goal.attrs.strokeWidth
: barWidth / 2
let x = barXPosition + sWidth + barWidth / 2
line = graphics.drawLine(
x - sWidth * 2,
goal.y,
x,
goal.y,
goal.attrs.strokeColor ? goal.attrs.strokeColor : undefined,
goal.attrs.strokeDashArray,
goal.attrs.strokeHeight ? goal.attrs.strokeHeight : 2,
goal.attrs.strokeLineCap
)
lineGroup.add(line)
})
}
}
return lineGroup
}
drawBarShadow({ prevPaths, currPaths, color }) {
const w = this.w
const { x: prevX2, x1: prevX1, barYPosition: prevY1 } = prevPaths
const { x: currX2, x1: currX1, barYPosition: currY1 } = currPaths
const prevY2 = prevY1 + currPaths.barHeight
const graphics = new Graphics(this.barCtx.ctx)
const utils = new Utils()
const shadowPath =
graphics.move(prevX1, prevY2) +
graphics.line(prevX2, prevY2) +
graphics.line(currX2, currY1) +
graphics.line(currX1, currY1) +
graphics.line(prevX1, prevY2) +
(w.config.plotOptions.bar.borderRadiusApplication === 'around'
? ' Z'
: ' z')
return graphics.drawPath({
d: shadowPath,
fill: utils.shadeColor(0.5, Utils.rgb2hex(color)),
stroke: 'none',
strokeWidth: 0,
fillOpacity: 1,
classes: 'apexcharts-bar-shadows',
})
}
getZeroValueEncounters({ i, j }) {
const w = this.w
let nonZeroColumns = 0
let zeroEncounters = 0
w.globals.seriesPercent.forEach((_s, _si) => {
if (_s[j]) {
nonZeroColumns++
}
if (_si < i && _s[j] === 0) {
zeroEncounters++
}
})
return {
nonZeroColumns,
zeroEncounters,
}
}
}
+30
View File
@@ -0,0 +1,30 @@
import Graphics from '../../../modules/Graphics'
export default class CircularChartsHelpers {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
drawYAxisTexts(x, y, i, text) {
const w = this.w
const yaxisConfig = w.config.yaxis[0]
const formatter = w.globals.yLabelFormatters[0]
const graphics = new Graphics(this.ctx)
const yaxisLabel = graphics.drawText({
x: x + yaxisConfig.labels.offsetX,
y: y + yaxisConfig.labels.offsetY,
text: formatter(text, i),
textAnchor: 'middle',
fontSize: yaxisConfig.labels.style.fontSize,
fontFamily: yaxisConfig.labels.style.fontFamily,
foreColor: Array.isArray(yaxisConfig.labels.style.colors)
? yaxisConfig.labels.style.colors[i]
: yaxisConfig.labels.style.colors
})
return yaxisLabel
}
}
+152
View File
@@ -0,0 +1,152 @@
import CoreUtils from '../../../modules/CoreUtils'
import Utils from '../../../utils/Utils'
export default class Helpers {
constructor(lineCtx) {
this.w = lineCtx.w
this.lineCtx = lineCtx
}
sameValueSeriesFix(i, series) {
const w = this.w
if (
w.config.fill.type === 'gradient' ||
w.config.fill.type[i] === 'gradient'
) {
const coreUtils = new CoreUtils(this.lineCtx.ctx, w)
// applied only to LINE chart
// a small adjustment to allow gradient line to draw correctly for all same values
/* #fix https://github.com/apexcharts/apexcharts.js/issues/358 */
if (coreUtils.seriesHaveSameValues(i)) {
let gSeries = series[i].slice()
gSeries[gSeries.length - 1] = gSeries[gSeries.length - 1] + 0.000001
series[i] = gSeries
}
}
return series
}
calculatePoints({ series, realIndex, x, y, i, j, prevY }) {
let w = this.w
let ptX = []
let ptY = []
if (j === 0) {
let xPT1st =
this.lineCtx.categoryAxisCorrection + w.config.markers.offsetX
// the first point for line series
// we need to check whether it's not a time series, because a time series may
// start from the middle of the x axis
if (w.globals.isXNumeric) {
xPT1st =
(w.globals.seriesX[realIndex][0] - w.globals.minX) /
this.lineCtx.xRatio +
w.config.markers.offsetX
}
// push 2 points for the first data values
ptX.push(xPT1st)
ptY.push(
Utils.isNumber(series[i][0]) ? prevY + w.config.markers.offsetY : null
)
ptX.push(x + w.config.markers.offsetX)
ptY.push(
Utils.isNumber(series[i][j + 1]) ? y + w.config.markers.offsetY : null
)
} else {
ptX.push(x + w.config.markers.offsetX)
ptY.push(
Utils.isNumber(series[i][j + 1]) ? y + w.config.markers.offsetY : null
)
}
let pointsPos = {
x: ptX,
y: ptY,
}
return pointsPos
}
checkPreviousPaths({ pathFromLine, pathFromArea, realIndex }) {
let w = this.w
for (let pp = 0; pp < w.globals.previousPaths.length; pp++) {
let gpp = w.globals.previousPaths[pp]
if (
(gpp.type === 'line' || gpp.type === 'area') &&
gpp.paths.length > 0 &&
parseInt(gpp.realIndex, 10) === parseInt(realIndex, 10)
) {
if (gpp.type === 'line') {
this.lineCtx.appendPathFrom = false
pathFromLine = w.globals.previousPaths[pp].paths[0].d
} else if (gpp.type === 'area') {
this.lineCtx.appendPathFrom = false
pathFromArea = w.globals.previousPaths[pp].paths[0].d
if (w.config.stroke.show && w.globals.previousPaths[pp].paths[1]) {
pathFromLine = w.globals.previousPaths[pp].paths[1].d
}
}
}
}
return {
pathFromLine,
pathFromArea,
}
}
determineFirstPrevY({ i, series, prevY, lineYPosition }) {
let w = this.w
let stackSeries =
(w.config.chart.stacked && !w.globals.comboCharts) ||
(w.config.chart.stacked &&
w.globals.comboCharts &&
(!this.w.config.chart.stackOnlyBar ||
this.w.config.series[i]?.type === 'bar'))
if (typeof series[i]?.[0] !== 'undefined') {
if (stackSeries) {
if (i > 0) {
// 1st y value of previous series
lineYPosition = this.lineCtx.prevSeriesY[i - 1][0]
} else {
// the first series will not have prevY values
lineYPosition = this.lineCtx.zeroY
}
} else {
lineYPosition = this.lineCtx.zeroY
}
prevY =
lineYPosition -
series[i][0] / this.lineCtx.yRatio[this.lineCtx.yaxisIndex] +
(this.lineCtx.isReversed
? series[i][0] / this.lineCtx.yRatio[this.lineCtx.yaxisIndex]
: 0) *
2
} else {
// the first value in the current series is null
if (stackSeries && i > 0 && typeof series[i][0] === 'undefined') {
// check for undefined value (undefined value will occur when we clear the series while user clicks on legend to hide serieses)
for (let s = i - 1; s >= 0; s--) {
// for loop to get to 1st previous value until we get it
if (series[s][0] !== null && typeof series[s][0] !== 'undefined') {
lineYPosition = this.lineCtx.prevSeriesY[s][0]
prevY = lineYPosition
break
}
}
}
}
return {
prevY,
lineYPosition,
}
}
}
+192
View File
@@ -0,0 +1,192 @@
import Utils from '../../../utils/Utils'
import Graphics from '../../../modules/Graphics'
import DataLabels from '../../../modules/DataLabels'
export default class TreemapHelpers {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
checkColorRange() {
const w = this.w
let negRange = false
let chartOpts = w.config.plotOptions[w.config.chart.type]
if (chartOpts.colorScale.ranges.length > 0) {
chartOpts.colorScale.ranges.map((range, index) => {
if (range.from <= 0) {
negRange = true
}
})
}
return negRange
}
getShadeColor(chartType, i, j, negRange) {
const w = this.w
let colorShadePercent = 1
let shadeIntensity = w.config.plotOptions[chartType].shadeIntensity
const colorProps = this.determineColor(chartType, i, j)
if (w.globals.hasNegs || negRange) {
if (w.config.plotOptions[chartType].reverseNegativeShade) {
if (colorProps.percent < 0) {
colorShadePercent =
(colorProps.percent / 100) * (shadeIntensity * 1.25)
} else {
colorShadePercent =
(1 - colorProps.percent / 100) * (shadeIntensity * 1.25)
}
} else {
if (colorProps.percent <= 0) {
colorShadePercent =
1 - (1 + colorProps.percent / 100) * shadeIntensity
} else {
colorShadePercent = (1 - colorProps.percent / 100) * shadeIntensity
}
}
} else {
colorShadePercent = 1 - colorProps.percent / 100
if (chartType === 'treemap') {
colorShadePercent =
(1 - colorProps.percent / 100) * (shadeIntensity * 1.25)
}
}
let color = colorProps.color
let utils = new Utils()
if (w.config.plotOptions[chartType].enableShades) {
if (this.w.config.theme.mode === 'dark') {
color = Utils.hexToRgba(
utils.shadeColor(colorShadePercent * -1, colorProps.color),
w.config.fill.opacity
)
} else {
color = Utils.hexToRgba(
utils.shadeColor(colorShadePercent, colorProps.color),
w.config.fill.opacity
)
}
}
return { color, colorProps }
}
determineColor(chartType, i, j) {
const w = this.w
let val = w.globals.series[i][j]
let chartOpts = w.config.plotOptions[chartType]
let seriesNumber = chartOpts.colorScale.inverse ? j : i
if (chartOpts.distributed && w.config.chart.type === 'treemap') {
seriesNumber = j
}
let color = w.globals.colors[seriesNumber]
let foreColor = null
let min = Math.min(...w.globals.series[i])
let max = Math.max(...w.globals.series[i])
if (!chartOpts.distributed && chartType === 'heatmap') {
min = w.globals.minY
max = w.globals.maxY
}
if (typeof chartOpts.colorScale.min !== 'undefined') {
min =
chartOpts.colorScale.min < w.globals.minY
? chartOpts.colorScale.min
: w.globals.minY
max =
chartOpts.colorScale.max > w.globals.maxY
? chartOpts.colorScale.max
: w.globals.maxY
}
let total = Math.abs(max) + Math.abs(min)
let percent = (100 * val) / (total === 0 ? total - 0.000001 : total)
if (chartOpts.colorScale.ranges.length > 0) {
const colorRange = chartOpts.colorScale.ranges
colorRange.map((range, index) => {
if (val >= range.from && val <= range.to) {
color = range.color
foreColor = range.foreColor ? range.foreColor : null
min = range.from
max = range.to
let rTotal = Math.abs(max) + Math.abs(min)
percent = (100 * val) / (rTotal === 0 ? rTotal - 0.000001 : rTotal)
}
})
}
return {
color,
foreColor,
percent
}
}
calculateDataLabels({ text, x, y, i, j, colorProps, fontSize }) {
let w = this.w
let dataLabelsConfig = w.config.dataLabels
const graphics = new Graphics(this.ctx)
let dataLabels = new DataLabels(this.ctx)
let elDataLabelsWrap = null
if (dataLabelsConfig.enabled) {
elDataLabelsWrap = graphics.group({
class: 'apexcharts-data-labels'
})
const offX = dataLabelsConfig.offsetX
const offY = dataLabelsConfig.offsetY
let dataLabelsX = x + offX
let dataLabelsY =
y + parseFloat(dataLabelsConfig.style.fontSize) / 3 + offY
dataLabels.plotDataLabelsText({
x: dataLabelsX,
y: dataLabelsY,
text,
i,
j,
color: colorProps.foreColor,
parent: elDataLabelsWrap,
fontSize,
dataLabelsConfig
})
}
return elDataLabelsWrap
}
addListeners(elRect) {
const graphics = new Graphics(this.ctx)
elRect.node.addEventListener(
'mouseenter',
graphics.pathMouseEnter.bind(this, elRect)
)
elRect.node.addEventListener(
'mouseleave',
graphics.pathMouseLeave.bind(this, elRect)
)
elRect.node.addEventListener(
'mousedown',
graphics.pathMouseDown.bind(this, elRect)
)
}
}
+290
View File
@@ -0,0 +1,290 @@
/*
* treemap-squarify.js - open source implementation of squarified treemaps
*
* Treemap Squared 0.5 - Treemap Charting library
*
* https://github.com/imranghory/treemap-squared/
*
* Copyright (c) 2012 Imran Ghory (imranghory@gmail.com)
* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
*
*
* Implementation of the squarify treemap algorithm described in:
*
* Bruls, Mark; Huizing, Kees; van Wijk, Jarke J. (2000), "Squarified treemaps"
* in de Leeuw, W.; van Liere, R., Data Visualization 2000:
* Proc. Joint Eurographics and IEEE TCVG Symp. on Visualization, Springer-Verlag, pp. 3342.
*
* Paper is available online at: http://www.win.tue.nl/~vanwijk/stm.pdf
*
* The code in this file is completeley decoupled from the drawing code so it should be trivial
* to port it to any other vector drawing library. Given an array of datapoints this library returns
* an array of cartesian coordinates that represent the rectangles that make up the treemap.
*
* The library also supports multidimensional data (nested treemaps) and performs normalization on the data.
*
* See the README file for more details.
*/
window.TreemapSquared = {}
;(function() {
'use strict'
window.TreemapSquared.generate = (function() {
function Container(xoffset, yoffset, width, height) {
this.xoffset = xoffset // offset from the the top left hand corner
this.yoffset = yoffset // ditto
this.height = height
this.width = width
this.shortestEdge = function() {
return Math.min(this.height, this.width)
}
// getCoordinates - for a row of boxes which we've placed
// return an array of their cartesian coordinates
this.getCoordinates = function(row) {
let coordinates = []
let subxoffset = this.xoffset,
subyoffset = this.yoffset //our offset within the container
let areawidth = sumArray(row) / this.height
let areaheight = sumArray(row) / this.width
let i
if (this.width >= this.height) {
for (i = 0; i < row.length; i++) {
coordinates.push([
subxoffset,
subyoffset,
subxoffset + areawidth,
subyoffset + row[i] / areawidth
])
subyoffset = subyoffset + row[i] / areawidth
}
} else {
for (i = 0; i < row.length; i++) {
coordinates.push([
subxoffset,
subyoffset,
subxoffset + row[i] / areaheight,
subyoffset + areaheight
])
subxoffset = subxoffset + row[i] / areaheight
}
}
return coordinates
}
// cutArea - once we've placed some boxes into an row we then need to identify the remaining area,
// this function takes the area of the boxes we've placed and calculates the location and
// dimensions of the remaining space and returns a container box defined by the remaining area
this.cutArea = function(area) {
let newcontainer
if (this.width >= this.height) {
let areawidth = area / this.height
let newwidth = this.width - areawidth
newcontainer = new Container(
this.xoffset + areawidth,
this.yoffset,
newwidth,
this.height
)
} else {
let areaheight = area / this.width
let newheight = this.height - areaheight
newcontainer = new Container(
this.xoffset,
this.yoffset + areaheight,
this.width,
newheight
)
}
return newcontainer
}
}
// normalize - the Bruls algorithm assumes we're passing in areas that nicely fit into our
// container box, this method takes our raw data and normalizes the data values into
// area values so that this assumption is valid.
function normalize(data, area) {
let normalizeddata = []
let sum = sumArray(data)
let multiplier = area / sum
let i
for (i = 0; i < data.length; i++) {
normalizeddata[i] = data[i] * multiplier
}
return normalizeddata
}
// treemapMultidimensional - takes multidimensional data (aka [[23,11],[11,32]] - nested array)
// and recursively calls itself using treemapSingledimensional
// to create a patchwork of treemaps and merge them
function treemapMultidimensional(data, width, height, xoffset, yoffset) {
xoffset = typeof xoffset === 'undefined' ? 0 : xoffset
yoffset = typeof yoffset === 'undefined' ? 0 : yoffset
let mergeddata = []
let mergedtreemap
let results = []
let i
if (isArray(data[0])) {
// if we've got more dimensions of depth
for (i = 0; i < data.length; i++) {
mergeddata[i] = sumMultidimensionalArray(data[i])
}
mergedtreemap = treemapSingledimensional(
mergeddata,
width,
height,
xoffset,
yoffset
)
for (i = 0; i < data.length; i++) {
results.push(
treemapMultidimensional(
data[i],
mergedtreemap[i][2] - mergedtreemap[i][0],
mergedtreemap[i][3] - mergedtreemap[i][1],
mergedtreemap[i][0],
mergedtreemap[i][1]
)
)
}
} else {
results = treemapSingledimensional(
data,
width,
height,
xoffset,
yoffset
)
}
return results
}
// treemapSingledimensional - simple wrapper around squarify
function treemapSingledimensional(data, width, height, xoffset, yoffset) {
xoffset = typeof xoffset === 'undefined' ? 0 : xoffset
yoffset = typeof yoffset === 'undefined' ? 0 : yoffset
let rawtreemap = squarify(
normalize(data, width * height),
[],
new Container(xoffset, yoffset, width, height),
[]
)
return flattenTreemap(rawtreemap)
}
// flattenTreemap - squarify implementation returns an array of arrays of coordinates
// because we have a new array everytime we switch to building a new row
// this converts it into an array of coordinates.
function flattenTreemap(rawtreemap) {
let flattreemap = []
let i, j
for (i = 0; i < rawtreemap.length; i++) {
for (j = 0; j < rawtreemap[i].length; j++) {
flattreemap.push(rawtreemap[i][j])
}
}
return flattreemap
}
// squarify - as per the Bruls paper
// plus coordinates stack and containers so we get
// usable data out of it
function squarify(data, currentrow, container, stack) {
let length
let nextdatapoint
let newcontainer
if (data.length === 0) {
stack.push(container.getCoordinates(currentrow))
return
}
length = container.shortestEdge()
nextdatapoint = data[0]
if (improvesRatio(currentrow, nextdatapoint, length)) {
currentrow.push(nextdatapoint)
squarify(data.slice(1), currentrow, container, stack)
} else {
newcontainer = container.cutArea(sumArray(currentrow), stack)
stack.push(container.getCoordinates(currentrow))
squarify(data, [], newcontainer, stack)
}
return stack
}
// improveRatio - implements the worse calculation and comparision as given in Bruls
// (note the error in the original paper; fixed here)
function improvesRatio(currentrow, nextnode, length) {
let newrow
if (currentrow.length === 0) {
return true
}
newrow = currentrow.slice()
newrow.push(nextnode)
let currentratio = calculateRatio(currentrow, length)
let newratio = calculateRatio(newrow, length)
// the pseudocode in the Bruls paper has the direction of the comparison
// wrong, this is the correct one.
return currentratio >= newratio
}
// calculateRatio - calculates the maximum width to height ratio of the
// boxes in this row
function calculateRatio(row, length) {
let min = Math.min.apply(Math, row)
let max = Math.max.apply(Math, row)
let sum = sumArray(row)
return Math.max(
(Math.pow(length, 2) * max) / Math.pow(sum, 2),
Math.pow(sum, 2) / (Math.pow(length, 2) * min)
)
}
// isArray - checks if arr is an array
function isArray(arr) {
return arr && arr.constructor === Array
}
// sumArray - sums a single dimensional array
function sumArray(arr) {
let sum = 0
let i
for (i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum
}
// sumMultidimensionalArray - sums the values in a nested array (aka [[0,1],[[2,3]]])
function sumMultidimensionalArray(arr) {
let i,
total = 0
if (isArray(arr[0])) {
for (i = 0; i < arr.length; i++) {
total += sumMultidimensionalArray(arr[i])
}
} else {
total = sumArray(arr)
}
return total
}
return treemapMultidimensional
})()
})()
+186
View File
@@ -0,0 +1,186 @@
/**
*
* @yr/monotone-cubic-spline (https://github.com/YR/monotone-cubic-spline)
*
* The MIT License (MIT)
*
* Copyright (c) 2015 yr.no
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* Generate tangents for 'points'
* @param {Array} points
* @returns {Array}
*/
export const tangents = (points) => {
const m = finiteDifferences(points)
const n = points.length - 1
const ε = 1e-6
const tgts = []
let a, b, d, s
for (let i = 0; i < n; i++) {
d = slope(points[i], points[i + 1])
if (Math.abs(d) < ε) {
m[i] = m[i + 1] = 0
} else {
a = m[i] / d
b = m[i + 1] / d
s = a * a + b * b
if (s > 9) {
s = (d * 3) / Math.sqrt(s)
m[i] = s * a
m[i + 1] = s * b
}
}
}
for (let i = 0; i <= n; i++) {
s =
(points[Math.min(n, i + 1)][0] - points[Math.max(0, i - 1)][0]) /
(6 * (1 + m[i] * m[i]))
tgts.push([s || 0, m[i] * s || 0])
}
return tgts
}
/**
* Convert 'points' to svg path
* @param {Array} points
* @returns {String}
*/
export const svgPath = (points) => {
let p = ''
for (let i = 0; i < points.length; i++) {
const point = points[i]
const n = point.length
if (n > 4) {
p += `C${point[0]}, ${point[1]}`
p += `, ${point[2]}, ${point[3]}`
p += `, ${point[4]}, ${point[5]}`
} else if (n > 2) {
p += `S${point[0]}, ${point[1]}`
p += `, ${point[2]}, ${point[3]}`
}
}
return p
}
export const spline = {
/**
* Convert 'points' to bezier
* @param {Array} points
* @returns {Array}
*/
points(points) {
const tgts = tangents(points)
const p = points[1]
const p0 = points[0]
const pts = []
const t = tgts[1]
const t0 = tgts[0]
// Add starting 'M' and 'C' points
pts.push(p0, [
p0[0] + t0[0],
p0[1] + t0[1],
p[0] - t[0],
p[1] - t[1],
p[0],
p[1],
])
// Add 'S' points
for (let i = 2, n = tgts.length; i < n; i++) {
const p = points[i]
const t = tgts[i]
pts.push([p[0] - t[0], p[1] - t[1], p[0], p[1]])
}
return pts
},
/**
* Slice out a segment of 'points'
* @param {Array} points
* @param {Number} start
* @param {Number} end
* @returns {Array}
*/
slice(points, start, end) {
const pts = points.slice(start, end)
if (start) {
// Add additional 'C' points
if (pts[1].length < 6) {
const n = pts[0].length
pts[1] = [
pts[0][n - 2] * 2 - pts[0][n - 4],
pts[0][n - 1] * 2 - pts[0][n - 3],
].concat(pts[1])
}
// Remove control points for 'M'
pts[0] = pts[0].slice(-2)
}
return pts
},
}
/**
* Compute slope from point 'p0' to 'p1'
* @param {Array} p0
* @param {Array} p1
* @returns {Number}
*/
function slope(p0, p1) {
return (p1[1] - p0[1]) / (p1[0] - p0[0])
}
/**
* Compute three-point differences for 'points'
* @param {Array} points
* @returns {Array}
*/
function finiteDifferences(points) {
const m = []
let p0 = points[0]
let p1 = points[1]
let d = (m[0] = slope(p0, p1))
let i = 1
for (let n = points.length - 1; i < n; i++) {
p0 = p1
p1 = points[i + 1]
m[i] = (d + (d = slope(p0, p1))) * 0.5
}
m[i] = d
return m
}
+63
View File
@@ -0,0 +1,63 @@
{
"name": "ar",
"options": {
"months": [
"يناير",
"فبراير",
"مارس",
"أبريل",
"مايو",
"يونيو",
"يوليو",
"أغسطس",
"سبتمبر",
"أكتوبر",
"نوفمبر",
"ديسمبر"
],
"shortMonths": [
"يناير",
"فبراير",
"مارس",
"أبريل",
"مايو",
"يونيو",
"يوليو",
"أغسطس",
"سبتمبر",
"أكتوبر",
"نوفمبر",
"ديسمبر"
],
"days": [
"الأحد",
"الإثنين",
"الثلاثاء",
"الأربعاء",
"الخميس",
"الجمعة",
"السبت"
],
"shortDays": [
"أحد",
"إثنين",
"ثلاثاء",
"أربعاء",
"خميس",
"جمعة",
"سبت"
],
"toolbar": {
"exportToSVG": "تحميل بصيغة SVG",
"exportToPNG": "تحميل بصيغة PNG",
"exportToCSV": "تحميل بصيغة CSV",
"menu": "القائمة",
"selection": "تحديد",
"selectionZoom": "تكبير التحديد",
"zoomIn": "تكبير",
"zoomOut": "تصغير",
"pan": "تحريك",
"reset": "إعادة التعيين"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "be-cyrl",
"options": {
"months": [
"Студзень",
"Люты",
"Сакавік",
"Красавік",
"Травень",
"Чэрвень",
"Ліпень",
"Жнівень",
"Верасень",
"Кастрычнік",
"Лістапад",
"Сьнежань"
],
"shortMonths": [
"Сту",
"Лют",
"Сак",
"Кра",
"Тра",
"Чэр",
"Ліп",
"Жні",
"Вер",
"Кас",
"Ліс",
"Сьн"
],
"days": [
"Нядзеля",
"Панядзелак",
"Аўторак",
"Серада",
"Чацьвер",
"Пятніца",
"Субота"
],
"shortDays": ["Нд", "Пн", "Аў", "Ср", "Чц", "Пт", "Сб"],
"toolbar": {
"exportToSVG": "Спампаваць SVG",
"exportToPNG": "Спампаваць PNG",
"exportToCSV": "Спампаваць CSV",
"menu": "Мэню",
"selection": "Вылучэньне",
"selectionZoom": "Вылучэньне з маштабаваньнем",
"zoomIn": "Наблізіць",
"zoomOut": "Аддаліць",
"pan": "Ссоўваньне",
"reset": "Скінуць маштабаваньне"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "be-latn",
"options": {
"months": [
"Studzień",
"Luty",
"Sakavik",
"Krasavik",
"Travień",
"Červień",
"Lipień",
"Žnivień",
"Vierasień",
"Kastryčnik",
"Listapad",
"Śniežań"
],
"shortMonths": [
"Stu",
"Lut",
"Sak",
"Kra",
"Tra",
"Čer",
"Lip",
"Žni",
"Vie",
"Kas",
"Lis",
"Śni"
],
"days": [
"Niadziela",
"Paniadziełak",
"Aŭtorak",
"Sierada",
"Čaćvier",
"Piatnica",
"Subota"
],
"shortDays": ["Nd", "Pn", "Aŭ", "Sr", "Čć", "Pt", "Sb"],
"toolbar": {
"exportToSVG": "Spampavać SVG",
"exportToPNG": "Spampavać PNG",
"exportToCSV": "Spampavać CSV",
"menu": "Meniu",
"selection": "Vyłučeńnie",
"selectionZoom": "Vyłučeńnie z maštabavańniem",
"zoomIn": "Nablizić",
"zoomOut": "Addalić",
"pan": "Ssoŭvańnie",
"reset": "Skinuć maštabavańnie"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "ca",
"options": {
"months": [
"Gener",
"Febrer",
"Març",
"Abril",
"Maig",
"Juny",
"Juliol",
"Agost",
"Setembre",
"Octubre",
"Novembre",
"Desembre"
],
"shortMonths": [
"Gen.",
"Febr.",
"Març",
"Abr.",
"Maig",
"Juny",
"Jul.",
"Ag.",
"Set.",
"Oct.",
"Nov.",
"Des."
],
"days": [
"Diumenge",
"Dilluns",
"Dimarts",
"Dimecres",
"Dijous",
"Divendres",
"Dissabte"
],
"shortDays": ["Dg", "Dl", "Dt", "Dc", "Dj", "Dv", "Ds"],
"toolbar": {
"exportToSVG": "Descarregar SVG",
"exportToPNG": "Descarregar PNG",
"exportToCSV": "Descarregar CSV",
"menu": "Menú",
"selection": "Seleccionar",
"selectionZoom": "Seleccionar Zoom",
"zoomIn": "Augmentar",
"zoomOut": "Disminuir",
"pan": "Navegació",
"reset": "Reiniciar Zoom"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "cs",
"options": {
"months": [
"Leden",
"Únor",
"Březen",
"Duben",
"Květen",
"Červen",
"Červenec",
"Srpen",
"Září",
"Říjen",
"Listopad",
"Prosinec"
],
"shortMonths": [
"Led",
"Úno",
"Bře",
"Dub",
"Kvě",
"Čvn",
"Čvc",
"Srp",
"Zář",
"Říj",
"Lis",
"Pro"
],
"days": [
"Neděle",
"Pondělí",
"Úterý",
"Středa",
"Čtvrtek",
"Pátek",
"Sobota"
],
"shortDays": ["Ne", "Po", "Út", "St", "Čt", "Pá", "So"],
"toolbar": {
"exportToSVG": "Stáhnout SVG",
"exportToPNG": "Stáhnout PNG",
"exportToCSV": "Stáhnout CSV",
"menu": "Menu",
"selection": "Vybrat",
"selectionZoom": "Zoom: Vybrat",
"zoomIn": "Zoom: Přiblížit",
"zoomOut": "Zoom: Oddálit",
"pan": "Přesouvat",
"reset": "Resetovat"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "da",
"options": {
"months": [
"januar",
"februar",
"marts",
"april",
"maj",
"juni",
"juli",
"august",
"september",
"oktober",
"november",
"december"
],
"shortMonths": [
"jan",
"feb",
"mar",
"apr",
"maj",
"jun",
"jul",
"aug",
"sep",
"okt",
"nov",
"dec"
],
"days": [
"Søndag",
"Mandag",
"Tirsdag",
"Onsdag",
"Torsdag",
"Fredag",
"Lørdag"
],
"shortDays": ["Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør"],
"toolbar": {
"exportToSVG": "Download SVG",
"exportToPNG": "Download PNG",
"exportToCSV": "Download CSV",
"menu": "Menu",
"selection": "Valg",
"selectionZoom": "Zoom til valg",
"zoomIn": "Zoom ind",
"zoomOut": "Zoom ud",
"pan": "Panorér",
"reset": "Nulstil zoom"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "de",
"options": {
"months": [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember"
],
"shortMonths": [
"Jan",
"Feb",
"Mär",
"Apr",
"Mai",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dez"
],
"days": [
"Sonntag",
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag"
],
"shortDays": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
"toolbar": {
"exportToSVG": "SVG speichern",
"exportToPNG": "PNG speichern",
"exportToCSV": "CSV speichern",
"menu": "Menü",
"selection": "Auswahl",
"selectionZoom": "Auswahl vergrößern",
"zoomIn": "Vergrößern",
"zoomOut": "Verkleinern",
"pan": "Verschieben",
"reset": "Zoom zurücksetzen"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "el",
"options": {
"months": [
"Ιανουάριος",
"Φεβρουάριος",
"Μάρτιος",
"Απρίλιος",
"Μάιος",
"Ιούνιος",
"Ιούλιος",
"Αύγουστος",
"Σεπτέμβριος",
"Οκτώβριος",
"Νοέμβριος",
"Δεκέμβριος"
],
"shortMonths": [
"Ιαν",
"Φευ",
"Μαρ",
"Απρ",
"Μάι",
"Ιουν",
"Ιουλ",
"Αυγ",
"Σεπ",
"Οκτ",
"Νοε",
"Δεκ"
],
"days": [
"Κυριακή",
"Δευτέρα",
"Τρίτη",
"Τετάρτη",
"Πέμπτη",
"Παρασκευή",
"Σάββατο"
],
"shortDays": ["Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ"],
"toolbar": {
"exportToSVG": "Λήψη SVG",
"exportToPNG": "Λήψη PNG",
"exportToCSV": "Λήψη CSV",
"menu": "Menu",
"selection": "Επιλογή",
"selectionZoom": "Μεγένθυση βάση επιλογής",
"zoomIn": "Μεγένθυνση",
"zoomOut": "Σμίκρυνση",
"pan": "Μετατόπιση",
"reset": "Επαναφορά μεγένθυνσης"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "en",
"options": {
"months": [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
],
"days": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
"shortDays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
"toolbar": {
"exportToSVG": "Download SVG",
"exportToPNG": "Download PNG",
"exportToCSV": "Download CSV",
"menu": "Menu",
"selection": "Selection",
"selectionZoom": "Selection Zoom",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"pan": "Panning",
"reset": "Reset Zoom"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "es",
"options": {
"months": [
"Enero",
"Febrero",
"Marzo",
"Abril",
"Mayo",
"Junio",
"Julio",
"Agosto",
"Septiembre",
"Octubre",
"Noviembre",
"Diciembre"
],
"shortMonths": [
"Ene",
"Feb",
"Mar",
"Abr",
"May",
"Jun",
"Jul",
"Ago",
"Sep",
"Oct",
"Nov",
"Dic"
],
"days": [
"Domingo",
"Lunes",
"Martes",
"Miércoles",
"Jueves",
"Viernes",
"Sábado"
],
"shortDays": ["Dom", "Lun", "Mar", "Mie", "Jue", "Vie", "Sab"],
"toolbar": {
"exportToSVG": "Descargar SVG",
"exportToPNG": "Descargar PNG",
"exportToCSV": "Descargar CSV",
"menu": "Menu",
"selection": "Seleccionar",
"selectionZoom": "Seleccionar Zoom",
"zoomIn": "Aumentar",
"zoomOut": "Disminuir",
"pan": "Navegación",
"reset": "Reiniciar Zoom"
}
}
}
+63
View File
@@ -0,0 +1,63 @@
{
"name": "et",
"options": {
"months": [
"jaanuar",
"veebruar",
"märts",
"aprill",
"mai",
"juuni",
"juuli",
"august",
"september",
"oktoober",
"november",
"detsember"
],
"shortMonths": [
"jaan",
"veebr",
"märts",
"apr",
"mai",
"juuni",
"juuli",
"aug",
"sept",
"okt",
"nov",
"dets"
],
"days": [
"pühapäev",
"esmaspäev",
"teisipäev",
"kolmapäev",
"neljapäev",
"reede",
"laupäev"
],
"shortDays": [
"P",
"E",
"T",
"K",
"N",
"R",
"L"
],
"toolbar": {
"exportToSVG": "Lae alla SVG",
"exportToPNG": "Lae alla PNG",
"exportToCSV": "Lae alla CSV",
"menu": "Menüü",
"selection": "Valik",
"selectionZoom": "Valiku suum",
"zoomIn": "Suurenda",
"zoomOut": "Vähenda",
"pan": "Panoraamimine",
"reset": "Lähtesta suum"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "fa",
"options": {
"months": [
"فروردین",
"اردیبهشت",
"خرداد",
"تیر",
"مرداد",
"شهریور",
"مهر",
"آبان",
"آذر",
"دی",
"بهمن",
"اسفند"
],
"shortMonths": [
"فرو",
"ارد",
"خرد",
"تیر",
"مرد",
"شهر",
"مهر",
"آبا",
"آذر",
"دی",
"بهمـ",
"اسفـ"
],
"days": [
"یکشنبه",
"دوشنبه",
"سه شنبه",
"چهارشنبه",
"پنجشنبه",
"جمعه",
"شنبه"
],
"shortDays": ["ی", "د", "س", "چ", "پ", "ج", "ش"],
"toolbar": {
"exportToSVG": "دانلود SVG",
"exportToPNG": "دانلود PNG",
"exportToCSV": "دانلود CSV",
"menu": "منو",
"selection": "انتخاب",
"selectionZoom": "بزرگنمایی انتخابی",
"zoomIn": "بزرگنمایی",
"zoomOut": "کوچکنمایی",
"pan": "پیمایش",
"reset": "بازنشانی بزرگنمایی"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "fi",
"options": {
"months": [
"Tammikuu",
"Helmikuu",
"Maaliskuu",
"Huhtikuu",
"Toukokuu",
"Kesäkuu",
"Heinäkuu",
"Elokuu",
"Syyskuu",
"Lokakuu",
"Marraskuu",
"Joulukuu"
],
"shortMonths": [
"Tammi",
"Helmi",
"Maalis",
"Huhti",
"Touko",
"Kesä",
"Heinä",
"Elo",
"Syys",
"Loka",
"Marras",
"Joulu"
],
"days": [
"Sunnuntai",
"Maanantai",
"Tiistai",
"Keskiviikko",
"Torstai",
"Perjantai",
"Lauantai"
],
"shortDays": ["Su", "Ma", "Ti", "Ke", "To", "Pe", "La"],
"toolbar": {
"exportToSVG": "Lataa SVG",
"exportToPNG": "Lataa PNG",
"exportToCSV": "Lataa CSV",
"menu": "Valikko",
"selection": "Valinta",
"selectionZoom": "Valinnan zoomaus",
"zoomIn": "Lähennä",
"zoomOut": "Loitonna",
"pan": "Panoroi",
"reset": "Nollaa zoomaus"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "fr",
"options": {
"months": [
"janvier",
"février",
"mars",
"avril",
"mai",
"juin",
"juillet",
"août",
"septembre",
"octobre",
"novembre",
"décembre"
],
"shortMonths": [
"janv.",
"févr.",
"mars",
"avr.",
"mai",
"juin",
"juill.",
"août",
"sept.",
"oct.",
"nov.",
"déc."
],
"days": [
"dimanche",
"lundi",
"mardi",
"mercredi",
"jeudi",
"vendredi",
"samedi"
],
"shortDays": ["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."],
"toolbar": {
"exportToSVG": "Télécharger au format SVG",
"exportToPNG": "Télécharger au format PNG",
"exportToCSV": "Télécharger au format CSV",
"menu": "Menu",
"selection": "Sélection",
"selectionZoom": "Sélection et zoom",
"zoomIn": "Zoomer",
"zoomOut": "Dézoomer",
"pan": "Navigation",
"reset": "Réinitialiser le zoom"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "he",
"options": {
"months": [
"ינואר",
"פברואר",
"מרץ",
"אפריל",
"מאי",
"יוני",
"יולי",
"אוגוסט",
"ספטמבר",
"אוקטובר",
"נובמבר",
"דצמבר"
],
"shortMonths": [
"ינו׳",
"פבר׳",
"מרץ",
"אפר׳",
"מאי",
"יוני",
"יולי",
"אוג׳",
"ספט׳",
"אוק׳",
"נוב׳",
"דצמ׳"
],
"days": [
"ראשון",
"שני",
"שלישי",
"רביעי",
"חמישי",
"שישי",
"שבת"
],
"shortDays": ["א׳", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳"],
"toolbar": {
"exportToSVG": "הורד SVG",
"exportToPNG": "הורד PNG",
"exportToCSV": "הורד CSV",
"menu": "תפריט",
"selection": "בחירה",
"selectionZoom": "זום בחירה",
"zoomIn": "הגדלה",
"zoomOut": "הקטנה",
"pan": "הזזה",
"reset": "איפוס תצוגה"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "hi",
"options": {
"months": [
"जनवरी",
"फ़रवरी",
"मार्च",
"अप्रैल",
"मई",
"जून",
"जुलाई",
"अगस्त",
"सितंबर",
"अक्टूबर",
"नवंबर",
"दिसंबर"
],
"shortMonths": [
"जनवरी",
"फ़रवरी",
"मार्च",
"अप्रैल",
"मई",
"जून",
"जुलाई",
"अगस्त",
"सितंबर",
"अक्टूबर",
"नवंबर",
"दिसंबर"
],
"days": [
"रविवार",
"सोमवार",
"मंगलवार",
"बुधवार",
"गुरुवार",
"शुक्रवार",
"शनिवार"
],
"shortDays": ["रवि", "सोम", "मंगल", "बुध", "गुरु", "शुक्र", "शनि"],
"toolbar": {
"exportToSVG": "निर्यात SVG",
"exportToPNG": "निर्यात PNG",
"exportToCSV": "निर्यात CSV",
"menu": "सूची",
"selection": "चयन",
"selectionZoom": "ज़ूम करना",
"zoomIn": "ज़ूम इन",
"zoomOut": "ज़ूम आउट",
"pan": "पैनिंग",
"reset": "फिर से कायम करना"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "hr",
"options": {
"months": [
"Siječanj",
"Veljača",
"Ožujak",
"Travanj",
"Svibanj",
"Lipanj",
"Srpanj",
"Kolovoz",
"Rujan",
"Listopad",
"Studeni",
"Prosinac"
],
"shortMonths": [
"Sij",
"Velj",
"Ožu",
"Tra",
"Svi",
"Lip",
"Srp",
"Kol",
"Ruj",
"Lis",
"Stu",
"Pro"
],
"days": [
"Nedjelja",
"Ponedjeljak",
"Utorak",
"Srijeda",
"Četvrtak",
"Petak",
"Subota"
],
"shortDays": ["Ned", "Pon", "Uto", "Sri", "Čet", "Pet", "Sub"],
"toolbar": {
"exportToSVG": "Preuzmi SVG",
"exportToPNG": "Preuzmi PNG",
"exportToCSV": "Preuzmi CSV",
"menu": "Izbornik",
"selection": "Odabir",
"selectionZoom": "Odabirno povećanje",
"zoomIn": "Uvećajte prikaz",
"zoomOut": "Umanjite prikaz",
"pan": "Pomicanje",
"reset": "Povratak na zadani prikaz"
}
}
}
+64
View File
@@ -0,0 +1,64 @@
{
"name": "hu",
"options": {
"months": [
"január",
"február",
"március",
"április",
"május",
"június",
"július",
"augusztus",
"szeptember",
"október",
"november",
"december"
],
"shortMonths": [
"jan",
"feb",
"mar",
"ápr",
"máj",
"jún",
"júl",
"aug",
"szept",
"okt",
"nov",
"dec"
],
"days": [
"hétfő",
"kedd",
"szerda",
"csütörtök",
"péntek",
"szombat",
"vasárnap"
],
"shortDays": [
"H",
"K",
"Sze",
"Cs",
"P",
"Szo",
"V"
],
"toolbar": {
"exportToSVG": "Exportálás SVG-be",
"exportToPNG": "Exportálás PNG-be",
"exportToCSV": "Exportálás CSV-be",
"menu": "Fő ajánlat",
"download": "SVG letöltése",
"selection": "Kiválasztás",
"selectionZoom": "Nagyító kiválasztása",
"zoomIn": "Nagyítás",
"zoomOut": "Kicsinyítés",
"pan": "Képcsúsztatás",
"reset": "Nagyító visszaállítása"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "hy",
"options": {
"months": [
"Հունվար",
"Փետրվար",
"Մարտ",
"Ապրիլ",
"Մայիս",
"Հունիս",
"Հուլիս",
"Օգոստոս",
"Սեպտեմբեր",
"Հոկտեմբեր",
"Նոյեմբեր",
"Դեկտեմբեր"
],
"shortMonths": [
"Հնվ",
"Փտվ",
"Մրտ",
"Ապր",
"Մյս",
"Հնս",
"Հլիս",
"Օգս",
"Սեպ",
"Հոկ",
"Նոյ",
"Դեկ"
],
"days": [
"Կիրակի",
"Երկուշաբթի",
"Երեքշաբթի",
"Չորեքշաբթի",
"Հինգշաբթի",
"Ուրբաթ",
"Շաբաթ"
],
"shortDays": ["Կիր", "Երկ", "Երք", "Չրք", "Հնգ", "Ուրբ", "Շբթ"],
"toolbar": {
"exportToSVG": "Բեռնել SVG",
"exportToPNG": "Բեռնել PNG",
"exportToCSV": "Բեռնել CSV",
"menu": "Մենյու",
"selection": "Ընտրված",
"selectionZoom": "Ընտրված հատվածի խոշորացում",
"zoomIn": "Խոշորացնել",
"zoomOut": "Մանրացնել",
"pan": "Տեղափոխում",
"reset": "Բերել սկզբնական վիճակի"
}
}
}
+47
View File
@@ -0,0 +1,47 @@
{
"name": "id",
"options": {
"months": [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"Mei",
"Jun",
"Jul",
"Agu",
"Sep",
"Okt",
"Nov",
"Des"
],
"days": ["Minggu", "Senin", "Selasa", "Rabu", "kamis", "Jumat", "Sabtu"],
"shortDays": ["Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"],
"toolbar": {
"exportToSVG": "Unduh SVG",
"exportToPNG": "Unduh PNG",
"exportToCSV": "Unduh CSV",
"menu": "Menu",
"selection": "Pilihan",
"selectionZoom": "Perbesar Pilihan",
"zoomIn": "Perbesar",
"zoomOut": "Perkecil",
"pan": "Geser",
"reset": "Atur Ulang Zoom"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "it",
"options": {
"months": [
"Gennaio",
"Febbraio",
"Marzo",
"Aprile",
"Maggio",
"Giugno",
"Luglio",
"Agosto",
"Settembre",
"Ottobre",
"Novembre",
"Dicembre"
],
"shortMonths": [
"Gen",
"Feb",
"Mar",
"Apr",
"Mag",
"Giu",
"Lug",
"Ago",
"Set",
"Ott",
"Nov",
"Dic"
],
"days": [
"Domenica",
"Lunedì",
"Martedì",
"Mercoledì",
"Giovedì",
"Venerdì",
"Sabato"
],
"shortDays": ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"],
"toolbar": {
"exportToSVG": "Scarica SVG",
"exportToPNG": "Scarica PNG",
"exportToCSV": "Scarica CSV",
"menu": "Menu",
"selection": "Selezione",
"selectionZoom": "Seleziona Zoom",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"pan": "Sposta",
"reset": "Reimposta Zoom"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "ja",
"options": {
"months": [
"1月",
"2月",
"3月",
"4月",
"5月",
"6月",
"7月",
"8月",
"9月",
"10月",
"11月",
"12月"
],
"shortMonths": [
"1月",
"2月",
"3月",
"4月",
"5月",
"6月",
"7月",
"8月",
"9月",
"10月",
"11月",
"12月"
],
"days": [
"日曜日",
"月曜日",
"火曜日",
"水曜日",
"木曜日",
"金曜日",
"土曜日"
],
"shortDays": ["日", "月", "火", "水", "木", "金", "土"],
"toolbar": {
"exportToSVG": "SVGダウンロード",
"exportToPNG": "PNGダウンロード",
"exportToCSV": "CSVダウンロード",
"menu": "メニュー",
"selection": "選択",
"selectionZoom": "選択ズーム",
"zoomIn": "拡大",
"zoomOut": "縮小",
"pan": "パン",
"reset": "ズームリセット"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "ka",
"options": {
"months": [
"იანვარი",
"თებერვალი",
"მარტი",
"აპრილი",
"მაისი",
"ივნისი",
"ივლისი",
"აგვისტო",
"სექტემბერი",
"ოქტომბერი",
"ნოემბერი",
"დეკემბერი"
],
"shortMonths": [
"იან",
"თებ",
"მარ",
"აპრ",
"მაი",
"ივნ",
"ივლ",
"აგვ",
"სექ",
"ოქტ",
"ნოე",
"დეკ"
],
"days": [
"კვირა",
"ორშაბათი",
"სამშაბათი",
"ოთხშაბათი",
"ხუთშაბათი",
"პარასკევი",
"შაბათი"
],
"shortDays": ["კვი", "ორშ", "სამ", "ოთხ", "ხუთ", "პარ", "შაბ"],
"toolbar": {
"exportToSVG": "გადმოქაჩე SVG",
"exportToPNG": "გადმოქაჩე PNG",
"exportToCSV": "გადმოქაჩე CSV",
"menu": "მენიუ",
"selection": "არჩევა",
"selectionZoom": "არჩეულის გადიდება",
"zoomIn": "გადიდება",
"zoomOut": "დაპატარაება",
"pan": "გადაჩოჩება",
"reset": "გადიდების გაუქმება"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "ko",
"options": {
"months": [
"1월",
"2월",
"3월",
"4월",
"5월",
"6월",
"7월",
"8월",
"9월",
"10월",
"11월",
"12월"
],
"shortMonths": [
"1월",
"2월",
"3월",
"4월",
"5월",
"6월",
"7월",
"8월",
"9월",
"10월",
"11월",
"12월"
],
"days": [
"일요일",
"월요일",
"화요일",
"수요일",
"목요일",
"금요일",
"토요일"
],
"shortDays": ["일", "월", "화", "수", "목", "금", "토"],
"toolbar": {
"exportToSVG": "SVG 다운로드",
"exportToPNG": "PNG 다운로드",
"exportToCSV": "CSV 다운로드",
"menu": "메뉴",
"selection": "선택",
"selectionZoom": "선택영역 확대",
"zoomIn": "확대",
"zoomOut": "축소",
"pan": "패닝",
"reset": "원래대로"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "lt",
"options": {
"months": [
"Sausis",
"Vasaris",
"Kovas",
"Balandis",
"Gegužė",
"Birželis",
"Liepa",
"Rugpjūtis",
"Rugsėjis",
"Spalis",
"Lapkritis",
"Gruodis"
],
"shortMonths": [
"Sau",
"Vas",
"Kov",
"Bal",
"Geg",
"Bir",
"Lie",
"Rgp",
"Rgs",
"Spl",
"Lap",
"Grd"
],
"days": [
"Sekmadienis",
"Pirmadienis",
"Antradienis",
"Trečiadienis",
"Ketvirtadienis",
"Penktadienis",
"Šeštadienis"
],
"shortDays": ["Sk", "Per", "An", "Tr", "Kt", "Pn", "Št"],
"toolbar": {
"exportToSVG": "Atsisiųsti SVG",
"exportToPNG": "Atsisiųsti PNG",
"exportToCSV": "Atsisiųsti CSV",
"menu": "Menu",
"selection": "Pasirinkimas",
"selectionZoom": "Zoom: Pasirinkimas",
"zoomIn": "Zoom: Priartinti",
"zoomOut": "Zoom: Atitolinti",
"pan": "Perkėlimas",
"reset": "Atstatyti"
}
}
}
+64
View File
@@ -0,0 +1,64 @@
{
"name": "lv",
"options": {
"months": [
"janvāris",
"februāris",
"marts",
"aprīlis",
"maijs",
"jūnijs",
"jūlijs",
"augusts",
"septembris",
"oktobris",
"novembris",
"decembris"
],
"shortMonths": [
"janv",
"febr",
"marts",
"apr",
"maijs",
"jūn",
"jūl",
"aug",
"sept",
"okt",
"nov",
"dec"
],
"days": [
"svētdiena",
"pirmdiena",
"otrdiena",
"trešdiena",
"ceturtdiena",
"piektdiena",
"sestdiena"
],
"shortDays": [
"Sv",
"P",
"O",
"T",
"C",
"P",
"S"
],
"toolbar": {
"exportToSVG": "Lejuplādēt SVG",
"exportToPNG": "Lejuplādēt PNG",
"exportToCSV": "Lejuplādēt CSV",
"menu": "Izvēlne",
"selection": "Atlase",
"selectionZoom": "Pietuvināt atlasi",
"zoomIn": "Pietuvināt",
"zoomOut": "Attālināt",
"pan": "Pārvietoties diagrammā",
"reset": "Atiestatīt pietuvinājumu"
}
}
}
+63
View File
@@ -0,0 +1,63 @@
{
"name": "ms",
"options": {
"months": [
"Januari",
"Februari",
"Mac",
"April",
"Mei",
"Jun",
"Julai",
"Ogos",
"September",
"Oktober",
"November",
"Disember"
],
"shortMonths": [
"Jan",
"Feb",
"Mac",
"Apr",
"Mei",
"Jun",
"Jul",
"Ogos",
"Sep",
"Okt",
"Nov",
"Dis"
],
"days": [
"Ahad",
"Isnin",
"Selasa",
"Rabu",
"Khamis",
"Jumaat",
"Sabtu"
],
"shortDays": [
"Ahd",
"Isn",
"Sel",
"Rab",
"Kha",
"Jum",
"Sab"
],
"toolbar": {
"exportToSVG": "Muat turun SVG",
"exportToPNG": "Muat turun PNG",
"exportToCSV": "Muat turun CSV",
"menu": "Menu",
"selection": "Pilihan",
"selectionZoom": "Zum Pilihan",
"zoomIn": "Zoom Masuk",
"zoomOut": "Zoom Keluar",
"pan": "Pemusingan",
"reset": "Tetapkan Semula Zum"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "nb",
"options": {
"months": [
"Januar",
"Februar",
"Mars",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Desember"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"Mai",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Des"
],
"days": [
"Søndag",
"Mandag",
"Tirsdag",
"Onsdag",
"Torsdag",
"Fredag",
"Lørdag"
],
"shortDays": ["Sø", "Ma", "Ti", "On", "To", "Fr", "Lø"],
"toolbar": {
"exportToSVG": "Last ned SVG",
"exportToPNG": "Last ned PNG",
"exportToCSV": "Last ned CSV",
"menu": "Menu",
"selection": "Velg",
"selectionZoom": "Zoom: Velg",
"zoomIn": "Zoome inn",
"zoomOut": "Zoome ut",
"pan": "Skyving",
"reset": "Start på nytt"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "nl",
"options": {
"months": [
"Januari",
"Februari",
"Maart",
"April",
"Mei",
"Juni",
"Juli",
"Augustus",
"September",
"Oktober",
"November",
"December"
],
"shortMonths": [
"Jan",
"Feb",
"Mrt",
"Apr",
"Mei",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dec"
],
"days": [
"Zondag",
"Maandag",
"Dinsdag",
"Woensdag",
"Donderdag",
"Vrijdag",
"Zaterdag"
],
"shortDays": ["Zo", "Ma", "Di", "Wo", "Do", "Vr", "Za"],
"toolbar": {
"exportToSVG": "Download SVG",
"exportToPNG": "Download PNG",
"exportToCSV": "Download CSV",
"menu": "Menu",
"selection": "Selectie",
"selectionZoom": "Zoom selectie",
"zoomIn": "Zoom in",
"zoomOut": "Zoom out",
"pan": "Verplaatsen",
"reset": "Standaardwaarden"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "pl",
"options": {
"months": [
"Styczeń",
"Luty",
"Marzec",
"Kwiecień",
"Maj",
"Czerwiec",
"Lipiec",
"Sierpień",
"Wrzesień",
"Październik",
"Listopad",
"Grudzień"
],
"shortMonths": [
"Sty",
"Lut",
"Mar",
"Kwi",
"Maj",
"Cze",
"Lip",
"Sie",
"Wrz",
"Paź",
"Lis",
"Gru"
],
"days": [
"Niedziela",
"Poniedziałek",
"Wtorek",
"Środa",
"Czwartek",
"Piątek",
"Sobota"
],
"shortDays": ["Nd", "Pn", "Wt", "Śr", "Cz", "Pt", "Sb"],
"toolbar": {
"exportToSVG": "Pobierz SVG",
"exportToPNG": "Pobierz PNG",
"exportToCSV": "Pobierz CSV",
"menu": "Menu",
"selection": "Wybieranie",
"selectionZoom": "Zoom: Wybieranie",
"zoomIn": "Zoom: Przybliż",
"zoomOut": "Zoom: Oddal",
"pan": "Przesuwanie",
"reset": "Resetuj"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "pt-br",
"options": {
"months": [
"Janeiro",
"Fevereiro",
"Março",
"Abril",
"Maio",
"Junho",
"Julho",
"Agosto",
"Setembro",
"Outubro",
"Novembro",
"Dezembro"
],
"shortMonths": [
"Jan",
"Fev",
"Mar",
"Abr",
"Mai",
"Jun",
"Jul",
"Ago",
"Set",
"Out",
"Nov",
"Dez"
],
"days": [
"Domingo",
"Segunda",
"Terça",
"Quarta",
"Quinta",
"Sexta",
"Sábado"
],
"shortDays": ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab"],
"toolbar": {
"exportToSVG": "Baixar SVG",
"exportToPNG": "Baixar PNG",
"exportToCSV": "Baixar CSV",
"menu": "Menu",
"selection": "Selecionar",
"selectionZoom": "Selecionar Zoom",
"zoomIn": "Aumentar",
"zoomOut": "Diminuir",
"pan": "Navegação",
"reset": "Reiniciar Zoom"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "pt",
"options": {
"months": [
"Janeiro",
"Fevereiro",
"Março",
"Abril",
"Maio",
"Junho",
"Julho",
"Agosto",
"Setembro",
"Outubro",
"Novembro",
"Dezembro"
],
"shortMonths": [
"Jan",
"Fev",
"Mar",
"Abr",
"Mai",
"Jun",
"Jul",
"Ag",
"Set",
"Out",
"Nov",
"Dez"
],
"days": [
"Domingo",
"Segunda-feira",
"Terça-feira",
"Quarta-feira",
"Quinta-feira",
"Sexta-feira",
"Sábado"
],
"shortDays": ["Do", "Se", "Te", "Qa", "Qi", "Sx", "Sa"],
"toolbar": {
"exportToSVG": "Transferir SVG",
"exportToPNG": "Transferir PNG",
"exportToCSV": "Transferir CSV",
"menu": "Menu",
"selection": "Selecionar",
"selectionZoom": "Zoom: Selecionar",
"zoomIn": "Zoom: Aumentar",
"zoomOut": "Zoom: Diminuir",
"pan": "Deslocamento",
"reset": "Redefinir"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "rs",
"options": {
"months": [
"Januar",
"Februar",
"Mart",
"April",
"Maj",
"Jun",
"Jul",
"Avgust",
"Septembar",
"Oktobar",
"Novembar",
"Decembar"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"Maj",
"Jun",
"Jul",
"Avg",
"Sep",
"Okt",
"Nov",
"Dec"
],
"days": [
"Nedelja",
"Ponedeljak",
"Utorak",
"Sreda",
"Četvrtak",
"Petak",
"Subota"
],
"shortDays": ["Ned", "Pon", "Uto", "Sre", "Čet", "Pet", "Sub"],
"toolbar": {
"exportToSVG": "Preuzmi SVG",
"exportToPNG": "Preuzmi PNG",
"exportToCSV": "Preuzmi CSV",
"menu": "Meni",
"selection": "Odabir",
"selectionZoom": "Odabirno povećanje",
"zoomIn": "Uvećajte prikaz",
"zoomOut": "Umanjite prikaz",
"pan": "Pomeranje",
"reset": "Resetuj prikaz"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "ru",
"options": {
"months": [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь"
],
"shortMonths": [
"Янв",
"Фев",
"Мар",
"Апр",
"Май",
"Июн",
"Июл",
"Авг",
"Сен",
"Окт",
"Ноя",
"Дек"
],
"days": [
"Воскресенье",
"Понедельник",
"Вторник",
"Среда",
"Четверг",
"Пятница",
"Суббота"
],
"shortDays": ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"],
"toolbar": {
"exportToSVG": "Сохранить SVG",
"exportToPNG": "Сохранить PNG",
"exportToCSV": "Сохранить CSV",
"menu": "Меню",
"selection": "Выбор",
"selectionZoom": "Выбор с увеличением",
"zoomIn": "Увеличить",
"zoomOut": "Уменьшить",
"pan": "Перемещение",
"reset": "Сбросить увеличение"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "se",
"options": {
"months": [
"Januari",
"Februari",
"Mars",
"April",
"Maj",
"Juni",
"Juli",
"Augusti",
"September",
"Oktober",
"November",
"December"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"Maj",
"Juni",
"Juli",
"Aug",
"Sep",
"Okt",
"Nov",
"Dec"
],
"days": [
"Söndag",
"Måndag",
"Tisdag",
"Onsdag",
"Torsdag",
"Fredag",
"Lördag"
],
"shortDays": ["Sön", "Mån", "Tis", "Ons", "Tor", "Fre", "Lör"],
"toolbar": {
"exportToSVG": "Ladda SVG",
"exportToPNG": "Ladda PNG",
"exportToCSV": "Ladda CSV",
"menu": "Meny",
"selection": "Selektion",
"selectionZoom": "Val av zoom",
"zoomIn": "Zooma in",
"zoomOut": "Zooma ut",
"pan": "Panorering",
"reset": "Återställ zoomning"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "sk",
"options": {
"months": [
"Január",
"Február",
"Marec",
"Apríl",
"Máj",
"Jún",
"Júl",
"August",
"September",
"Október",
"November",
"December"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"Máj",
"Jún",
"Júl",
"Aug",
"Sep",
"Okt",
"Nov",
"Dec"
],
"days": [
"Nedeľa",
"Pondelok",
"Utorok",
"Streda",
"Štvrtok",
"Piatok",
"Sobota"
],
"shortDays": ["Ne", "Po", "Ut", "St", "Št", "Pi", "So"],
"toolbar": {
"exportToSVG": "Stiahnuť SVG",
"exportToPNG": "Stiahnuť PNG",
"exportToCSV": "Stiahnuť CSV",
"menu": "Menu",
"selection": "Vyberanie",
"selectionZoom": "Zoom: Vyberanie",
"zoomIn": "Zoom: Priblížiť",
"zoomOut": "Zoom: Vzdialiť",
"pan": "Presúvanie",
"reset": "Resetovať"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "sl",
"options": {
"months": [
"Januar",
"Februar",
"Marec",
"April",
"Maj",
"Junij",
"Julij",
"Avgust",
"Septemer",
"Oktober",
"November",
"December"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"Maj",
"Jun",
"Jul",
"Avg",
"Sep",
"Okt",
"Nov",
"Dec"
],
"days": [
"Nedelja",
"Ponedeljek",
"Torek",
"Sreda",
"Četrtek",
"Petek",
"Sobota"
],
"shortDays": ["Ne", "Po", "To", "Sr", "Če", "Pe", "So"],
"toolbar": {
"exportToSVG": "Prenesi SVG",
"exportToPNG": "Prenesi PNG",
"exportToCSV": "Prenesi CSV",
"menu": "Menu",
"selection": "Izbiranje",
"selectionZoom": "Zoom: Izbira",
"zoomIn": "Zoom: Približaj",
"zoomOut": "Zoom: Oddalji",
"pan": "Pomikanje",
"reset": "Resetiraj"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "sq",
"options": {
"months": [
"Janar",
"Shkurt",
"Mars",
"Prill",
"Maj",
"Qershor",
"Korrik",
"Gusht",
"Shtator",
"Tetor",
"Nëntor",
"Dhjetor"
],
"shortMonths": [
"Jan",
"Shk",
"Mar",
"Pr",
"Maj",
"Qer",
"Korr",
"Gush",
"Sht",
"Tet",
"Nën",
"Dhj"
],
"days": [
"e Dielë",
"e Hënë",
"e Martë",
"e Mërkurë",
"e Enjte",
"e Premte",
"e Shtunë"
],
"shortDays": ["Die", "Hën", "Mar", "Mër", "Enj", "Pre", "Sht"],
"toolbar": {
"exportToSVG": "Shkarko SVG",
"exportToPNG": "Shkarko PNG",
"exportToCSV": "Shkarko CSV",
"menu": "Menu",
"selection": "Seleksiono",
"selectionZoom": "Seleksiono Zmadhim",
"zoomIn": "Zmadho",
"zoomOut": "Zvogëlo",
"pan": "Spostoje",
"reset": "Rikthe dimensionin"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "th",
"options": {
"months": [
"มกราคม",
"กุมภาพันธ์",
"มีนาคม",
"เมษายน",
"พฤษภาคม",
"มิถุนายน",
"กรกฎาคม",
"สิงหาคม",
"กันยายน",
"ตุลาคม",
"พฤศจิกายน",
"ธันวาคม"
],
"shortMonths": [
"ม.ค.",
"ก.พ.",
"มี.ค.",
"เม.ย.",
"พ.ค.",
"มิ.ย.",
"ก.ค.",
"ส.ค.",
"ก.ย.",
"ต.ค.",
"พ.ย.",
"ธ.ค."
],
"days": [
"อาทิตย์",
"จันทร์",
"อังคาร",
"พุธ",
"พฤหัสบดี",
"ศุกร์",
"เสาร์"
],
"shortDays": ["อา", "จ", "อ", "พ", "พฤ", "ศ", "ส"],
"toolbar": {
"exportToSVG": "ดาวน์โหลด SVG",
"exportToPNG": "ดาวน์โหลด PNG",
"exportToCSV": "ดาวน์โหลด CSV",
"menu": "เมนู",
"selection": "เลือก",
"selectionZoom": "เลือกจุดที่จะซูม",
"zoomIn": "ซูมเข้า",
"zoomOut": "ซูมออก",
"pan": "ปรากฎว่า",
"reset": "รีเซ็ตการซูม"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "tr",
"options": {
"months": [
"Ocak",
"Şubat",
"Mart",
"Nisan",
"Mayıs",
"Haziran",
"Temmuz",
"Ağustos",
"Eylül",
"Ekim",
"Kasım",
"Aralık"
],
"shortMonths": [
"Oca",
"Şub",
"Mar",
"Nis",
"May",
"Haz",
"Tem",
"Ağu",
"Eyl",
"Eki",
"Kas",
"Ara"
],
"days": [
"Pazar",
"Pazartesi",
"Salı",
"Çarşamba",
"Perşembe",
"Cuma",
"Cumartesi"
],
"shortDays": ["Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"],
"toolbar": {
"exportToSVG": "SVG İndir",
"exportToPNG": "PNG İndir",
"exportToCSV": "CSV İndir",
"menu": "Menü",
"selection": "Seçim",
"selectionZoom": "Seçim Yakınlaştır",
"zoomIn": "Yakınlaştır",
"zoomOut": "Uzaklaştır",
"pan": "Kaydır",
"reset": "Yakınlaştırmayı Sıfırla"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "ua",
"options": {
"months": [
"Січень",
"Лютий",
"Березень",
"Квітень",
"Травень",
"Червень",
"Липень",
"Серпень",
"Вересень",
"Жовтень",
"Листопад",
"Грудень"
],
"shortMonths": [
"Січ",
"Лют",
"Бер",
"Кві",
"Тра",
"Чер",
"Лип",
"Сер",
"Вер",
"Жов",
"Лис",
"Гру"
],
"days": [
"Неділя",
"Понеділок",
"Вівторок",
"Середа",
"Четвер",
"П'ятниця",
"Субота"
],
"shortDays": ["Нд", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"],
"toolbar": {
"exportToSVG": "Зберегти SVG",
"exportToPNG": "Зберегти PNG",
"exportToCSV": "Зберегти CSV",
"menu": "Меню",
"selection": "Вибір",
"selectionZoom": "Вибір із збільшенням",
"zoomIn": "Збільшити",
"zoomOut": "Зменшити",
"pan": "Переміщення",
"reset": "Скинути збільшення"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "zh-cn",
"options": {
"months": [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月"
],
"shortMonths": [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月"
],
"days": [
"星期天",
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六"
],
"shortDays": ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
"toolbar": {
"exportToSVG": "下载 SVG",
"exportToPNG": "下载 PNG",
"exportToCSV": "下载 CSV",
"menu": "菜单",
"selection": "选择",
"selectionZoom": "选择缩放",
"zoomIn": "放大",
"zoomOut": "缩小",
"pan": "平移",
"reset": "重置缩放"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "zh-tw",
"options": {
"months": [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月"
],
"shortMonths": [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月"
],
"days": [
"星期日",
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六"
],
"shortDays": ["週日", "週一", "週二", "週三", "週四", "週五", "週六"],
"toolbar": {
"exportToSVG": "下載 SVG",
"exportToPNG": "下載 PNG",
"exportToCSV": "下載 CSV",
"menu": "菜單",
"selection": "選擇",
"selectionZoom": "選擇縮放",
"zoomIn": "放大",
"zoomOut": "縮小",
"pan": "平移",
"reset": "重置縮放"
}
}
}
+260
View File
@@ -0,0 +1,260 @@
import Utils from '../utils/Utils'
/**
* ApexCharts Animation Class.
*
* @module Animations
**/
export default class Animations {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.setEasingFunctions()
}
setEasingFunctions() {
let easing
if (this.w.globals.easing) return
const userDefinedEasing = this.w.config.chart.animations.easing
switch (userDefinedEasing) {
case 'linear': {
easing = '-'
break
}
case 'easein': {
easing = '<'
break
}
case 'easeout': {
easing = '>'
break
}
case 'easeinout': {
easing = '<>'
break
}
case 'swing': {
easing = (pos) => {
let s = 1.70158
let ret = (pos -= 1) * pos * ((s + 1) * pos + s) + 1
return ret
}
break
}
case 'bounce': {
easing = (pos) => {
let ret = ''
if (pos < 1 / 2.75) {
ret = 7.5625 * pos * pos
} else if (pos < 2 / 2.75) {
ret = 7.5625 * (pos -= 1.5 / 2.75) * pos + 0.75
} else if (pos < 2.5 / 2.75) {
ret = 7.5625 * (pos -= 2.25 / 2.75) * pos + 0.9375
} else {
ret = 7.5625 * (pos -= 2.625 / 2.75) * pos + 0.984375
}
return ret
}
break
}
case 'elastic': {
easing = (pos) => {
if (pos === !!pos) return pos
return (
Math.pow(2, -10 * pos) *
Math.sin(((pos - 0.075) * (2 * Math.PI)) / 0.3) +
1
)
}
break
}
default: {
easing = '<>'
}
}
this.w.globals.easing = easing
}
animateLine(el, from, to, speed) {
el.attr(from).animate(speed).attr(to)
}
/*
** Animate radius of a circle element
*/
animateMarker(el, from, to, speed, easing, cb) {
if (!from) from = 0
el.attr({
r: from,
width: from,
height: from,
})
.animate(speed, easing)
.attr({
r: to,
width: to.width,
height: to.height,
})
.afterAll(() => {
cb()
})
}
/*
** Animate radius and position of a circle element
*/
animateCircle(el, from, to, speed, easing) {
el.attr({
r: from.r,
cx: from.cx,
cy: from.cy,
})
.animate(speed, easing)
.attr({
r: to.r,
cx: to.cx,
cy: to.cy,
})
}
/*
** Animate rect properties
*/
animateRect(el, from, to, speed, fn) {
el.attr(from)
.animate(speed)
.attr(to)
.afterAll(() => fn())
}
animatePathsGradually(params) {
let { el, realIndex, j, fill, pathFrom, pathTo, speed, delay } = params
let me = this
let w = this.w
let delayFactor = 0
if (w.config.chart.animations.animateGradually.enabled) {
delayFactor = w.config.chart.animations.animateGradually.delay
}
if (
w.config.chart.animations.dynamicAnimation.enabled &&
w.globals.dataChanged &&
w.config.chart.type !== 'bar'
) {
// disabled due to this bug - https://github.com/apexcharts/vue-apexcharts/issues/75
delayFactor = 0
}
me.morphSVG(
el,
realIndex,
j,
w.config.chart.type === 'line' && !w.globals.comboCharts
? 'stroke'
: fill,
pathFrom,
pathTo,
speed,
delay * delayFactor
)
}
showDelayedElements() {
this.w.globals.delayedElements.forEach((d) => {
const ele = d.el
ele.classList.remove('apexcharts-element-hidden')
ele.classList.add('apexcharts-hidden-element-shown')
})
}
animationCompleted(el) {
const w = this.w
if (w.globals.animationEnded) return
w.globals.animationEnded = true
this.showDelayedElements()
if (typeof w.config.chart.events.animationEnd === 'function') {
w.config.chart.events.animationEnd(this.ctx, { el, w })
}
}
// SVG.js animation for morphing one path to another
morphSVG(el, realIndex, j, fill, pathFrom, pathTo, speed, delay) {
let w = this.w
if (!pathFrom) {
pathFrom = el.attr('pathFrom')
}
if (!pathTo) {
pathTo = el.attr('pathTo')
}
const disableAnimationForCorrupPath = (path) => {
if (w.config.chart.type === 'radar') {
// radar chart drops the path to bottom and hence a corrup path looks ugly
// therefore, disable animation for such a case
speed = 1
}
return `M 0 ${w.globals.gridHeight}`
}
if (
!pathFrom ||
pathFrom.indexOf('undefined') > -1 ||
pathFrom.indexOf('NaN') > -1
) {
pathFrom = disableAnimationForCorrupPath()
}
if (
!pathTo ||
pathTo.indexOf('undefined') > -1 ||
pathTo.indexOf('NaN') > -1
) {
pathTo = disableAnimationForCorrupPath()
}
if (!w.globals.shouldAnimate) {
speed = 1
}
el.plot(pathFrom)
.animate(1, w.globals.easing, delay)
.plot(pathFrom)
.animate(speed, w.globals.easing, delay)
.plot(pathTo)
.afterAll(() => {
// a flag to indicate that the original mount function can return true now as animation finished here
if (Utils.isNumber(j)) {
if (
j === w.globals.series[w.globals.maxValsInArrayIndex].length - 2 &&
w.globals.shouldAnimate
) {
this.animationCompleted(el)
}
} else if (fill !== 'none' && w.globals.shouldAnimate) {
if (
(!w.globals.comboCharts &&
realIndex === w.globals.series.length - 1) ||
w.globals.comboCharts
) {
this.animationCompleted(el)
}
}
this.showDelayedElements()
})
}
}
+25
View File
@@ -0,0 +1,25 @@
import Config from './settings/Config'
import Globals from './settings/Globals'
/**
* ApexCharts Base Class for extending user options with pre-defined ApexCharts config.
*
* @module Base
**/
export default class Base {
constructor(opts) {
this.opts = opts
}
init() {
const config = new Config(this.opts).init({ responsiveOverride: false })
const globals = new Globals().init(config)
const w = {
config,
globals
}
return w
}
}
+655
View File
@@ -0,0 +1,655 @@
import Bar from '../charts/Bar'
import BarStacked from '../charts/BarStacked'
import BoxCandleStick from '../charts/BoxCandleStick'
import CoreUtils from './CoreUtils'
import Crosshairs from './Crosshairs'
import HeatMap from '../charts/HeatMap'
import Globals from '../modules/settings/Globals'
import Pie from '../charts/Pie'
import Radar from '../charts/Radar'
import Radial from '../charts/Radial'
import RangeBar from '../charts/RangeBar'
import Legend from './legend/Legend'
import Line from '../charts/Line'
import Treemap from '../charts/Treemap'
import Graphics from './Graphics'
import Range from './Range'
import Utils from '../utils/Utils'
import Scales from './Scales'
import TimeScale from './TimeScale'
/**
* ApexCharts Core Class responsible for major calculations and creating elements.
*
* @module Core
**/
export default class Core {
constructor(el, ctx) {
this.ctx = ctx
this.w = ctx.w
this.el = el
}
// get data and store into appropriate vars
setupElements() {
let gl = this.w.globals
let cnf = this.w.config
// const graphics = new Graphics(this.ctx)
let ct = cnf.chart.type
let axisChartsArrTypes = [
'line',
'area',
'bar',
'rangeBar',
'rangeArea',
'candlestick',
'boxPlot',
'scatter',
'bubble',
'radar',
'heatmap',
'treemap',
]
let xyChartsArrTypes = [
'line',
'area',
'bar',
'rangeBar',
'rangeArea',
'candlestick',
'boxPlot',
'scatter',
'bubble',
]
gl.axisCharts = axisChartsArrTypes.indexOf(ct) > -1
gl.xyCharts = xyChartsArrTypes.indexOf(ct) > -1
gl.isBarHorizontal =
(cnf.chart.type === 'bar' ||
cnf.chart.type === 'rangeBar' ||
cnf.chart.type === 'boxPlot') &&
cnf.plotOptions.bar.horizontal
gl.chartClass = '.apexcharts' + gl.chartID
gl.dom.baseEl = this.el
gl.dom.elWrap = document.createElement('div')
Graphics.setAttrs(gl.dom.elWrap, {
id: gl.chartClass.substring(1),
class: 'apexcharts-canvas ' + gl.chartClass.substring(1),
})
this.el.appendChild(gl.dom.elWrap)
gl.dom.Paper = new window.SVG.Doc(gl.dom.elWrap)
gl.dom.Paper.attr({
class: 'apexcharts-svg',
'xmlns:data': 'ApexChartsNS',
transform: `translate(${cnf.chart.offsetX}, ${cnf.chart.offsetY})`,
})
gl.dom.Paper.node.style.background =
cnf.theme.mode === 'dark' && !cnf.chart.background
? 'rgba(0, 0, 0, 0.8)'
: cnf.chart.background
this.setSVGDimensions()
// append foreignElement (legend's parent)
// legend is kept in foreignElement to be included while exporting
// removing foreignElement and creating legend through HTML will not render legend in export
gl.dom.elLegendForeign = document.createElementNS(gl.SVGNS, 'foreignObject')
Graphics.setAttrs(gl.dom.elLegendForeign, {
x: 0,
y: 0,
width: gl.svgWidth,
height: gl.svgHeight,
})
gl.dom.elLegendWrap = document.createElement('div')
gl.dom.elLegendWrap.classList.add('apexcharts-legend')
gl.dom.elLegendWrap.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
gl.dom.elLegendForeign.appendChild(gl.dom.elLegendWrap)
gl.dom.Paper.node.appendChild(gl.dom.elLegendForeign)
// the elGraphical is the parent of all primary visuals
gl.dom.elGraphical = gl.dom.Paper.group().attr({
class: 'apexcharts-inner apexcharts-graphical',
})
gl.dom.elDefs = gl.dom.Paper.defs()
gl.dom.Paper.add(gl.dom.elGraphical)
gl.dom.elGraphical.add(gl.dom.elDefs)
}
plotChartType(ser, xyRatios) {
const w = this.w
const cnf = w.config
const gl = w.globals
let lineSeries = {
series: [],
i: [],
}
let areaSeries = {
series: [],
i: [],
}
let scatterSeries = {
series: [],
i: [],
}
let bubbleSeries = {
series: [],
i: [],
}
let columnSeries = {
series: [],
i: [],
}
let candlestickSeries = {
series: [],
i: [],
}
let boxplotSeries = {
series: [],
i: [],
}
let rangeBarSeries = {
series: [],
i: [],
}
let rangeAreaSeries = {
series: [],
seriesRangeEnd: [],
i: [],
}
gl.series.map((serie, st) => {
let comboCount = 0
// if user has specified a particular type for particular series
if (typeof ser[st].type !== 'undefined') {
if (ser[st].type === 'column' || ser[st].type === 'bar') {
if (gl.series.length > 1 && cnf.plotOptions.bar.horizontal) {
// horizontal bars not supported in mixed charts, hence show a warning
console.warn(
'Horizontal bars are not supported in a mixed/combo chart. Please turn off `plotOptions.bar.horizontal`'
)
}
columnSeries.series.push(serie)
columnSeries.i.push(st)
comboCount++
w.globals.columnSeries = columnSeries.series
} else if (ser[st].type === 'area') {
areaSeries.series.push(serie)
areaSeries.i.push(st)
comboCount++
} else if (ser[st].type === 'line') {
lineSeries.series.push(serie)
lineSeries.i.push(st)
comboCount++
} else if (ser[st].type === 'scatter') {
scatterSeries.series.push(serie)
scatterSeries.i.push(st)
} else if (ser[st].type === 'bubble') {
bubbleSeries.series.push(serie)
bubbleSeries.i.push(st)
comboCount++
} else if (ser[st].type === 'candlestick') {
candlestickSeries.series.push(serie)
candlestickSeries.i.push(st)
comboCount++
} else if (ser[st].type === 'boxPlot') {
boxplotSeries.series.push(serie)
boxplotSeries.i.push(st)
comboCount++
} else if (ser[st].type === 'rangeBar') {
rangeBarSeries.series.push(serie)
rangeBarSeries.i.push(st)
comboCount++
} else if (ser[st].type === 'rangeArea') {
rangeAreaSeries.series.push(gl.seriesRangeStart[st])
rangeAreaSeries.seriesRangeEnd.push(gl.seriesRangeEnd[st])
rangeAreaSeries.i.push(st)
comboCount++
} else {
// user has specified type, but it is not valid (other than line/area/column)
console.warn(
'You have specified an unrecognized chart type. Available types for this property are line/area/column/bar/scatter/bubble/candlestick/boxPlot/rangeBar/rangeArea'
)
}
if (comboCount > 1) {
gl.comboCharts = true
}
} else {
lineSeries.series.push(serie)
lineSeries.i.push(st)
}
})
let line = new Line(this.ctx, xyRatios)
let boxCandlestick = new BoxCandleStick(this.ctx, xyRatios)
this.ctx.pie = new Pie(this.ctx)
let radialBar = new Radial(this.ctx)
this.ctx.rangeBar = new RangeBar(this.ctx, xyRatios)
let radar = new Radar(this.ctx)
let elGraph = []
if (gl.comboCharts) {
if (areaSeries.series.length > 0) {
elGraph.push(line.draw(areaSeries.series, 'area', areaSeries.i))
}
if (columnSeries.series.length > 0) {
if (w.config.chart.stacked) {
let barStacked = new BarStacked(this.ctx, xyRatios)
elGraph.push(barStacked.draw(columnSeries.series, columnSeries.i))
} else {
this.ctx.bar = new Bar(this.ctx, xyRatios)
elGraph.push(this.ctx.bar.draw(columnSeries.series, columnSeries.i))
}
}
if (rangeAreaSeries.series.length > 0) {
elGraph.push(
line.draw(
rangeAreaSeries.series,
'rangeArea',
rangeAreaSeries.i,
rangeAreaSeries.seriesRangeEnd
)
)
}
if (lineSeries.series.length > 0) {
elGraph.push(line.draw(lineSeries.series, 'line', lineSeries.i))
}
if (candlestickSeries.series.length > 0) {
elGraph.push(
boxCandlestick.draw(
candlestickSeries.series,
'candlestick',
candlestickSeries.i
)
)
}
if (boxplotSeries.series.length > 0) {
elGraph.push(
boxCandlestick.draw(boxplotSeries.series, 'boxPlot', boxplotSeries.i)
)
}
if (rangeBarSeries.series.length > 0) {
elGraph.push(
this.ctx.rangeBar.draw(rangeBarSeries.series, rangeBarSeries.i)
)
}
if (scatterSeries.series.length > 0) {
const scatterLine = new Line(this.ctx, xyRatios, true)
elGraph.push(
scatterLine.draw(scatterSeries.series, 'scatter', scatterSeries.i)
)
}
if (bubbleSeries.series.length > 0) {
const bubbleLine = new Line(this.ctx, xyRatios, true)
elGraph.push(
bubbleLine.draw(bubbleSeries.series, 'bubble', bubbleSeries.i)
)
}
} else {
switch (cnf.chart.type) {
case 'line':
elGraph = line.draw(gl.series, 'line')
break
case 'area':
elGraph = line.draw(gl.series, 'area')
break
case 'bar':
if (cnf.chart.stacked) {
let barStacked = new BarStacked(this.ctx, xyRatios)
elGraph = barStacked.draw(gl.series)
} else {
this.ctx.bar = new Bar(this.ctx, xyRatios)
elGraph = this.ctx.bar.draw(gl.series)
}
break
case 'candlestick':
let candleStick = new BoxCandleStick(this.ctx, xyRatios)
elGraph = candleStick.draw(gl.series, 'candlestick')
break
case 'boxPlot':
let boxPlot = new BoxCandleStick(this.ctx, xyRatios)
elGraph = boxPlot.draw(gl.series, cnf.chart.type)
break
case 'rangeBar':
elGraph = this.ctx.rangeBar.draw(gl.series)
break
case 'rangeArea':
elGraph = line.draw(
gl.seriesRangeStart,
'rangeArea',
undefined,
gl.seriesRangeEnd
)
break
case 'heatmap':
let heatmap = new HeatMap(this.ctx, xyRatios)
elGraph = heatmap.draw(gl.series)
break
case 'treemap':
let treemap = new Treemap(this.ctx, xyRatios)
elGraph = treemap.draw(gl.series)
break
case 'pie':
case 'donut':
case 'polarArea':
elGraph = this.ctx.pie.draw(gl.series)
break
case 'radialBar':
elGraph = radialBar.draw(gl.series)
break
case 'radar':
elGraph = radar.draw(gl.series)
break
default:
elGraph = line.draw(gl.series)
}
}
return elGraph
}
setSVGDimensions() {
let gl = this.w.globals
let cnf = this.w.config
gl.svgWidth = cnf.chart.width
gl.svgHeight = cnf.chart.height
let elDim = Utils.getDimensions(this.el)
let widthUnit = cnf.chart.width
.toString()
.split(/[0-9]+/g)
.pop()
if (widthUnit === '%') {
if (Utils.isNumber(elDim[0])) {
if (elDim[0].width === 0) {
elDim = Utils.getDimensions(this.el.parentNode)
}
gl.svgWidth = (elDim[0] * parseInt(cnf.chart.width, 10)) / 100
}
} else if (widthUnit === 'px' || widthUnit === '') {
gl.svgWidth = parseInt(cnf.chart.width, 10)
}
let heightUnit = cnf.chart.height
.toString()
.split(/[0-9]+/g)
.pop()
if (gl.svgHeight !== 'auto' && gl.svgHeight !== '') {
if (heightUnit === '%') {
let elParentDim = Utils.getDimensions(this.el.parentNode)
gl.svgHeight = (elParentDim[1] * parseInt(cnf.chart.height, 10)) / 100
} else {
gl.svgHeight = parseInt(cnf.chart.height, 10)
}
} else {
if (gl.axisCharts) {
gl.svgHeight = gl.svgWidth / 1.61
} else {
gl.svgHeight = gl.svgWidth / 1.2
}
}
if (gl.svgWidth < 0) gl.svgWidth = 0
if (gl.svgHeight < 0) gl.svgHeight = 0
Graphics.setAttrs(gl.dom.Paper.node, {
width: gl.svgWidth,
height: gl.svgHeight,
})
if (heightUnit !== '%') {
// fixes https://github.com/apexcharts/apexcharts.js/issues/2059
let offsetY = cnf.chart.sparkline.enabled
? 0
: gl.axisCharts
? cnf.chart.parentHeightOffset
: 0
gl.dom.Paper.node.parentNode.parentNode.style.minHeight =
gl.svgHeight + offsetY + 'px'
}
gl.dom.elWrap.style.width = gl.svgWidth + 'px'
gl.dom.elWrap.style.height = gl.svgHeight + 'px'
}
shiftGraphPosition() {
let gl = this.w.globals
let tY = gl.translateY
let tX = gl.translateX
let scalingAttrs = {
transform: 'translate(' + tX + ', ' + tY + ')',
}
Graphics.setAttrs(gl.dom.elGraphical.node, scalingAttrs)
}
// To prevent extra spacings in the bottom of the chart, we need to recalculate the height for pie/donut/radialbar charts
resizeNonAxisCharts() {
const w = this.w
const gl = w.globals
let legendHeight = 0
let offY = w.config.chart.sparkline.enabled ? 1 : 15
offY = offY + w.config.grid.padding.bottom
if (
(w.config.legend.position === 'top' ||
w.config.legend.position === 'bottom') &&
w.config.legend.show &&
!w.config.legend.floating
) {
legendHeight =
new Legend(this.ctx).legendHelpers.getLegendBBox().clwh + 10
}
let el = w.globals.dom.baseEl.querySelector(
'.apexcharts-radialbar, .apexcharts-pie'
)
let chartInnerDimensions = w.globals.radialSize * 2.05
if (
el &&
!w.config.chart.sparkline.enabled &&
w.config.plotOptions.radialBar.startAngle !== 0
) {
let elRadialRect = Utils.getBoundingClientRect(el)
chartInnerDimensions = elRadialRect.bottom
let maxHeight = elRadialRect.bottom - elRadialRect.top
chartInnerDimensions = Math.max(w.globals.radialSize * 2.05, maxHeight)
}
let newHeight = chartInnerDimensions + gl.translateY + legendHeight + offY
if (gl.dom.elLegendForeign) {
gl.dom.elLegendForeign.setAttribute('height', newHeight)
}
// fix apexcharts/apexcharts.js/issues/3105 (when % is provided in height, it keeps increasing)
if (w.config.chart.height && String(w.config.chart.height).indexOf('%') > 0)
return
gl.dom.elWrap.style.height = newHeight + 'px'
Graphics.setAttrs(gl.dom.Paper.node, {
height: newHeight,
})
gl.dom.Paper.node.parentNode.parentNode.style.minHeight = newHeight + 'px'
}
/*
** All the calculations for setting range in charts will be done here
*/
coreCalculations() {
const range = new Range(this.ctx)
range.init()
}
resetGlobals() {
const resetxyValues = () => {
return this.w.config.series.map((s) => [])
}
const globalObj = new Globals()
let gl = this.w.globals
globalObj.initGlobalVars(gl)
gl.seriesXvalues = resetxyValues()
gl.seriesYvalues = resetxyValues()
}
isMultipleY() {
// user has supplied an array in yaxis property. So, turn on multipleYAxis flag
if (
this.w.config.yaxis.constructor === Array &&
this.w.config.yaxis.length > 1
) {
this.w.globals.isMultipleYAxis = true
return true
}
}
xySettings() {
let xyRatios = null
const w = this.w
if (w.globals.axisCharts) {
if (w.config.xaxis.crosshairs.position === 'back') {
const crosshairs = new Crosshairs(this.ctx)
crosshairs.drawXCrosshairs()
}
if (w.config.yaxis[0].crosshairs.position === 'back') {
const crosshairs = new Crosshairs(this.ctx)
crosshairs.drawYCrosshairs()
}
if (
w.config.xaxis.type === 'datetime' &&
w.config.xaxis.labels.formatter === undefined
) {
this.ctx.timeScale = new TimeScale(this.ctx)
let formattedTimeScale = []
if (
isFinite(w.globals.minX) &&
isFinite(w.globals.maxX) &&
!w.globals.isBarHorizontal
) {
formattedTimeScale = this.ctx.timeScale.calculateTimeScaleTicks(
w.globals.minX,
w.globals.maxX
)
} else if (w.globals.isBarHorizontal) {
formattedTimeScale = this.ctx.timeScale.calculateTimeScaleTicks(
w.globals.minY,
w.globals.maxY
)
}
this.ctx.timeScale.recalcDimensionsBasedOnFormat(formattedTimeScale)
}
const coreUtils = new CoreUtils(this.ctx)
xyRatios = coreUtils.getCalculatedRatios()
}
return xyRatios
}
updateSourceChart(targetChart) {
this.ctx.w.globals.selection = undefined
this.ctx.updateHelpers._updateOptions(
{
chart: {
selection: {
xaxis: {
min: targetChart.w.globals.minX,
max: targetChart.w.globals.maxX,
},
},
},
},
false,
false
)
}
setupBrushHandler() {
const w = this.w
// only for brush charts
if (!w.config.chart.brush.enabled) {
return
}
// if user has not defined a custom function for selection - we handle the brush chart
// otherwise we leave it to the user to define the functionality for selection
if (typeof w.config.chart.events.selection !== 'function') {
let targets = Array.isArray(w.config.chart.brush.targets) ? w.config.chart.brush.targets : [
w.config.chart.brush.target,
]
// retro compatibility with single target option
targets.forEach((target) => {
let targetChart = ApexCharts.getChartByID(target)
targetChart.w.globals.brushSource = this.ctx
if (typeof targetChart.w.config.chart.events.zoomed !== 'function') {
targetChart.w.config.chart.events.zoomed = () => {
this.updateSourceChart(targetChart)
}
}
if (typeof targetChart.w.config.chart.events.scrolled !== 'function') {
targetChart.w.config.chart.events.scrolled = () => {
this.updateSourceChart(targetChart)
}
}
})
w.config.chart.events.selection = (chart, e) => {
targets.forEach((target) => {
let targetChart = ApexCharts.getChartByID(target)
targetChart.ctx.updateHelpers._updateOptions(
{
xaxis: {
min: e.xaxis.min,
max: e.xaxis.max,
}
},
false,
false,
false,
false
)
})
}
}
}
}
+412
View File
@@ -0,0 +1,412 @@
/*
** Util functions which are dependent on ApexCharts instance
*/
class CoreUtils {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
static checkComboSeries(series) {
let comboCharts = false
let comboBarCount = 0
let comboCount = 0
// if user specified a type in series too, turn on comboCharts flag
if (series.length && typeof series[0].type !== 'undefined') {
series.forEach((s) => {
if (
s.type === 'bar' ||
s.type === 'column' ||
s.type === 'candlestick' ||
s.type === 'boxPlot'
) {
comboBarCount++
}
if (typeof s.type !== 'undefined') {
comboCount++
}
})
}
if (comboCount > 0) {
comboCharts = true
}
return {
comboBarCount,
comboCharts,
}
}
/**
* @memberof CoreUtils
* returns the sum of all individual values in a multiple stacked series
* Eg. w.globals.series = [[32,33,43,12], [2,3,5,1]]
* @return [34,36,48,13]
**/
getStackedSeriesTotals(excludedSeriesIndices = []) {
const w = this.w
let total = []
if (w.globals.series.length === 0) return total
for (
let i = 0;
i < w.globals.series[w.globals.maxValsInArrayIndex].length;
i++
) {
let t = 0
for (let j = 0; j < w.globals.series.length; j++) {
if (
typeof w.globals.series[j][i] !== 'undefined' &&
excludedSeriesIndices.indexOf(j) === -1
) {
t += w.globals.series[j][i]
}
}
total.push(t)
}
return total
}
// get total of the all values inside all series
getSeriesTotalByIndex(index = null) {
if (index === null) {
// non-plot chart types - pie / donut / circle
return this.w.config.series.reduce((acc, cur) => acc + cur, 0)
} else {
// axis charts - supporting multiple series
return this.w.globals.series[index].reduce((acc, cur) => acc + cur, 0)
}
}
/**
* @memberof CoreUtils
* returns the sum of values in a multiple stacked grouped charts
* Eg. w.globals.series = [[32,33,43,12], [2,3,5,1], [43, 23, 34, 22]]
* series 1 and 2 are in a group, while series 3 is in another group
* @return [[34, 36, 48, 12], [43, 23, 34, 22]]
**/
getStackedSeriesTotalsByGroups() {
const w = this.w
let total = []
w.globals.seriesGroups.forEach((sg) => {
let includedIndexes = []
w.config.series.forEach((s, si) => {
if (sg.indexOf(s.name) > -1) {
includedIndexes.push(si)
}
})
const excludedIndices = w.globals.series
.map((_, fi) => (includedIndexes.indexOf(fi) === -1 ? fi : -1))
.filter((f) => f !== -1)
total.push(this.getStackedSeriesTotals(excludedIndices))
})
return total
}
isSeriesNull(index = null) {
let r = []
if (index === null) {
// non-plot chart types - pie / donut / circle
r = this.w.config.series.filter((d) => d !== null)
} else {
// axis charts - supporting multiple series
r = this.w.config.series[index].data.filter((d) => d !== null)
}
return r.length === 0
}
seriesHaveSameValues(index) {
return this.w.globals.series[index].every((val, i, arr) => val === arr[0])
}
getCategoryLabels(labels) {
const w = this.w
let catLabels = labels.slice()
if (w.config.xaxis.convertedCatToNumeric) {
catLabels = labels.map((i, li) => {
return w.config.xaxis.labels.formatter(i - w.globals.minX + 1)
})
}
return catLabels
}
// maxValsInArrayIndex is the index of series[] which has the largest number of items
getLargestSeries() {
const w = this.w
w.globals.maxValsInArrayIndex = w.globals.series
.map((a) => a.length)
.indexOf(
Math.max.apply(
Math,
w.globals.series.map((a) => a.length)
)
)
}
getLargestMarkerSize() {
const w = this.w
let size = 0
w.globals.markers.size.forEach((m) => {
size = Math.max(size, m)
})
if (w.config.markers.discrete && w.config.markers.discrete.length) {
w.config.markers.discrete.forEach((m) => {
size = Math.max(size, m.size)
})
}
if (size > 0) {
size += w.config.markers.hover.sizeOffset + 1
}
w.globals.markers.largestSize = size
return size
}
/**
* @memberof Core
* returns the sum of all values in a series
* Eg. w.globals.series = [[32,33,43,12], [2,3,5,1]]
* @return [120, 11]
**/
getSeriesTotals() {
const w = this.w
w.globals.seriesTotals = w.globals.series.map((ser, index) => {
let total = 0
if (Array.isArray(ser)) {
for (let j = 0; j < ser.length; j++) {
total += ser[j]
}
} else {
// for pie/donuts/gauges
total += ser
}
return total
})
}
getSeriesTotalsXRange(minX, maxX) {
const w = this.w
const seriesTotalsXRange = w.globals.series.map((ser, index) => {
let total = 0
for (let j = 0; j < ser.length; j++) {
if (
w.globals.seriesX[index][j] > minX &&
w.globals.seriesX[index][j] < maxX
) {
total += ser[j]
}
}
return total
})
return seriesTotalsXRange
}
/**
* @memberof CoreUtils
* returns the percentage value of all individual values which can be used in a 100% stacked series
* Eg. w.globals.series = [[32, 33, 43, 12], [2, 3, 5, 1]]
* @return [[94.11, 91.66, 89.58, 92.30], [5.88, 8.33, 10.41, 7.7]]
**/
getPercentSeries() {
const w = this.w
w.globals.seriesPercent = w.globals.series.map((ser, index) => {
let seriesPercent = []
if (Array.isArray(ser)) {
for (let j = 0; j < ser.length; j++) {
let total = w.globals.stackedSeriesTotals[j]
let percent = 0
if (total) {
percent = (100 * ser[j]) / total
}
seriesPercent.push(percent)
}
} else {
const total = w.globals.seriesTotals.reduce((acc, val) => acc + val, 0)
let percent = (100 * ser) / total
seriesPercent.push(percent)
}
return seriesPercent
})
}
getCalculatedRatios() {
let gl = this.w.globals
let yRatio = []
let invertedYRatio = 0
let xRatio = 0
let invertedXRatio = 0
let zRatio = 0
let baseLineY = []
let baseLineInvertedY = 0.1
let baseLineX = 0
gl.yRange = []
if (gl.isMultipleYAxis) {
for (let i = 0; i < gl.minYArr.length; i++) {
gl.yRange.push(Math.abs(gl.minYArr[i] - gl.maxYArr[i]))
baseLineY.push(0)
}
} else {
gl.yRange.push(Math.abs(gl.minY - gl.maxY))
}
gl.xRange = Math.abs(gl.maxX - gl.minX)
gl.zRange = Math.abs(gl.maxZ - gl.minZ)
// multiple y axis
for (let i = 0; i < gl.yRange.length; i++) {
yRatio.push(gl.yRange[i] / gl.gridHeight)
}
xRatio = gl.xRange / gl.gridWidth
invertedYRatio = gl.yRange / gl.gridWidth
invertedXRatio = gl.xRange / gl.gridHeight
zRatio = (gl.zRange / gl.gridHeight) * 16
if (!zRatio) {
zRatio = 1
}
if (gl.minY !== Number.MIN_VALUE && Math.abs(gl.minY) !== 0) {
// Negative numbers present in series
gl.hasNegs = true
}
if (gl.isMultipleYAxis) {
baseLineY = []
// baseline variables is the 0 of the yaxis which will be needed when there are negatives
for (let i = 0; i < yRatio.length; i++) {
baseLineY.push(-gl.minYArr[i] / yRatio[i])
}
} else {
baseLineY.push(-gl.minY / yRatio[0])
if (gl.minY !== Number.MIN_VALUE && Math.abs(gl.minY) !== 0) {
baseLineInvertedY = -gl.minY / invertedYRatio // this is for bar chart
baseLineX = gl.minX / xRatio
}
}
return {
yRatio,
invertedYRatio,
zRatio,
xRatio,
invertedXRatio,
baseLineInvertedY,
baseLineY,
baseLineX,
}
}
getLogSeries(series) {
const w = this.w
w.globals.seriesLog = series.map((s, i) => {
if (w.config.yaxis[i] && w.config.yaxis[i].logarithmic) {
return s.map((d) => {
if (d === null) return null
return this.getLogVal(w.config.yaxis[i].logBase, d, i)
})
} else {
return s
}
})
return w.globals.invalidLogScale ? series : w.globals.seriesLog
}
getBaseLog(base, value) {
return Math.log(value) / Math.log(base)
}
getLogVal(b, d, yIndex) {
if (d === 0) {
return 0
}
const w = this.w
const min_log_val =
w.globals.minYArr[yIndex] === 0
? -1 // make sure we dont calculate log of 0
: this.getBaseLog(b, w.globals.minYArr[yIndex])
const max_log_val =
w.globals.maxYArr[yIndex] === 0
? 0 // make sure we dont calculate log of 0
: this.getBaseLog(b, w.globals.maxYArr[yIndex])
const number_of_height_levels = max_log_val - min_log_val
if (d < 1) return d / number_of_height_levels
const log_height_value = this.getBaseLog(b, d) - min_log_val
return log_height_value / number_of_height_levels
}
getLogYRatios(yRatio) {
const w = this.w
const gl = this.w.globals
gl.yLogRatio = yRatio.slice()
gl.logYRange = gl.yRange.map((yRange, i) => {
if (w.config.yaxis[i] && this.w.config.yaxis[i].logarithmic) {
let maxY = -Number.MAX_VALUE
let minY = Number.MIN_VALUE
let range = 1
gl.seriesLog.forEach((s, si) => {
s.forEach((v) => {
if (w.config.yaxis[si] && w.config.yaxis[si].logarithmic) {
maxY = Math.max(v, maxY)
minY = Math.min(v, minY)
}
})
})
range = Math.pow(gl.yRange[i], Math.abs(minY - maxY) / gl.yRange[i])
gl.yLogRatio[i] = range / gl.gridHeight
return range
}
})
return gl.invalidLogScale ? yRatio.slice() : gl.yLogRatio
}
// Some config objects can be array - and we need to extend them correctly
static extendArrayProps(configInstance, options, w) {
if (options.yaxis) {
options = configInstance.extendYAxis(options, w)
}
if (options.annotations) {
if (options.annotations.yaxis) {
options = configInstance.extendYAxisAnnotations(options)
}
if (options.annotations.xaxis) {
options = configInstance.extendXAxisAnnotations(options)
}
if (options.annotations.points) {
options = configInstance.extendPointAnnotations(options)
}
}
return options
}
}
export default CoreUtils
+138
View File
@@ -0,0 +1,138 @@
import Graphics from './Graphics'
import Filters from './Filters'
import Utils from '../utils/Utils'
class Crosshairs {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
drawXCrosshairs() {
const w = this.w
let graphics = new Graphics(this.ctx)
let filters = new Filters(this.ctx)
let crosshairGradient = w.config.xaxis.crosshairs.fill.gradient
let crosshairShadow = w.config.xaxis.crosshairs.dropShadow
let fillType = w.config.xaxis.crosshairs.fill.type
let gradientFrom = crosshairGradient.colorFrom
let gradientTo = crosshairGradient.colorTo
let opacityFrom = crosshairGradient.opacityFrom
let opacityTo = crosshairGradient.opacityTo
let stops = crosshairGradient.stops
let shadow = 'none'
let dropShadow = crosshairShadow.enabled
let shadowLeft = crosshairShadow.left
let shadowTop = crosshairShadow.top
let shadowBlur = crosshairShadow.blur
let shadowColor = crosshairShadow.color
let shadowOpacity = crosshairShadow.opacity
let xcrosshairsFill = w.config.xaxis.crosshairs.fill.color
if (w.config.xaxis.crosshairs.show) {
if (fillType === 'gradient') {
xcrosshairsFill = graphics.drawGradient(
'vertical',
gradientFrom,
gradientTo,
opacityFrom,
opacityTo,
null,
stops,
null
)
}
let xcrosshairs = graphics.drawRect()
if (w.config.xaxis.crosshairs.width === 1) {
// to prevent drawing 2 lines, convert rect to line
xcrosshairs = graphics.drawLine()
}
let gridHeight = w.globals.gridHeight
if (!Utils.isNumber(gridHeight) || gridHeight < 0) {
gridHeight = 0
}
let crosshairsWidth = w.config.xaxis.crosshairs.width
if (!Utils.isNumber(crosshairsWidth) || crosshairsWidth < 0) {
crosshairsWidth = 0
}
xcrosshairs.attr({
class: 'apexcharts-xcrosshairs',
x: 0,
y: 0,
y2: gridHeight,
width: crosshairsWidth,
height: gridHeight,
fill: xcrosshairsFill,
filter: shadow,
'fill-opacity': w.config.xaxis.crosshairs.opacity,
stroke: w.config.xaxis.crosshairs.stroke.color,
'stroke-width': w.config.xaxis.crosshairs.stroke.width,
'stroke-dasharray': w.config.xaxis.crosshairs.stroke.dashArray
})
if (dropShadow) {
xcrosshairs = filters.dropShadow(xcrosshairs, {
left: shadowLeft,
top: shadowTop,
blur: shadowBlur,
color: shadowColor,
opacity: shadowOpacity
})
}
w.globals.dom.elGraphical.add(xcrosshairs)
}
}
drawYCrosshairs() {
const w = this.w
let graphics = new Graphics(this.ctx)
let crosshair = w.config.yaxis[0].crosshairs
const offX = w.globals.barPadForNumericAxis
if (w.config.yaxis[0].crosshairs.show) {
let ycrosshairs = graphics.drawLine(
-offX,
0,
w.globals.gridWidth + offX,
0,
crosshair.stroke.color,
crosshair.stroke.dashArray,
crosshair.stroke.width
)
ycrosshairs.attr({
class: 'apexcharts-ycrosshairs'
})
w.globals.dom.elGraphical.add(ycrosshairs)
}
// draw an invisible crosshair to help in positioning the yaxis tooltip
let ycrosshairsHidden = graphics.drawLine(
-offX,
0,
w.globals.gridWidth + offX,
0,
crosshair.stroke.color,
0,
0
)
ycrosshairsHidden.attr({
class: 'apexcharts-ycrosshairs-hidden'
})
w.globals.dom.elGraphical.add(ycrosshairsHidden)
}
}
export default Crosshairs
+727
View File
@@ -0,0 +1,727 @@
import CoreUtils from './CoreUtils'
import DateTime from './../utils/DateTime'
import Series from './Series'
import Utils from '../utils/Utils'
import Defaults from './settings/Defaults'
export default class Data {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.twoDSeries = []
this.threeDSeries = []
this.twoDSeriesX = []
this.seriesGoals = []
this.coreUtils = new CoreUtils(this.ctx)
}
isMultiFormat() {
return this.isFormatXY() || this.isFormat2DArray()
}
// given format is [{x, y}, {x, y}]
isFormatXY() {
const series = this.w.config.series.slice()
const sr = new Series(this.ctx)
this.activeSeriesIndex = sr.getActiveConfigSeriesIndex()
if (
typeof series[this.activeSeriesIndex].data !== 'undefined' &&
series[this.activeSeriesIndex].data.length > 0 &&
series[this.activeSeriesIndex].data[0] !== null &&
typeof series[this.activeSeriesIndex].data[0].x !== 'undefined' &&
series[this.activeSeriesIndex].data[0] !== null
) {
return true
}
}
// given format is [[x, y], [x, y]]
isFormat2DArray() {
const series = this.w.config.series.slice()
const sr = new Series(this.ctx)
this.activeSeriesIndex = sr.getActiveConfigSeriesIndex()
if (
typeof series[this.activeSeriesIndex].data !== 'undefined' &&
series[this.activeSeriesIndex].data.length > 0 &&
typeof series[this.activeSeriesIndex].data[0] !== 'undefined' &&
series[this.activeSeriesIndex].data[0] !== null &&
series[this.activeSeriesIndex].data[0].constructor === Array
) {
return true
}
}
handleFormat2DArray(ser, i) {
const cnf = this.w.config
const gl = this.w.globals
const isBoxPlot =
cnf.chart.type === 'boxPlot' || cnf.series[i].type === 'boxPlot'
for (let j = 0; j < ser[i].data.length; j++) {
if (typeof ser[i].data[j][1] !== 'undefined') {
if (
Array.isArray(ser[i].data[j][1]) &&
ser[i].data[j][1].length === 4 &&
!isBoxPlot
) {
// candlestick nested ohlc format
this.twoDSeries.push(Utils.parseNumber(ser[i].data[j][1][3]))
} else if (ser[i].data[j].length >= 5) {
// candlestick non-nested ohlc format
this.twoDSeries.push(Utils.parseNumber(ser[i].data[j][4]))
} else {
this.twoDSeries.push(Utils.parseNumber(ser[i].data[j][1]))
}
gl.dataFormatXNumeric = true
}
if (cnf.xaxis.type === 'datetime') {
// if timestamps are provided and xaxis type is datetime,
let ts = new Date(ser[i].data[j][0])
ts = new Date(ts).getTime()
this.twoDSeriesX.push(ts)
} else {
this.twoDSeriesX.push(ser[i].data[j][0])
}
}
for (let j = 0; j < ser[i].data.length; j++) {
if (typeof ser[i].data[j][2] !== 'undefined') {
this.threeDSeries.push(ser[i].data[j][2])
gl.isDataXYZ = true
}
}
}
handleFormatXY(ser, i) {
const cnf = this.w.config
const gl = this.w.globals
const dt = new DateTime(this.ctx)
let activeI = i
if (gl.collapsedSeriesIndices.indexOf(i) > -1) {
// fix #368
activeI = this.activeSeriesIndex
}
// get series
for (let j = 0; j < ser[i].data.length; j++) {
if (typeof ser[i].data[j].y !== 'undefined') {
if (Array.isArray(ser[i].data[j].y)) {
this.twoDSeries.push(
Utils.parseNumber(ser[i].data[j].y[ser[i].data[j].y.length - 1])
)
} else {
this.twoDSeries.push(Utils.parseNumber(ser[i].data[j].y))
}
}
if (
typeof ser[i].data[j].goals !== 'undefined' &&
Array.isArray(ser[i].data[j].goals)
) {
if (typeof this.seriesGoals[i] === 'undefined') {
this.seriesGoals[i] = []
}
this.seriesGoals[i].push(ser[i].data[j].goals)
} else {
if (typeof this.seriesGoals[i] === 'undefined') {
this.seriesGoals[i] = []
}
this.seriesGoals[i].push(null)
}
}
// get seriesX
for (let j = 0; j < ser[activeI].data.length; j++) {
const isXString = typeof ser[activeI].data[j].x === 'string'
const isXArr = Array.isArray(ser[activeI].data[j].x)
const isXDate = !isXArr && !!dt.isValidDate(ser[activeI].data[j].x)
if (isXString || isXDate) {
// user supplied '01/01/2017' or a date string (a JS date object is not supported)
if (isXString || cnf.xaxis.convertedCatToNumeric) {
const isRangeColumn = gl.isBarHorizontal && gl.isRangeData
if (cnf.xaxis.type === 'datetime' && !isRangeColumn) {
this.twoDSeriesX.push(dt.parseDate(ser[activeI].data[j].x))
} else {
// a category and not a numeric x value
this.fallbackToCategory = true
this.twoDSeriesX.push(ser[activeI].data[j].x)
if (
!isNaN(ser[activeI].data[j].x) &&
this.w.config.xaxis.type !== 'category' &&
typeof ser[activeI].data[j].x !== 'string'
) {
gl.isXNumeric = true
}
}
} else {
if (cnf.xaxis.type === 'datetime') {
this.twoDSeriesX.push(
dt.parseDate(ser[activeI].data[j].x.toString())
)
} else {
gl.dataFormatXNumeric = true
gl.isXNumeric = true
this.twoDSeriesX.push(parseFloat(ser[activeI].data[j].x))
}
}
} else if (isXArr) {
// a multiline label described in array format
this.fallbackToCategory = true
this.twoDSeriesX.push(ser[activeI].data[j].x)
} else {
// a numeric value in x property
gl.isXNumeric = true
gl.dataFormatXNumeric = true
this.twoDSeriesX.push(ser[activeI].data[j].x)
}
}
if (ser[i].data[0] && typeof ser[i].data[0].z !== 'undefined') {
for (let t = 0; t < ser[i].data.length; t++) {
this.threeDSeries.push(ser[i].data[t].z)
}
gl.isDataXYZ = true
}
}
handleRangeData(ser, i) {
const gl = this.w.globals
let range = {}
if (this.isFormat2DArray()) {
range = this.handleRangeDataFormat('array', ser, i)
} else if (this.isFormatXY()) {
range = this.handleRangeDataFormat('xy', ser, i)
}
gl.seriesRangeStart.push(range.start)
gl.seriesRangeEnd.push(range.end)
gl.seriesRange.push(range.rangeUniques)
// check for overlaps to avoid clashes in a timeline chart
gl.seriesRange.forEach((sr, si) => {
if (sr) {
sr.forEach((sarr, sarri) => {
sarr.y.forEach((arr, arri) => {
for (let sri = 0; sri < sarr.y.length; sri++) {
if (arri !== sri) {
const range1y1 = arr.y1
const range1y2 = arr.y2
const range2y1 = sarr.y[sri].y1
const range2y2 = sarr.y[sri].y2
if (range1y1 <= range2y2 && range2y1 <= range1y2) {
if (sarr.overlaps.indexOf(arr.rangeName) < 0) {
sarr.overlaps.push(arr.rangeName)
}
if (sarr.overlaps.indexOf(sarr.y[sri].rangeName) < 0) {
sarr.overlaps.push(sarr.y[sri].rangeName)
}
}
}
}
})
})
}
})
return range
}
handleCandleStickBoxData(ser, i) {
const gl = this.w.globals
let ohlc = {}
if (this.isFormat2DArray()) {
ohlc = this.handleCandleStickBoxDataFormat('array', ser, i)
} else if (this.isFormatXY()) {
ohlc = this.handleCandleStickBoxDataFormat('xy', ser, i)
}
gl.seriesCandleO[i] = ohlc.o
gl.seriesCandleH[i] = ohlc.h
gl.seriesCandleM[i] = ohlc.m
gl.seriesCandleL[i] = ohlc.l
gl.seriesCandleC[i] = ohlc.c
return ohlc
}
handleRangeDataFormat(format, ser, i) {
const rangeStart = []
const rangeEnd = []
const uniqueKeys = ser[i].data
.filter(
(thing, index, self) => index === self.findIndex((t) => t.x === thing.x)
)
.map((r, index) => {
return {
x: r.x,
overlaps: [],
y: [],
}
})
if (format === 'array') {
for (let j = 0; j < ser[i].data.length; j++) {
if (Array.isArray(ser[i].data[j])) {
rangeStart.push(ser[i].data[j][1][0])
rangeEnd.push(ser[i].data[j][1][1])
} else {
rangeStart.push(ser[i].data[j])
rangeEnd.push(ser[i].data[j])
}
}
} else if (format === 'xy') {
for (let j = 0; j < ser[i].data.length; j++) {
let isDataPoint2D = Array.isArray(ser[i].data[j].y)
const id = Utils.randomId()
const x = ser[i].data[j].x
const y = {
y1: isDataPoint2D ? ser[i].data[j].y[0] : ser[i].data[j].y,
y2: isDataPoint2D ? ser[i].data[j].y[1] : ser[i].data[j].y,
rangeName: id,
}
// CAUTION: mutating config object by adding a new property
// TODO: As this is specifically for timeline rangebar charts, update the docs mentioning the series only supports xy format
ser[i].data[j].rangeName = id
const uI = uniqueKeys.findIndex((t) => t.x === x)
uniqueKeys[uI].y.push(y)
rangeStart.push(y.y1)
rangeEnd.push(y.y2)
}
}
return {
start: rangeStart,
end: rangeEnd,
rangeUniques: uniqueKeys,
}
}
handleCandleStickBoxDataFormat(format, ser, i) {
const w = this.w
const isBoxPlot =
w.config.chart.type === 'boxPlot' || w.config.series[i].type === 'boxPlot'
const serO = []
const serH = []
const serM = []
const serL = []
const serC = []
if (format === 'array') {
if (
(isBoxPlot && ser[i].data[0].length === 6) ||
(!isBoxPlot && ser[i].data[0].length === 5)
) {
for (let j = 0; j < ser[i].data.length; j++) {
serO.push(ser[i].data[j][1])
serH.push(ser[i].data[j][2])
if (isBoxPlot) {
serM.push(ser[i].data[j][3])
serL.push(ser[i].data[j][4])
serC.push(ser[i].data[j][5])
} else {
serL.push(ser[i].data[j][3])
serC.push(ser[i].data[j][4])
}
}
} else {
for (let j = 0; j < ser[i].data.length; j++) {
if (Array.isArray(ser[i].data[j][1])) {
serO.push(ser[i].data[j][1][0])
serH.push(ser[i].data[j][1][1])
if (isBoxPlot) {
serM.push(ser[i].data[j][1][2])
serL.push(ser[i].data[j][1][3])
serC.push(ser[i].data[j][1][4])
} else {
serL.push(ser[i].data[j][1][2])
serC.push(ser[i].data[j][1][3])
}
}
}
}
} else if (format === 'xy') {
for (let j = 0; j < ser[i].data.length; j++) {
if (Array.isArray(ser[i].data[j].y)) {
serO.push(ser[i].data[j].y[0])
serH.push(ser[i].data[j].y[1])
if (isBoxPlot) {
serM.push(ser[i].data[j].y[2])
serL.push(ser[i].data[j].y[3])
serC.push(ser[i].data[j].y[4])
} else {
serL.push(ser[i].data[j].y[2])
serC.push(ser[i].data[j].y[3])
}
}
}
}
return {
o: serO,
h: serH,
m: serM,
l: serL,
c: serC,
}
}
parseDataAxisCharts(ser, ctx = this.ctx) {
const cnf = this.w.config
const gl = this.w.globals
const dt = new DateTime(ctx)
const xlabels =
cnf.labels.length > 0 ? cnf.labels.slice() : cnf.xaxis.categories.slice()
gl.isRangeBar = cnf.chart.type === 'rangeBar' && gl.isBarHorizontal
gl.hasXaxisGroups =
cnf.xaxis.type === 'category' && cnf.xaxis.group.groups.length > 0
if (gl.hasXaxisGroups) {
gl.groups = cnf.xaxis.group.groups
}
gl.hasSeriesGroups = ser[0]?.group
if (gl.hasSeriesGroups) {
let buckets = []
let groups = [...new Set(ser.map((s) => s.group))]
ser.forEach((s, i) => {
let index = groups.indexOf(s.group)
if (!buckets[index]) buckets[index] = []
buckets[index].push(s.name)
})
gl.seriesGroups = buckets
}
const handleDates = () => {
for (let j = 0; j < xlabels.length; j++) {
if (typeof xlabels[j] === 'string') {
// user provided date strings
let isDate = dt.isValidDate(xlabels[j])
if (isDate) {
this.twoDSeriesX.push(dt.parseDate(xlabels[j]))
} else {
throw new Error(
'You have provided invalid Date format. Please provide a valid JavaScript Date'
)
}
} else {
// user provided timestamps
this.twoDSeriesX.push(xlabels[j])
}
}
}
for (let i = 0; i < ser.length; i++) {
this.twoDSeries = []
this.twoDSeriesX = []
this.threeDSeries = []
if (typeof ser[i].data === 'undefined') {
console.error(
"It is a possibility that you may have not included 'data' property in series."
)
return
}
if (
cnf.chart.type === 'rangeBar' ||
cnf.chart.type === 'rangeArea' ||
ser[i].type === 'rangeBar' ||
ser[i].type === 'rangeArea'
) {
gl.isRangeData = true
if (cnf.chart.type === 'rangeBar' || cnf.chart.type === 'rangeArea') {
this.handleRangeData(ser, i)
}
}
if (this.isMultiFormat()) {
if (this.isFormat2DArray()) {
this.handleFormat2DArray(ser, i)
} else if (this.isFormatXY()) {
this.handleFormatXY(ser, i)
}
if (
cnf.chart.type === 'candlestick' ||
ser[i].type === 'candlestick' ||
cnf.chart.type === 'boxPlot' ||
ser[i].type === 'boxPlot'
) {
this.handleCandleStickBoxData(ser, i)
}
gl.series.push(this.twoDSeries)
gl.labels.push(this.twoDSeriesX)
gl.seriesX.push(this.twoDSeriesX)
gl.seriesGoals = this.seriesGoals
if (i === this.activeSeriesIndex && !this.fallbackToCategory) {
gl.isXNumeric = true
}
} else {
if (cnf.xaxis.type === 'datetime') {
// user didn't supplied [{x,y}] or [[x,y]], but single array in data.
// Also labels/categories were supplied differently
gl.isXNumeric = true
handleDates()
gl.seriesX.push(this.twoDSeriesX)
} else if (cnf.xaxis.type === 'numeric') {
gl.isXNumeric = true
if (xlabels.length > 0) {
this.twoDSeriesX = xlabels
gl.seriesX.push(this.twoDSeriesX)
}
}
gl.labels.push(this.twoDSeriesX)
const singleArray = ser[i].data.map((d) => Utils.parseNumber(d))
gl.series.push(singleArray)
}
gl.seriesZ.push(this.threeDSeries)
if (ser[i].name !== undefined) {
gl.seriesNames.push(ser[i].name)
} else {
gl.seriesNames.push('series-' + parseInt(i + 1, 10))
}
// overrided default color if user inputs color with series data
if (ser[i].color !== undefined) {
gl.seriesColors.push(ser[i].color)
} else {
gl.seriesColors.push(undefined)
}
}
return this.w
}
parseDataNonAxisCharts(ser) {
const gl = this.w.globals
const cnf = this.w.config
gl.series = ser.slice()
gl.seriesNames = cnf.labels.slice()
for (let i = 0; i < gl.series.length; i++) {
if (gl.seriesNames[i] === undefined) {
gl.seriesNames.push('series-' + (i + 1))
}
}
return this.w
}
/** User possibly set string categories in xaxis.categories or labels prop
* Or didn't set xaxis labels at all - in which case we manually do it.
* If user passed series data as [[3, 2], [4, 5]] or [{ x: 3, y: 55 }],
* this shouldn't be called
* @param {array} ser - the series which user passed to the config
*/
handleExternalLabelsData(ser) {
const cnf = this.w.config
const gl = this.w.globals
if (cnf.xaxis.categories.length > 0) {
// user provided labels in xaxis.category prop
gl.labels = cnf.xaxis.categories
} else if (cnf.labels.length > 0) {
// user provided labels in labels props
gl.labels = cnf.labels.slice()
} else if (this.fallbackToCategory) {
// user provided labels in x prop in [{ x: 3, y: 55 }] data, and those labels are already stored in gl.labels[0], so just re-arrange the gl.labels array
gl.labels = gl.labels[0]
if (gl.seriesRange.length) {
gl.seriesRange.map((srt) => {
srt.forEach((sr) => {
if (gl.labels.indexOf(sr.x) < 0 && sr.x) {
gl.labels.push(sr.x)
}
})
})
// remove duplicate x-axis labels
gl.labels = Array.from(
new Set(gl.labels.map(JSON.stringify)),
JSON.parse
)
}
if (cnf.xaxis.convertedCatToNumeric) {
const defaults = new Defaults(cnf)
defaults.convertCatToNumericXaxis(cnf, this.ctx, gl.seriesX[0])
this._generateExternalLabels(ser)
}
} else {
this._generateExternalLabels(ser)
}
}
_generateExternalLabels(ser) {
const gl = this.w.globals
const cnf = this.w.config
// user didn't provided any labels, fallback to 1-2-3-4-5
let labelArr = []
if (gl.axisCharts) {
if (gl.series.length > 0) {
if (this.isFormatXY()) {
// in case there is a combo chart (boxplot/scatter)
// and there are duplicated x values, we need to eliminate duplicates
const seriesDataFiltered = cnf.series.map((serie, s) => {
return serie.data.filter(
(v, i, a) => a.findIndex((t) => t.x === v.x) === i
)
})
const len = seriesDataFiltered.reduce(
(p, c, i, a) => (a[p].length > c.length ? p : i),
0
)
for (let i = 0; i < seriesDataFiltered[len].length; i++) {
labelArr.push(i + 1)
}
} else {
for (let i = 0; i < gl.series[gl.maxValsInArrayIndex].length; i++) {
labelArr.push(i + 1)
}
}
}
gl.seriesX = []
// create gl.seriesX as it will be used in calculations of x positions
for (let i = 0; i < ser.length; i++) {
gl.seriesX.push(labelArr)
}
// turn on the isXNumeric flag to allow minX and maxX to function properly
if (!this.w.globals.isBarHorizontal) {
gl.isXNumeric = true
}
}
// no series to pull labels from, put a 0-10 series
// possibly, user collapsed all series. Hence we can't work with above calc
if (labelArr.length === 0) {
labelArr = gl.axisCharts
? []
: gl.series.map((gls, glsi) => {
return glsi + 1
})
for (let i = 0; i < ser.length; i++) {
gl.seriesX.push(labelArr)
}
}
// Finally, pass the labelArr in gl.labels which will be printed on x-axis
gl.labels = labelArr
if (cnf.xaxis.convertedCatToNumeric) {
gl.categoryLabels = labelArr.map((l) => {
return cnf.xaxis.labels.formatter(l)
})
}
// Turn on this global flag to indicate no labels were provided by user
gl.noLabelsProvided = true
}
// Segregate user provided data into appropriate vars
parseData(ser) {
let w = this.w
let cnf = w.config
let gl = w.globals
this.excludeCollapsedSeriesInYAxis()
// If we detected string in X prop of series, we fallback to category x-axis
this.fallbackToCategory = false
this.ctx.core.resetGlobals()
this.ctx.core.isMultipleY()
if (gl.axisCharts) {
// axisCharts includes line / area / column / scatter
this.parseDataAxisCharts(ser)
this.coreUtils.getLargestSeries()
} else {
// non-axis charts are pie / donut
this.parseDataNonAxisCharts(ser)
}
// set Null values to 0 in all series when user hides/shows some series
if (cnf.chart.stacked) {
const series = new Series(this.ctx)
gl.series = series.setNullSeriesToZeroValues(gl.series)
}
this.coreUtils.getSeriesTotals()
if (gl.axisCharts) {
gl.stackedSeriesTotals = this.coreUtils.getStackedSeriesTotals()
gl.stackedSeriesTotalsByGroups =
this.coreUtils.getStackedSeriesTotalsByGroups()
}
this.coreUtils.getPercentSeries()
if (
!gl.dataFormatXNumeric &&
(!gl.isXNumeric ||
(cnf.xaxis.type === 'numeric' &&
cnf.labels.length === 0 &&
cnf.xaxis.categories.length === 0))
) {
// x-axis labels couldn't be detected; hence try searching every option in config
this.handleExternalLabelsData(ser)
}
// check for multiline xaxis
const catLabels = this.coreUtils.getCategoryLabels(gl.labels)
for (let l = 0; l < catLabels.length; l++) {
if (Array.isArray(catLabels[l])) {
gl.isMultiLineX = true
break
}
}
}
excludeCollapsedSeriesInYAxis() {
const w = this.w
w.globals.ignoreYAxisIndexes = w.globals.collapsedSeries.map(
(collapsed, i) => {
// fix issue #1215
// if stacked, not returning collapsed.index to preserve yaxis
if (this.w.globals.isMultipleYAxis && !w.config.chart.stacked) {
return collapsed.index
}
}
)
}
}
+382
View File
@@ -0,0 +1,382 @@
import Scatter from './../charts/Scatter'
import Graphics from './Graphics'
import Filters from './Filters'
/**
* ApexCharts DataLabels Class for drawing dataLabels on Axes based Charts.
*
* @module DataLabels
**/
class DataLabels {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
// When there are many datalabels to be printed, and some of them overlaps each other in the same series, this method will take care of that
// Also, when datalabels exceeds the drawable area and get clipped off, we need to adjust and move some pixels to make them visible again
dataLabelsCorrection(
x,
y,
val,
i,
dataPointIndex,
alwaysDrawDataLabel,
fontSize
) {
let w = this.w
let graphics = new Graphics(this.ctx)
let drawnextLabel = false //
let textRects = graphics.getTextRects(val, fontSize)
let width = textRects.width
let height = textRects.height
if (y < 0) y = 0
if (y > w.globals.gridHeight + height) y = w.globals.gridHeight + height / 2
// first value in series, so push an empty array
if (typeof w.globals.dataLabelsRects[i] === 'undefined')
w.globals.dataLabelsRects[i] = []
// then start pushing actual rects in that sub-array
w.globals.dataLabelsRects[i].push({ x, y, width, height })
let len = w.globals.dataLabelsRects[i].length - 2
let lastDrawnIndex =
typeof w.globals.lastDrawnDataLabelsIndexes[i] !== 'undefined'
? w.globals.lastDrawnDataLabelsIndexes[i][
w.globals.lastDrawnDataLabelsIndexes[i].length - 1
]
: 0
if (typeof w.globals.dataLabelsRects[i][len] !== 'undefined') {
let lastDataLabelRect = w.globals.dataLabelsRects[i][lastDrawnIndex]
if (
// next label forward and x not intersecting
x > lastDataLabelRect.x + lastDataLabelRect.width ||
y > lastDataLabelRect.y + lastDataLabelRect.height ||
y + height < lastDataLabelRect.y ||
x + width < lastDataLabelRect.x // next label is going to be drawn backwards
) {
// the 2 indexes don't override, so OK to draw next label
drawnextLabel = true
}
}
if (dataPointIndex === 0 || alwaysDrawDataLabel) {
drawnextLabel = true
}
return {
x,
y,
textRects,
drawnextLabel,
}
}
drawDataLabel({ type, pos, i, j, isRangeStart, strokeWidth = 2 }) {
// this method handles line, area, bubble, scatter charts as those charts contains markers/points which have pre-defined x/y positions
// all other charts like radar / bars / heatmaps will define their own drawDataLabel routine
let w = this.w
const graphics = new Graphics(this.ctx)
let dataLabelsConfig = w.config.dataLabels
let x = 0
let y = 0
let dataPointIndex = j
let elDataLabelsWrap = null
if (!dataLabelsConfig.enabled || !Array.isArray(pos.x)) {
return elDataLabelsWrap
}
elDataLabelsWrap = graphics.group({
class: 'apexcharts-data-labels',
})
for (let q = 0; q < pos.x.length; q++) {
x = pos.x[q] + dataLabelsConfig.offsetX
y = pos.y[q] + dataLabelsConfig.offsetY + strokeWidth
if (!isNaN(x)) {
// a small hack as we have 2 points for the first val to connect it
if (j === 1 && q === 0) dataPointIndex = 0
if (j === 1 && q === 1) dataPointIndex = 1
let val = w.globals.series[i][dataPointIndex]
if (type === 'rangeArea') {
if (isRangeStart) {
val = w.globals.seriesRangeStart[i][dataPointIndex]
} else {
val = w.globals.seriesRangeEnd[i][dataPointIndex]
}
}
let text = ''
const getText = (v) => {
return w.config.dataLabels.formatter(v, {
ctx: this.ctx,
seriesIndex: i,
dataPointIndex,
w,
})
}
if (w.config.chart.type === 'bubble') {
val = w.globals.seriesZ[i][dataPointIndex]
text = getText(val)
y = pos.y[q]
const scatter = new Scatter(this.ctx)
let centerTextInBubbleCoords = scatter.centerTextInBubble(
y,
i,
dataPointIndex
)
y = centerTextInBubbleCoords.y
} else {
if (typeof val !== 'undefined') {
text = getText(val)
}
}
this.plotDataLabelsText({
x,
y,
text,
i,
j: dataPointIndex,
parent: elDataLabelsWrap,
offsetCorrection: true,
dataLabelsConfig: w.config.dataLabels,
})
}
}
return elDataLabelsWrap
}
plotDataLabelsText(opts) {
let w = this.w
let graphics = new Graphics(this.ctx)
let {
x,
y,
i,
j,
text,
textAnchor,
fontSize,
parent,
dataLabelsConfig,
color,
alwaysDrawDataLabel,
offsetCorrection,
} = opts
if (Array.isArray(w.config.dataLabels.enabledOnSeries)) {
if (w.config.dataLabels.enabledOnSeries.indexOf(i) < 0) {
return
}
}
let correctedLabels = {
x,
y,
drawnextLabel: true,
textRects: null,
}
if (offsetCorrection) {
correctedLabels = this.dataLabelsCorrection(
x,
y,
text,
i,
j,
alwaysDrawDataLabel,
parseInt(dataLabelsConfig.style.fontSize, 10)
)
}
// when zoomed, we don't need to correct labels offsets,
// but if normally, labels get cropped, correct them
if (!w.globals.zoomed) {
x = correctedLabels.x
y = correctedLabels.y
}
if (correctedLabels.textRects) {
// fixes #2264
if (
x < -20 - correctedLabels.textRects.width ||
x > w.globals.gridWidth + correctedLabels.textRects.width + 30
) {
// datalabels fall outside drawing area, so draw a blank label
text = ''
}
}
let dataLabelColor = w.globals.dataLabels.style.colors[i]
if (
((w.config.chart.type === 'bar' || w.config.chart.type === 'rangeBar') &&
w.config.plotOptions.bar.distributed) ||
w.config.dataLabels.distributed
) {
dataLabelColor = w.globals.dataLabels.style.colors[j]
}
if (typeof dataLabelColor === 'function') {
dataLabelColor = dataLabelColor({
series: w.globals.series,
seriesIndex: i,
dataPointIndex: j,
w,
})
}
if (color) {
dataLabelColor = color
}
let offX = dataLabelsConfig.offsetX
let offY = dataLabelsConfig.offsetY
if (w.config.chart.type === 'bar' || w.config.chart.type === 'rangeBar') {
// for certain chart types, we handle offsets while calculating datalabels pos
// why? because bars/column may have negative values and based on that
// offsets becomes reversed
offX = 0
offY = 0
}
if (correctedLabels.drawnextLabel) {
let dataLabelText = graphics.drawText({
width: 100,
height: parseInt(dataLabelsConfig.style.fontSize, 10),
x: x + offX,
y: y + offY,
foreColor: dataLabelColor,
textAnchor: textAnchor || dataLabelsConfig.textAnchor,
text,
fontSize: fontSize || dataLabelsConfig.style.fontSize,
fontFamily: dataLabelsConfig.style.fontFamily,
fontWeight: dataLabelsConfig.style.fontWeight || 'normal',
})
dataLabelText.attr({
class: 'apexcharts-datalabel',
cx: x,
cy: y,
})
if (dataLabelsConfig.dropShadow.enabled) {
const textShadow = dataLabelsConfig.dropShadow
const filters = new Filters(this.ctx)
filters.dropShadow(dataLabelText, textShadow)
}
parent.add(dataLabelText)
if (typeof w.globals.lastDrawnDataLabelsIndexes[i] === 'undefined') {
w.globals.lastDrawnDataLabelsIndexes[i] = []
}
w.globals.lastDrawnDataLabelsIndexes[i].push(j)
}
}
addBackgroundToDataLabel(el, coords) {
const w = this.w
const bCnf = w.config.dataLabels.background
const paddingH = bCnf.padding
const paddingV = bCnf.padding / 2
const width = coords.width
const height = coords.height
const graphics = new Graphics(this.ctx)
const elRect = graphics.drawRect(
coords.x - paddingH,
coords.y - paddingV / 2,
width + paddingH * 2,
height + paddingV,
bCnf.borderRadius,
w.config.chart.background === 'transparent'
? '#fff'
: w.config.chart.background,
bCnf.opacity,
bCnf.borderWidth,
bCnf.borderColor
)
if (bCnf.dropShadow.enabled) {
const filters = new Filters(this.ctx)
filters.dropShadow(elRect, bCnf.dropShadow)
}
return elRect
}
dataLabelsBackground() {
const w = this.w
if (w.config.chart.type === 'bubble') return
const elDataLabels = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-datalabels text'
)
for (let i = 0; i < elDataLabels.length; i++) {
const el = elDataLabels[i]
const coords = el.getBBox()
let elRect = null
if (coords.width && coords.height) {
elRect = this.addBackgroundToDataLabel(el, coords)
}
if (elRect) {
el.parentNode.insertBefore(elRect.node, el)
const background = el.getAttribute('fill')
const shouldAnim =
w.config.chart.animations.enabled &&
!w.globals.resized &&
!w.globals.dataChanged
if (shouldAnim) {
elRect.animate().attr({ fill: background })
} else {
elRect.attr({ fill: background })
}
el.setAttribute('fill', w.config.dataLabels.background.foreColor)
}
}
}
bringForward() {
const w = this.w
const elDataLabelsNodes = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-datalabels'
)
const elSeries = w.globals.dom.baseEl.querySelector(
'.apexcharts-plot-series:last-child'
)
for (let i = 0; i < elDataLabelsNodes.length; i++) {
if (elSeries) {
elSeries.insertBefore(elDataLabelsNodes[i], elSeries.nextSibling)
}
}
}
}
export default DataLabels
+118
View File
@@ -0,0 +1,118 @@
import Utils from '../utils/Utils'
export default class Events {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.documentEvent = Utils.bind(this.documentEvent, this)
}
addEventListener(name, handler) {
const w = this.w
if (w.globals.events.hasOwnProperty(name)) {
w.globals.events[name].push(handler)
} else {
w.globals.events[name] = [handler]
}
}
removeEventListener(name, handler) {
const w = this.w
if (!w.globals.events.hasOwnProperty(name)) {
return
}
let index = w.globals.events[name].indexOf(handler)
if (index !== -1) {
w.globals.events[name].splice(index, 1)
}
}
fireEvent(name, args) {
const w = this.w
if (!w.globals.events.hasOwnProperty(name)) {
return
}
if (!args || !args.length) {
args = []
}
let evs = w.globals.events[name]
let l = evs.length
for (let i = 0; i < l; i++) {
evs[i].apply(null, args)
}
}
setupEventHandlers() {
const w = this.w
const me = this.ctx
let clickableArea = w.globals.dom.baseEl.querySelector(w.globals.chartClass)
this.ctx.eventList.forEach((event) => {
clickableArea.addEventListener(
event,
(e) => {
const opts = Object.assign({}, w, {
seriesIndex: w.globals.capturedSeriesIndex,
dataPointIndex: w.globals.capturedDataPointIndex
})
if (e.type === 'mousemove' || e.type === 'touchmove') {
if (typeof w.config.chart.events.mouseMove === 'function') {
w.config.chart.events.mouseMove(e, me, opts)
}
} else if (e.type === 'mouseleave' || e.type === 'touchleave') {
if (typeof w.config.chart.events.mouseLeave === 'function') {
w.config.chart.events.mouseLeave(e, me, opts)
}
} else if (
(e.type === 'mouseup' && e.which === 1) ||
e.type === 'touchend'
) {
if (typeof w.config.chart.events.click === 'function') {
w.config.chart.events.click(e, me, opts)
}
me.ctx.events.fireEvent('click', [e, me, opts])
}
},
{ capture: false, passive: true }
)
})
this.ctx.eventList.forEach((event) => {
w.globals.dom.baseEl.addEventListener(event, this.documentEvent, {
passive: true
})
})
this.ctx.core.setupBrushHandler()
}
documentEvent(e) {
const w = this.w
const target = e.target.className
if (e.type === 'click') {
let elMenu = w.globals.dom.baseEl.querySelector('.apexcharts-menu')
if (
elMenu &&
elMenu.classList.contains('apexcharts-menu-open') &&
target !== 'apexcharts-menu-icon'
) {
elMenu.classList.remove('apexcharts-menu-open')
}
}
w.globals.clientX =
e.type === 'touchmove' ? e.touches[0].clientX : e.clientX
w.globals.clientY =
e.type === 'touchmove' ? e.touches[0].clientY : e.clientY
}
}
+465
View File
@@ -0,0 +1,465 @@
import Data from '../modules/Data'
import AxesUtils from '../modules/axes/AxesUtils'
import Series from '../modules/Series'
import Utils from '../utils/Utils'
class Exports {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
scaleSvgNode(svg, scale) {
// get current both width and height of the svg
let svgWidth = parseFloat(svg.getAttributeNS(null, 'width'))
let svgHeight = parseFloat(svg.getAttributeNS(null, 'height'))
// set new width and height based on the scale
svg.setAttributeNS(null, 'width', svgWidth * scale)
svg.setAttributeNS(null, 'height', svgHeight * scale)
svg.setAttributeNS(null, 'viewBox', '0 0 ' + svgWidth + ' ' + svgHeight)
}
fixSvgStringForIe11(svgData) {
// IE11 generates broken SVG that we have to fix by using regex
if (!Utils.isIE11()) {
// not IE11 - noop
return svgData.replace(/&nbsp;/g, '&#160;')
}
// replace second occurrence of "xmlns" attribute with "xmlns:xlink" with correct url + add xmlns:svgjs
let nXmlnsSeen = 0
let result = svgData.replace(
/xmlns="http:\/\/www.w3.org\/2000\/svg"/g,
(match) => {
nXmlnsSeen++
return nXmlnsSeen === 2
? 'xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev"'
: match
}
)
// remove the invalid empty namespace declarations
result = result.replace(/xmlns:NS\d+=""/g, '')
// remove these broken namespaces from attributes
result = result.replace(/NS\d+:(\w+:\w+=")/g, '$1')
return result
}
getSvgString(scale) {
if (scale == undefined) {
scale = 1 // if no scale is specified, don't scale...
}
let svgString = this.w.globals.dom.Paper.svg()
// in case the scale is different than 1, the svg needs to be rescaled
if (scale !== 1) {
// clone the svg node so it remains intact in the UI
const svgNode = this.w.globals.dom.Paper.node.cloneNode(true)
// scale the image
this.scaleSvgNode(svgNode, scale)
// get the string representation of the svgNode
svgString = new XMLSerializer().serializeToString(svgNode)
}
return this.fixSvgStringForIe11(svgString)
}
cleanup() {
const w = this.w
// hide some elements to avoid printing them on exported svg
const xcrosshairs = w.globals.dom.baseEl.getElementsByClassName(
'apexcharts-xcrosshairs'
)
const ycrosshairs = w.globals.dom.baseEl.getElementsByClassName(
'apexcharts-ycrosshairs'
)
const zoomSelectionRects = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-zoom-rect, .apexcharts-selection-rect'
)
Array.prototype.forEach.call(zoomSelectionRects, (z) => {
z.setAttribute('width', 0)
})
if (xcrosshairs && xcrosshairs[0]) {
xcrosshairs[0].setAttribute('x', -500)
xcrosshairs[0].setAttribute('x1', -500)
xcrosshairs[0].setAttribute('x2', -500)
}
if (ycrosshairs && ycrosshairs[0]) {
ycrosshairs[0].setAttribute('y', -100)
ycrosshairs[0].setAttribute('y1', -100)
ycrosshairs[0].setAttribute('y2', -100)
}
}
svgUrl() {
this.cleanup()
const svgData = this.getSvgString()
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
return URL.createObjectURL(svgBlob)
}
dataURI(options) {
return new Promise((resolve) => {
const w = this.w
const scale = options
? options.scale || options.width / w.globals.svgWidth
: 1
this.cleanup()
const canvas = document.createElement('canvas')
canvas.width = w.globals.svgWidth * scale
canvas.height = parseInt(w.globals.dom.elWrap.style.height, 10) * scale // because of resizeNonAxisCharts
const canvasBg =
w.config.chart.background === 'transparent'
? '#fff'
: w.config.chart.background
let ctx = canvas.getContext('2d')
ctx.fillStyle = canvasBg
ctx.fillRect(0, 0, canvas.width * scale, canvas.height * scale)
const svgData = this.getSvgString(scale)
if (window.canvg && Utils.isIE11()) {
// use canvg as a polyfill to workaround ie11 considering a canvas with loaded svg 'unsafe'
// without ignoreClear we lose our background color; without ignoreDimensions some grid lines become invisible
let v = window.canvg.Canvg.fromString(ctx, svgData, {
ignoreClear: true,
ignoreDimensions: true,
})
// render the svg to canvas
v.start()
let blob = canvas.msToBlob()
// dispose - missing this will cause a memory leak
v.stop()
resolve({ blob })
} else {
const svgUrl = 'data:image/svg+xml,' + encodeURIComponent(svgData)
let img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
ctx.drawImage(img, 0, 0)
if (canvas.msToBlob) {
// IE and Edge can't navigate to data urls, so we return the blob instead
let blob = canvas.msToBlob()
resolve({ blob })
} else {
let imgURI = canvas.toDataURL('image/png')
resolve({ imgURI })
}
}
img.src = svgUrl
}
})
}
exportToSVG() {
this.triggerDownload(
this.svgUrl(),
this.w.config.chart.toolbar.export.svg.filename,
'.svg'
)
}
exportToPng() {
this.dataURI().then(({ imgURI, blob }) => {
if (blob) {
navigator.msSaveOrOpenBlob(blob, this.w.globals.chartID + '.png')
} else {
this.triggerDownload(
imgURI,
this.w.config.chart.toolbar.export.png.filename,
'.png'
)
}
})
}
exportToCSV({
series,
fileName,
columnDelimiter = ',',
lineDelimiter = '\n',
}) {
const w = this.w
if (!series) series = w.config.series
let columns = []
let rows = []
let result = ''
let universalBOM = '\uFEFF'
let gSeries = w.globals.series.map((s, i) => {
return w.globals.collapsedSeriesIndices.indexOf(i) === -1 ? s : []
})
const isTimeStamp = (num) => {
return w.config.xaxis.type === 'datetime' && String(num).length >= 10
}
const seriesMaxDataLength = Math.max(
...series.map((s) => {
return s.data ? s.data.length : 0
})
)
const dataFormat = new Data(this.ctx)
const axesUtils = new AxesUtils(this.ctx)
const getCat = (i) => {
let cat = ''
// pie / donut/ radial
if (!w.globals.axisCharts) {
cat = w.config.labels[i]
} else {
// xy charts
// non datetime
if (
w.config.xaxis.type === 'category' ||
w.config.xaxis.convertedCatToNumeric
) {
if (w.globals.isBarHorizontal) {
let lbFormatter = w.globals.yLabelFormatters[0]
let sr = new Series(this.ctx)
let activeSeries = sr.getActiveConfigSeriesIndex()
cat = lbFormatter(w.globals.labels[i], {
seriesIndex: activeSeries,
dataPointIndex: i,
w,
})
} else {
cat = axesUtils.getLabel(
w.globals.labels,
w.globals.timescaleLabels,
0,
i
).text
}
}
// datetime, but labels specified in categories or labels
if (w.config.xaxis.type === 'datetime') {
if (w.config.xaxis.categories.length) {
cat = w.config.xaxis.categories[i]
} else if (w.config.labels.length) {
cat = w.config.labels[i]
}
}
}
if (Array.isArray(cat)) {
cat = cat.join(' ')
}
return Utils.isNumber(cat) ? cat : cat.split(columnDelimiter).join('')
}
// Fix https://github.com/apexcharts/apexcharts.js/issues/3365
const getEmptyDataForCsvColumn = () => {
return [...Array(seriesMaxDataLength)].map(() => '')
}
const handleAxisRowsColumns = (s, sI) => {
if (columns.length && sI === 0) {
// It's the first series. Go ahead and create the first row with header information.
rows.push(columns.join(columnDelimiter))
}
if (s.data) {
// Use the data we have, or generate a properly sized empty array with empty data if some data is missing.
s.data = (s.data.length && s.data) || getEmptyDataForCsvColumn()
for (let i = 0; i < s.data.length; i++) {
// Reset the columns array so that we can start building columns for this row.
columns = []
let cat = getCat(i)
if (!cat) {
if (dataFormat.isFormatXY()) {
cat = series[sI].data[i].x
} else if (dataFormat.isFormat2DArray()) {
cat = series[sI].data[i] ? series[sI].data[i][0] : ''
}
}
if (sI === 0) {
// It's the first series. Also handle the category.
columns.push(
isTimeStamp(cat)
? w.config.chart.toolbar.export.csv.dateFormatter(cat)
: Utils.isNumber(cat)
? cat
: cat.split(columnDelimiter).join('')
)
for (let ci = 0; ci < w.globals.series.length; ci++) {
if (dataFormat.isFormatXY()) {
columns.push(series[ci].data[i]?.y)
} else {
columns.push(gSeries[ci][i])
}
}
}
if (
w.config.chart.type === 'candlestick' ||
(s.type && s.type === 'candlestick')
) {
columns.pop()
columns.push(w.globals.seriesCandleO[sI][i])
columns.push(w.globals.seriesCandleH[sI][i])
columns.push(w.globals.seriesCandleL[sI][i])
columns.push(w.globals.seriesCandleC[sI][i])
}
if (
w.config.chart.type === 'boxPlot' ||
(s.type && s.type === 'boxPlot')
) {
columns.pop()
columns.push(w.globals.seriesCandleO[sI][i])
columns.push(w.globals.seriesCandleH[sI][i])
columns.push(w.globals.seriesCandleM[sI][i])
columns.push(w.globals.seriesCandleL[sI][i])
columns.push(w.globals.seriesCandleC[sI][i])
}
if (w.config.chart.type === 'rangeBar') {
columns.pop()
columns.push(w.globals.seriesRangeStart[sI][i])
columns.push(w.globals.seriesRangeEnd[sI][i])
}
if (columns.length) {
rows.push(columns.join(columnDelimiter))
}
}
}
}
const handleUnequalXValues = () => {
const categories = new Set()
const data = {}
series.forEach((s, sI) => {
s?.data.forEach((dataItem) => {
let cat, value
if (dataFormat.isFormatXY()) {
cat = dataItem.x
value = dataItem.y
} else if (dataFormat.isFormat2DArray()) {
cat = dataItem[0]
value = dataItem[1]
} else {
return
}
if (!data[cat]) {
data[cat] = Array(series.length).fill('')
}
data[cat][sI] = value
categories.add(cat)
})
})
if (columns.length) {
rows.push(columns.join(columnDelimiter))
}
Array.from(categories)
.sort()
.forEach((cat) => {
rows.push([
isTimeStamp(cat) && w.config.xaxis.type === 'datetime'
? w.config.chart.toolbar.export.csv.dateFormatter(cat)
: Utils.isNumber(cat)
? cat
: cat.split(columnDelimiter).join(''),
data[cat].join(columnDelimiter),
])
})
}
columns.push(w.config.chart.toolbar.export.csv.headerCategory)
if (w.config.chart.type === 'boxPlot') {
columns.push('minimum')
columns.push('q1')
columns.push('median')
columns.push('q3')
columns.push('maximum')
} else if (w.config.chart.type === 'candlestick') {
columns.push('open')
columns.push('high')
columns.push('low')
columns.push('close')
} else if (w.config.chart.type === 'rangeBar') {
columns.push('minimum')
columns.push('maximum')
} else {
series.map((s, sI) => {
const sname = (s.name ? s.name : `series-${sI}`) + ''
if (w.globals.axisCharts) {
columns.push(
sname.split(columnDelimiter).join('')
? sname.split(columnDelimiter).join('')
: `series-${sI}`
)
}
})
}
if (!w.globals.axisCharts) {
columns.push(w.config.chart.toolbar.export.csv.headerValue)
rows.push(columns.join(columnDelimiter))
}
if (
!w.globals.allSeriesHasEqualX &&
w.globals.axisCharts &&
!w.config.xaxis.categories.length &&
!w.config.labels.length
) {
handleUnequalXValues()
} else {
series.map((s, sI) => {
if (w.globals.axisCharts) {
handleAxisRowsColumns(s, sI)
} else {
columns = []
columns.push(w.globals.labels[sI].split(columnDelimiter).join(''))
columns.push(gSeries[sI])
rows.push(columns.join(columnDelimiter))
}
})
}
result += rows.join(lineDelimiter)
this.triggerDownload(
'data:text/csv; charset=utf-8,' +
encodeURIComponent(universalBOM + result),
fileName ? fileName : w.config.chart.toolbar.export.csv.filename,
'.csv'
)
}
triggerDownload(href, filename, ext) {
const downloadLink = document.createElement('a')
downloadLink.href = href
downloadLink.download = (filename ? filename : this.w.globals.chartID) + ext
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
}
}
export default Exports
+414
View File
@@ -0,0 +1,414 @@
import Graphics from './Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts Fill Class for setting fill options of the paths.
*
* @module Fill
**/
class Fill {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.opts = null
this.seriesIndex = 0
}
clippedImgArea(params) {
let w = this.w
let cnf = w.config
let svgW = parseInt(w.globals.gridWidth, 10)
let svgH = parseInt(w.globals.gridHeight, 10)
let size = svgW > svgH ? svgW : svgH
let 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
}
let elPattern = document.createElementNS(w.globals.SVGNS, 'pattern')
Graphics.setAttrs(elPattern, {
id: params.patternID,
patternUnits: params.patternUnits
? params.patternUnits
: 'userSpaceOnUse',
width: imgWidth + 'px',
height: imgHeight + 'px',
})
let elImage = document.createElementNS(w.globals.SVGNS, 'image')
elPattern.appendChild(elImage)
elImage.setAttributeNS(window.SVG.xlink, 'href', fillImg)
Graphics.setAttrs(elImage, {
x: 0,
y: 0,
preserveAspectRatio: 'none',
width: imgWidth + 'px',
height: imgHeight + 'px',
})
elImage.style.opacity = params.opacity
w.globals.dom.elDefs.node.appendChild(elPattern)
}
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.globals.series.length
}
return this.seriesIndex
}
fillPath(opts) {
let w = this.w
this.opts = opts
let cnf = this.w.config
let pathFill
let patternFill, gradientFill
this.seriesIndex = this.getSeriesIndex(opts)
let fillColors = this.getFillColors()
let fillColor = fillColors[this.seriesIndex]
//override fillcolor if user inputted color with data
if (w.globals.seriesColors[this.seriesIndex] !== undefined) {
fillColor = w.globals.seriesColors[this.seriesIndex]
}
if (typeof fillColor === 'function') {
fillColor = fillColor({
seriesIndex: this.seriesIndex,
dataPointIndex: opts.dataPointIndex,
value: opts.value,
w,
})
}
let fillType = opts.fillType
? opts.fillType
: this.getFillType(this.seriesIndex)
let fillOpacity = Array.isArray(cnf.fill.opacity)
? cnf.fill.opacity[this.seriesIndex]
: cnf.fill.opacity
if (opts.color) {
fillColor = opts.color
}
// in case a color is undefined, fallback to white color to prevent runtime error
if (!fillColor) {
fillColor = '#fff'
console.warn('undefined color - ApexCharts')
}
let defaultColor = fillColor
if (fillColor.indexOf('rgb') === -1) {
if (fillColor.length < 9) {
// if the hex contains alpha and is of 9 digit, skip the opacity
defaultColor = Utils.hexToRgba(fillColor, fillOpacity)
}
} else {
if (fillColor.indexOf('rgba') > -1) {
fillOpacity = Utils.getOpacityFromRGBA(fillColor)
}
}
if (opts.opacity) fillOpacity = opts.opacity
if (fillType === 'pattern') {
patternFill = this.handlePatternFill({
fillConfig: opts.fillConfig,
patternFill,
fillColor,
fillOpacity,
defaultColor,
})
}
if (fillType === 'gradient') {
gradientFill = this.handleGradientFill({
fillConfig: opts.fillConfig,
fillColor,
fillOpacity,
i: this.seriesIndex,
})
}
if (fillType === 'image') {
let imgSrc = cnf.fill.image.src
let patternID = opts.patternID ? opts.patternID : ''
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: `pattern${w.globals.cuid}${
opts.seriesNumber + 1
}${patternID}`,
})
pathFill = `url(#pattern${w.globals.cuid}${
opts.seriesNumber + 1
}${patternID})`
} else if (fillType === 'gradient') {
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
}
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 (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
}
handlePatternFill({
fillConfig,
patternFill,
fillColor,
fillOpacity,
defaultColor,
}) {
let fillCnf = this.w.config.fill
if (fillConfig) {
fillCnf = fillConfig
}
const opts = this.opts
let graphics = new Graphics(this.ctx)
let patternStrokeWidth = Array.isArray(fillCnf.pattern.strokeWidth)
? fillCnf.pattern.strokeWidth[this.seriesIndex]
: fillCnf.pattern.strokeWidth
let patternLineColor = fillColor
if (Array.isArray(fillCnf.pattern.style)) {
if (typeof fillCnf.pattern.style[opts.seriesNumber] !== 'undefined') {
let pf = graphics.drawPattern(
fillCnf.pattern.style[opts.seriesNumber],
fillCnf.pattern.width,
fillCnf.pattern.height,
patternLineColor,
patternStrokeWidth,
fillOpacity
)
patternFill = pf
} else {
patternFill = defaultColor
}
} else {
patternFill = graphics.drawPattern(
fillCnf.pattern.style,
fillCnf.pattern.width,
fillCnf.pattern.height,
patternLineColor,
patternStrokeWidth,
fillOpacity
)
}
return patternFill
}
handleGradientFill({ fillColor, fillOpacity, fillConfig, i }) {
let fillCnf = this.w.config.fill
if (fillConfig) {
fillCnf = {
...fillCnf,
...fillConfig,
}
}
const opts = this.opts
let graphics = new Graphics(this.ctx)
let utils = new Utils()
let 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) {
let 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,
fillCnf.gradient.colorStops,
i
)
}
}
export default Fill
+225
View File
@@ -0,0 +1,225 @@
import Utils from './../utils/Utils'
/**
* ApexCharts Filters Class for setting hover/active states on the paths.
*
* @module Formatters
**/
class Filters {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
// create a re-usable filter which can be appended other filter effects and applied to multiple elements
getDefaultFilter(el, i) {
const w = this.w
el.unfilter(true)
let filter = new window.SVG.Filter()
filter.size('120%', '180%', '-5%', '-40%')
if (w.config.states.normal.filter !== 'none') {
this.applyFilter(
el,
i,
w.config.states.normal.filter.type,
w.config.states.normal.filter.value
)
} else {
if (w.config.chart.dropShadow.enabled) {
this.dropShadow(el, w.config.chart.dropShadow, i)
}
}
}
addNormalFilter(el, i) {
const w = this.w
// revert shadow if it was there
// but, ignore marker as marker don't have dropshadow yet
if (
w.config.chart.dropShadow.enabled &&
!el.node.classList.contains('apexcharts-marker')
) {
this.dropShadow(el, w.config.chart.dropShadow, i)
}
}
// appends dropShadow to the filter object which can be chained with other filter effects
addLightenFilter(el, i, attrs) {
const w = this.w
const { intensity } = attrs
el.unfilter(true)
let filter = new window.SVG.Filter()
el.filter((add) => {
const shadowAttr = w.config.chart.dropShadow
if (shadowAttr.enabled) {
filter = this.addShadow(add, i, shadowAttr)
} else {
filter = add
}
filter.componentTransfer({
rgb: { type: 'linear', slope: 1.5, intercept: intensity },
})
})
el.filterer.node.setAttribute('filterUnits', 'userSpaceOnUse')
this._scaleFilterSize(el.filterer.node)
}
// appends dropShadow to the filter object which can be chained with other filter effects
addDarkenFilter(el, i, attrs) {
const w = this.w
const { intensity } = attrs
el.unfilter(true)
let filter = new window.SVG.Filter()
el.filter((add) => {
const shadowAttr = w.config.chart.dropShadow
if (shadowAttr.enabled) {
filter = this.addShadow(add, i, shadowAttr)
} else {
filter = add
}
filter.componentTransfer({
rgb: { type: 'linear', slope: intensity },
})
})
el.filterer.node.setAttribute('filterUnits', 'userSpaceOnUse')
this._scaleFilterSize(el.filterer.node)
}
applyFilter(el, i, filter, intensity = 0.5) {
switch (filter) {
case 'none': {
this.addNormalFilter(el, i)
break
}
case 'lighten': {
this.addLightenFilter(el, i, {
intensity,
})
break
}
case 'darken': {
this.addDarkenFilter(el, i, {
intensity,
})
break
}
default:
// do nothing
break
}
}
// appends dropShadow to the filter object which can be chained with other filter effects
addShadow(add, i, attrs) {
const w = this.w
const { blur, top, left, color, opacity } = attrs
if (w.config.chart.dropShadow.enabledOnSeries?.length > 0) {
if (w.config.chart.dropShadow.enabledOnSeries.indexOf(i) === -1) {
return add
}
}
let shadowBlur = add
.flood(Array.isArray(color) ? color[i] : color, opacity)
.composite(add.sourceAlpha, 'in')
.offset(left, top)
.gaussianBlur(blur)
.merge(add.source)
return add.blend(add.source, shadowBlur)
}
// directly adds dropShadow to the element and returns the same element.
// the only way it is different from the addShadow() function is that addShadow is chainable to other filters, while this function discards all filters and add dropShadow
dropShadow(el, attrs, i = 0) {
let { top, left, blur, color, opacity, noUserSpaceOnUse } = attrs
const w = this.w
el.unfilter(true)
if (Utils.isIE() && w.config.chart.type === 'radialBar') {
// in radialbar charts, dropshadow is clipping actual drawing in IE
return el
}
if (w.config.chart.dropShadow.enabledOnSeries?.length > 0) {
if (w.config.chart.dropShadow.enabledOnSeries?.indexOf(i) === -1) {
return el
}
}
color = Array.isArray(color) ? color[i] : color
el.filter((add) => {
let shadowBlur = null
if (Utils.isSafari() || Utils.isFirefox() || Utils.isIE()) {
// safari/firefox/IE have some alternative way to use this filter
shadowBlur = add
.flood(color, opacity)
.composite(add.sourceAlpha, 'in')
.offset(left, top)
.gaussianBlur(blur)
} else {
shadowBlur = add
.flood(color, opacity)
.composite(add.sourceAlpha, 'in')
.offset(left, top)
.gaussianBlur(blur)
.merge(add.source)
}
add.blend(add.source, shadowBlur)
})
if (!noUserSpaceOnUse) {
el.filterer.node.setAttribute('filterUnits', 'userSpaceOnUse')
}
this._scaleFilterSize(el.filterer.node)
return el
}
setSelectionFilter(el, realIndex, dataPointIndex) {
const w = this.w
if (typeof w.globals.selectedDataPoints[realIndex] !== 'undefined') {
if (
w.globals.selectedDataPoints[realIndex].indexOf(dataPointIndex) > -1
) {
el.node.setAttribute('selected', true)
let activeFilter = w.config.states.active.filter
if (activeFilter !== 'none') {
this.applyFilter(el, realIndex, activeFilter.type, activeFilter.value)
}
}
}
}
_scaleFilterSize(el) {
const setAttributes = (attrs) => {
for (let key in attrs) {
if (attrs.hasOwnProperty(key)) {
el.setAttribute(key, attrs[key])
}
}
}
setAttributes({
width: '200%',
height: '200%',
x: '-50%',
y: '-50%',
})
}
}
export default Filters
+182
View File
@@ -0,0 +1,182 @@
import DateTime from '../utils/DateTime'
import Utils from '../utils/Utils'
/**
* ApexCharts Formatter Class for setting value formatters for axes as well as tooltips.
*
* @module Formatters
**/
class Formatters {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.tooltipKeyFormat = 'dd MMM'
}
xLabelFormat(fn, val, timestamp, opts) {
let w = this.w
if (w.config.xaxis.type === 'datetime') {
if (w.config.xaxis.labels.formatter === undefined) {
// if user has not specified a custom formatter, use the default tooltip.x.format
if (w.config.tooltip.x.formatter === undefined) {
let datetimeObj = new DateTime(this.ctx)
return datetimeObj.formatDate(
datetimeObj.getDate(val),
w.config.tooltip.x.format
)
}
}
}
return fn(val, timestamp, opts)
}
defaultGeneralFormatter(val) {
if (Array.isArray(val)) {
return val.map((v) => {
return v
})
} else {
return val
}
}
defaultYFormatter(v, yaxe, i) {
let w = this.w
if (Utils.isNumber(v)) {
if (w.globals.yValueDecimal !== 0) {
v = v.toFixed(
yaxe.decimalsInFloat !== undefined
? yaxe.decimalsInFloat
: w.globals.yValueDecimal
)
} else if (w.globals.maxYArr[i] - w.globals.minYArr[i] < 5) {
v = v.toFixed(1)
} else {
v = v.toFixed(0)
}
}
return v
}
setLabelFormatters() {
let w = this.w
w.globals.xaxisTooltipFormatter = (val) => {
return this.defaultGeneralFormatter(val)
}
w.globals.ttKeyFormatter = (val) => {
return this.defaultGeneralFormatter(val)
}
w.globals.ttZFormatter = (val) => {
return val
}
w.globals.legendFormatter = (val) => {
return this.defaultGeneralFormatter(val)
}
// formatter function will always overwrite format property
if (w.config.xaxis.labels.formatter !== undefined) {
w.globals.xLabelFormatter = w.config.xaxis.labels.formatter
} else {
w.globals.xLabelFormatter = (val) => {
if (Utils.isNumber(val)) {
if (
!w.config.xaxis.convertedCatToNumeric &&
w.config.xaxis.type === 'numeric'
) {
if (Utils.isNumber(w.config.xaxis.decimalsInFloat)) {
return val.toFixed(w.config.xaxis.decimalsInFloat)
} else {
const diff = w.globals.maxX - w.globals.minX
if (diff > 0 && diff < 100) {
return val.toFixed(1)
}
return val.toFixed(0)
}
}
if (w.globals.isBarHorizontal) {
const range = w.globals.maxY - w.globals.minYArr
if (range < 4) {
return val.toFixed(1)
}
}
return val.toFixed(0)
}
return val
}
}
if (typeof w.config.tooltip.x.formatter === 'function') {
w.globals.ttKeyFormatter = w.config.tooltip.x.formatter
} else {
w.globals.ttKeyFormatter = w.globals.xLabelFormatter
}
if (typeof w.config.xaxis.tooltip.formatter === 'function') {
w.globals.xaxisTooltipFormatter = w.config.xaxis.tooltip.formatter
}
if (Array.isArray(w.config.tooltip.y)) {
w.globals.ttVal = w.config.tooltip.y
} else {
if (w.config.tooltip.y.formatter !== undefined) {
w.globals.ttVal = w.config.tooltip.y
}
}
if (w.config.tooltip.z.formatter !== undefined) {
w.globals.ttZFormatter = w.config.tooltip.z.formatter
}
// legend formatter - if user wants to append any global values of series to legend text
if (w.config.legend.formatter !== undefined) {
w.globals.legendFormatter = w.config.legend.formatter
}
// formatter function will always overwrite format property
w.config.yaxis.forEach((yaxe, i) => {
if (yaxe.labels.formatter !== undefined) {
w.globals.yLabelFormatters[i] = yaxe.labels.formatter
} else {
w.globals.yLabelFormatters[i] = (val) => {
if (!w.globals.xyCharts) return val
if (Array.isArray(val)) {
return val.map((v) => {
return this.defaultYFormatter(v, yaxe, i)
})
} else {
return this.defaultYFormatter(val, yaxe, i)
}
}
}
})
return w.globals
}
heatmapLabelFormatters() {
const w = this.w
if (w.config.chart.type === 'heatmap') {
w.globals.yAxisScale[0].result = w.globals.seriesNames.slice()
// get the longest string from the labels array and also apply label formatter to it
let longest = w.globals.seriesNames.reduce(
(a, b) => (a.length > b.length ? a : b),
0
)
w.globals.yAxisScale[0].niceMax = longest
w.globals.yAxisScale[0].niceMin = longest
}
}
}
export default Formatters
+1112
View File
File diff suppressed because it is too large Load Diff
+255
View File
@@ -0,0 +1,255 @@
import Filters from './Filters'
import Graphics from './Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts Markers Class for drawing points on y values in axes charts.
*
* @module Markers
**/
export default class Markers {
constructor(ctx, opts) {
this.ctx = ctx
this.w = ctx.w
}
setGlobalMarkerSize() {
const w = this.w
w.globals.markers.size = Array.isArray(w.config.markers.size)
? w.config.markers.size
: [w.config.markers.size]
if (w.globals.markers.size.length > 0) {
if (w.globals.markers.size.length < w.globals.series.length + 1) {
for (let i = 0; i <= w.globals.series.length; i++) {
if (typeof w.globals.markers.size[i] === 'undefined') {
w.globals.markers.size.push(w.globals.markers.size[0])
}
}
}
} else {
w.globals.markers.size = w.config.series.map((s) => w.config.markers.size)
}
}
plotChartMarkers(pointsPos, seriesIndex, j, pSize, alwaysDrawMarker = false) {
let w = this.w
let i = seriesIndex
let p = pointsPos
let elPointsWrap = null
let graphics = new Graphics(this.ctx)
let point
const hasDiscreteMarkers =
w.config.markers.discrete && w.config.markers.discrete.length
if (
w.globals.markers.size[seriesIndex] > 0 ||
alwaysDrawMarker ||
hasDiscreteMarkers
) {
elPointsWrap = graphics.group({
class:
alwaysDrawMarker || hasDiscreteMarkers
? ''
: 'apexcharts-series-markers',
})
elPointsWrap.attr(
'clip-path',
`url(#gridRectMarkerMask${w.globals.cuid})`
)
}
if (Array.isArray(p.x)) {
for (let q = 0; q < p.x.length; q++) {
let dataPointIndex = j
// a small hack as we have 2 points for the first val to connect it
if (j === 1 && q === 0) dataPointIndex = 0
if (j === 1 && q === 1) dataPointIndex = 1
let PointClasses = 'apexcharts-marker'
if (
(w.config.chart.type === 'line' || w.config.chart.type === 'area') &&
!w.globals.comboCharts &&
!w.config.tooltip.intersect
) {
PointClasses += ' no-pointer-events'
}
const shouldMarkerDraw = Array.isArray(w.config.markers.size)
? w.globals.markers.size[seriesIndex] > 0
: w.config.markers.size > 0
if (shouldMarkerDraw || alwaysDrawMarker || hasDiscreteMarkers) {
if (Utils.isNumber(p.y[q])) {
PointClasses += ` w${Utils.randomId()}`
} else {
PointClasses = 'apexcharts-nullpoint'
}
let opts = this.getMarkerConfig({
cssClass: PointClasses,
seriesIndex,
dataPointIndex,
})
if (w.config.series[i].data[dataPointIndex]) {
if (w.config.series[i].data[dataPointIndex].fillColor) {
opts.pointFillColor =
w.config.series[i].data[dataPointIndex].fillColor
}
if (w.config.series[i].data[dataPointIndex].strokeColor) {
opts.pointStrokeColor =
w.config.series[i].data[dataPointIndex].strokeColor
}
}
if (pSize) {
opts.pSize = pSize
}
if (
p.x[q] < 0 ||
p.x[q] > w.globals.gridWidth ||
p.y[q] < -w.globals.markers.largestSize ||
p.y[q] > w.globals.gridHeight + w.globals.markers.largestSize
) {
opts.pSize = 0
}
point = graphics.drawMarker(p.x[q], p.y[q], opts)
point.attr('rel', dataPointIndex)
point.attr('j', dataPointIndex)
point.attr('index', seriesIndex)
point.node.setAttribute('default-marker-size', opts.pSize)
const filters = new Filters(this.ctx)
filters.setSelectionFilter(point, seriesIndex, dataPointIndex)
this.addEvents(point)
if (elPointsWrap) {
elPointsWrap.add(point)
}
} else {
// dynamic array creation - multidimensional
if (typeof w.globals.pointsArray[seriesIndex] === 'undefined')
w.globals.pointsArray[seriesIndex] = []
w.globals.pointsArray[seriesIndex].push([p.x[q], p.y[q]])
}
}
}
return elPointsWrap
}
getMarkerConfig({
cssClass,
seriesIndex,
dataPointIndex = null,
finishRadius = null,
}) {
const w = this.w
let pStyle = this.getMarkerStyle(seriesIndex)
let pSize = w.globals.markers.size[seriesIndex]
const m = w.config.markers
// discrete markers is an option where user can specify a particular marker with different shape, size and color
if (dataPointIndex !== null && m.discrete.length) {
m.discrete.map((marker) => {
if (
marker.seriesIndex === seriesIndex &&
marker.dataPointIndex === dataPointIndex
) {
pStyle.pointStrokeColor = marker.strokeColor
pStyle.pointFillColor = marker.fillColor
pSize = marker.size
pStyle.pointShape = marker.shape
}
})
}
return {
pSize: finishRadius === null ? pSize : finishRadius,
pRadius: m.radius,
width: Array.isArray(m.width) ? m.width[seriesIndex] : m.width,
height: Array.isArray(m.height) ? m.height[seriesIndex] : m.height,
pointStrokeWidth: Array.isArray(m.strokeWidth)
? m.strokeWidth[seriesIndex]
: m.strokeWidth,
pointStrokeColor: pStyle.pointStrokeColor,
pointFillColor: pStyle.pointFillColor,
shape:
pStyle.pointShape ||
(Array.isArray(m.shape) ? m.shape[seriesIndex] : m.shape),
class: cssClass,
pointStrokeOpacity: Array.isArray(m.strokeOpacity)
? m.strokeOpacity[seriesIndex]
: m.strokeOpacity,
pointStrokeDashArray: Array.isArray(m.strokeDashArray)
? m.strokeDashArray[seriesIndex]
: m.strokeDashArray,
pointFillOpacity: Array.isArray(m.fillOpacity)
? m.fillOpacity[seriesIndex]
: m.fillOpacity,
seriesIndex,
}
}
addEvents(circle) {
const w = this.w
const graphics = new Graphics(this.ctx)
circle.node.addEventListener(
'mouseenter',
graphics.pathMouseEnter.bind(this.ctx, circle)
)
circle.node.addEventListener(
'mouseleave',
graphics.pathMouseLeave.bind(this.ctx, circle)
)
circle.node.addEventListener(
'mousedown',
graphics.pathMouseDown.bind(this.ctx, circle)
)
circle.node.addEventListener('click', w.config.markers.onClick)
circle.node.addEventListener('dblclick', w.config.markers.onDblClick)
circle.node.addEventListener(
'touchstart',
graphics.pathMouseDown.bind(this.ctx, circle),
{ passive: true }
)
}
getMarkerStyle(seriesIndex) {
let w = this.w
let colors = w.globals.markers.colors
let strokeColors =
w.config.markers.strokeColor || w.config.markers.strokeColors
let pointStrokeColor = Array.isArray(strokeColors)
? strokeColors[seriesIndex]
: strokeColors
let pointFillColor = Array.isArray(colors) ? colors[seriesIndex] : colors
return {
pointStrokeColor,
pointFillColor,
}
}
}
+605
View File
@@ -0,0 +1,605 @@
import Utils from '../utils/Utils'
import DateTime from '../utils/DateTime'
import Scales from './Scales'
/**
* Range is used to generates values between min and max.
*
* @module Range
**/
class Range {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.scales = new Scales(ctx)
}
init() {
this.setYRange()
this.setXRange()
this.setZRange()
}
getMinYMaxY(
startingIndex,
lowestY = Number.MAX_VALUE,
highestY = -Number.MAX_VALUE,
endingIndex = null
) {
const cnf = this.w.config
const gl = this.w.globals
let maxY = -Number.MAX_VALUE
let minY = Number.MIN_VALUE
if (endingIndex === null) {
endingIndex = startingIndex + 1
}
let firstXIndex = 0
let lastXIndex = 0
let seriesX = undefined
if (gl.seriesX.length >= endingIndex) {
seriesX = [...new Set([].concat(...gl.seriesX.slice(startingIndex, endingIndex)))]
firstXIndex = 0
lastXIndex = seriesX.length - 1
if (cnf.xaxis.min) {
for (
firstXIndex = 0;
firstXIndex < lastXIndex && seriesX[firstXIndex] <= cnf.xaxis.min;
firstXIndex++
) {}
}
if (cnf.xaxis.max) {
for (
;
lastXIndex > firstXIndex && seriesX[lastXIndex] >= cnf.xaxis.max;
lastXIndex--
) {}
}
}
let series = gl.series
let seriesMin = series
let seriesMax = series
if (cnf.chart.type === 'candlestick') {
seriesMin = gl.seriesCandleL
seriesMax = gl.seriesCandleH
} else if (cnf.chart.type === 'boxPlot') {
seriesMin = gl.seriesCandleO
seriesMax = gl.seriesCandleC
} else if (gl.isRangeData) {
seriesMin = gl.seriesRangeStart
seriesMax = gl.seriesRangeEnd
}
for (let i = startingIndex; i < endingIndex; i++) {
gl.dataPoints = Math.max(gl.dataPoints, series[i].length)
if (gl.categoryLabels.length) {
gl.dataPoints = gl.categoryLabels.filter(
(label) => typeof label !== 'undefined'
).length
}
if (
gl.labels.length &&
cnf.xaxis.type !== 'datetime' &&
gl.series.reduce((a, c) => a + c.length, 0) !== 0
) {
// the condition cnf.xaxis.type !== 'datetime' fixes #3897 and #3905
gl.dataPoints = Math.max(gl.dataPoints, gl.labels.length)
}
if (!seriesX) {
firstXIndex = 0
lastXIndex = gl.series[i].length
}
for (let j = firstXIndex; j <= lastXIndex; j++) {
let val = series[i][j]
if (val !== null && Utils.isNumber(val)) {
if (typeof seriesMax[i][j] !== 'undefined') {
maxY = Math.max(maxY, seriesMax[i][j])
lowestY = Math.min(lowestY, seriesMax[i][j])
}
if (typeof seriesMin[i][j] !== 'undefined') {
lowestY = Math.min(lowestY, seriesMin[i][j])
highestY = Math.max(highestY, seriesMin[i][j])
}
// These series arrays are dual purpose:
// Array : CandleO, CandleH, CandleM, CandleL, CandleC
// Candlestick: O H L C
// Boxplot : Min Q1 Median Q3 Max
switch (cnf.series[i].type) {
case 'candlestick': {
if (typeof gl.seriesCandleC[i][j] !== 'undefined') {
maxY = Math.max(maxY, gl.seriesCandleH[i][j])
lowestY = Math.min(lowestY, gl.seriesCandleL[i][j])
}
}
case 'boxPlot': {
if (typeof gl.seriesCandleC[i][j] !== 'undefined') {
maxY = Math.max(maxY, gl.seriesCandleC[i][j])
lowestY = Math.min(lowestY, gl.seriesCandleO[i][j])
}
}
}
// there is a combo chart and the specified series in not either candlestick, boxplot, or rangeArea/rangeBar; find the max there
if (
cnf.series[i].type &&
(cnf.series[i].type !== 'candlestick' &&
cnf.series[i].type !== 'boxPlot' &&
cnf.series[i].type !== 'rangeArea' &&
cnf.series[i].type !== 'rangeBar')
) {
maxY = Math.max(maxY, gl.series[i][j])
lowestY = Math.min(lowestY, gl.series[i][j])
}
highestY = maxY
if (
gl.seriesGoals[i] &&
gl.seriesGoals[i][j] &&
Array.isArray(gl.seriesGoals[i][j])
) {
gl.seriesGoals[i][j].forEach((g) => {
if (minY !== Number.MIN_VALUE) {
minY = Math.min(minY, g.value)
lowestY = minY
}
maxY = Math.max(maxY, g.value)
highestY = maxY
})
}
if (Utils.isFloat(val)) {
val = Utils.noExponents(val)
gl.yValueDecimal = Math.max(
gl.yValueDecimal,
val.toString().split('.')[1].length
)
}
if (minY > seriesMin[i][j] && seriesMin[i][j] < 0) {
minY = seriesMin[i][j]
}
} else {
gl.hasNullValues = true
}
}
}
if (
cnf.chart.type === 'rangeBar' &&
gl.seriesRangeStart.length &&
gl.isBarHorizontal
) {
minY = lowestY
}
if (cnf.chart.type === 'bar') {
if (minY < 0 && maxY < 0) {
// all negative values in a bar chart, hence make the max to 0
maxY = 0
}
if (minY === Number.MIN_VALUE) {
minY = 0
}
}
return {
minY,
maxY,
lowestY,
highestY,
}
}
setYRange() {
let gl = this.w.globals
let cnf = this.w.config
gl.maxY = -Number.MAX_VALUE
gl.minY = Number.MIN_VALUE
let lowestYInAllSeries = Number.MAX_VALUE
if (gl.isMultipleYAxis) {
// we need to get minY and maxY for multiple y axis
lowestYInAllSeries = Number.MAX_VALUE
for (let i = 0; i < gl.series.length; i++) {
const minYMaxYArr = this.getMinYMaxY(i)
gl.minYArr[i] = minYMaxYArr.lowestY
gl.maxYArr[i] = minYMaxYArr.highestY
lowestYInAllSeries = Math.min(lowestYInAllSeries, minYMaxYArr.lowestY)
}
}
// and then, get the minY and maxY from all series
const minYMaxY = this.getMinYMaxY(
0,
lowestYInAllSeries,
null,
gl.series.length
)
gl.minY = minYMaxY.lowestY
gl.maxY = minYMaxY.highestY
lowestYInAllSeries = minYMaxY.lowestY
if (cnf.chart.stacked) {
this._setStackedMinMax()
}
// if the numbers are too big, reduce the range
// for eg, if number is between 100000-110000, putting 0 as the lowest
// value is not so good idea. So change the gl.minY for
// line/area/scatter/candlesticks/boxPlot/vertical rangebar
if (
cnf.chart.type === 'line' ||
cnf.chart.type === 'area' ||
cnf.chart.type === 'scatter' ||
cnf.chart.type === 'candlestick' ||
cnf.chart.type === 'boxPlot' ||
(cnf.chart.type === 'rangeBar' && !gl.isBarHorizontal)
) {
if (
gl.minY === Number.MIN_VALUE &&
lowestYInAllSeries !== -Number.MAX_VALUE &&
lowestYInAllSeries !== gl.maxY // single value possibility
) {
gl.minY = lowestYInAllSeries
}
} else {
gl.minY = minYMaxY.minY
}
cnf.yaxis.forEach((yaxe, index) => {
// override all min/max values by user defined values (y axis)
if (yaxe.max !== undefined) {
if (typeof yaxe.max === 'number') {
gl.maxYArr[index] = yaxe.max
} else if (typeof yaxe.max === 'function') {
// fixes apexcharts.js/issues/2098
gl.maxYArr[index] = yaxe.max(
gl.isMultipleYAxis ? gl.maxYArr[index] : gl.maxY
)
}
// gl.maxY is for single y-axis chart, it will be ignored in multi-yaxis
gl.maxY = gl.maxYArr[index]
}
if (yaxe.min !== undefined) {
if (typeof yaxe.min === 'number') {
gl.minYArr[index] = yaxe.min
} else if (typeof yaxe.min === 'function') {
// fixes apexcharts.js/issues/2098
gl.minYArr[index] = yaxe.min(
gl.isMultipleYAxis
? gl.minYArr[index] === Number.MIN_VALUE
? 0
: gl.minYArr[index]
: gl.minY
)
}
// gl.minY is for single y-axis chart, it will be ignored in multi-yaxis
gl.minY = gl.minYArr[index]
}
})
// for horizontal bar charts, we need to check xaxis min/max as user may have specified there
if (gl.isBarHorizontal) {
const minmax = ['min', 'max']
minmax.forEach((m) => {
if (cnf.xaxis[m] !== undefined && typeof cnf.xaxis[m] === 'number') {
m === 'min' ? (gl.minY = cnf.xaxis[m]) : (gl.maxY = cnf.xaxis[m])
}
})
}
// for multi y-axis we need different scales for each
if (gl.isMultipleYAxis) {
this.scales.setMultipleYScales()
gl.minY = lowestYInAllSeries
gl.yAxisScale.forEach((scale, i) => {
gl.minYArr[i] = scale.niceMin
gl.maxYArr[i] = scale.niceMax
})
} else {
this.scales.setYScaleForIndex(0, gl.minY, gl.maxY)
gl.minY = gl.yAxisScale[0].niceMin
gl.maxY = gl.yAxisScale[0].niceMax
gl.minYArr[0] = gl.yAxisScale[0].niceMin
gl.maxYArr[0] = gl.yAxisScale[0].niceMax
}
return {
minY: gl.minY,
maxY: gl.maxY,
minYArr: gl.minYArr,
maxYArr: gl.maxYArr,
yAxisScale: gl.yAxisScale,
}
}
setXRange() {
let gl = this.w.globals
let cnf = this.w.config
const isXNumeric =
cnf.xaxis.type === 'numeric' ||
cnf.xaxis.type === 'datetime' ||
(cnf.xaxis.type === 'category' && !gl.noLabelsProvided) ||
gl.noLabelsProvided ||
gl.isXNumeric
const getInitialMinXMaxX = () => {
for (let i = 0; i < gl.series.length; i++) {
if (gl.labels[i]) {
for (let j = 0; j < gl.labels[i].length; j++) {
if (gl.labels[i][j] !== null && Utils.isNumber(gl.labels[i][j])) {
gl.maxX = Math.max(gl.maxX, gl.labels[i][j])
gl.initialMaxX = Math.max(gl.maxX, gl.labels[i][j])
gl.minX = Math.min(gl.minX, gl.labels[i][j])
gl.initialMinX = Math.min(gl.minX, gl.labels[i][j])
}
}
}
}
}
// minX maxX starts here
if (gl.isXNumeric) {
getInitialMinXMaxX()
}
if (gl.noLabelsProvided) {
if (cnf.xaxis.categories.length === 0) {
gl.maxX = gl.labels[gl.labels.length - 1]
gl.initialMaxX = gl.labels[gl.labels.length - 1]
gl.minX = 1
gl.initialMinX = 1
}
}
if (gl.isXNumeric || gl.noLabelsProvided || gl.dataFormatXNumeric) {
let ticks
if (cnf.xaxis.tickAmount === undefined) {
ticks = Math.round(gl.svgWidth / 150)
// no labels provided and total number of dataPoints is less than 30
if (cnf.xaxis.type === 'numeric' && gl.dataPoints < 30) {
ticks = gl.dataPoints - 1
}
// this check is for when ticks exceeds total datapoints and that would result in duplicate labels
if (ticks > gl.dataPoints && gl.dataPoints !== 0) {
ticks = gl.dataPoints - 1
}
} else if (cnf.xaxis.tickAmount === 'dataPoints') {
if (gl.series.length > 1) {
ticks = gl.series[gl.maxValsInArrayIndex].length - 1
}
if (gl.isXNumeric) {
ticks = gl.maxX - gl.minX - 1
}
} else {
ticks = cnf.xaxis.tickAmount
}
gl.xTickAmount = ticks
// override all min/max values by user defined values (x axis)
if (cnf.xaxis.max !== undefined && typeof cnf.xaxis.max === 'number') {
gl.maxX = cnf.xaxis.max
}
if (cnf.xaxis.min !== undefined && typeof cnf.xaxis.min === 'number') {
gl.minX = cnf.xaxis.min
}
// if range is provided, adjust the new minX
if (cnf.xaxis.range !== undefined) {
gl.minX = gl.maxX - cnf.xaxis.range
}
if (gl.minX !== Number.MAX_VALUE && gl.maxX !== -Number.MAX_VALUE) {
if (cnf.xaxis.convertedCatToNumeric && !gl.dataFormatXNumeric) {
let catScale = []
for (let i = gl.minX - 1; i < gl.maxX; i++) {
catScale.push(i + 1)
}
gl.xAxisScale = {
result: catScale,
niceMin: catScale[0],
niceMax: catScale[catScale.length - 1]
}
} else {
gl.xAxisScale = this.scales.setXScale(gl.minX, gl.maxX)
}
} else {
gl.xAxisScale = this.scales.linearScale(
0,
ticks,
ticks,
0,
cnf.xaxis.stepSize
)
if (gl.noLabelsProvided && gl.labels.length > 0) {
gl.xAxisScale = this.scales.linearScale(
1,
gl.labels.length,
ticks - 1,
0,
cnf.xaxis.stepSize
)
// this is the only place seriesX is again mutated
gl.seriesX = gl.labels.slice()
}
}
// we will still store these labels as the count for this will be different (to draw grid and labels placement)
if (isXNumeric) {
gl.labels = gl.xAxisScale.result.slice()
}
}
if (gl.isBarHorizontal && gl.labels.length) {
gl.xTickAmount = gl.labels.length
}
// single dataPoint
this._handleSingleDataPoint()
// minimum x difference to calculate bar width in numeric bars
this._getMinXDiff()
return {
minX: gl.minX,
maxX: gl.maxX,
}
}
setZRange() {
// minZ, maxZ starts here
let gl = this.w.globals
if (!gl.isDataXYZ) return
for (let i = 0; i < gl.series.length; i++) {
if (typeof gl.seriesZ[i] !== 'undefined') {
for (let j = 0; j < gl.seriesZ[i].length; j++) {
if (gl.seriesZ[i][j] !== null && Utils.isNumber(gl.seriesZ[i][j])) {
gl.maxZ = Math.max(gl.maxZ, gl.seriesZ[i][j])
gl.minZ = Math.min(gl.minZ, gl.seriesZ[i][j])
}
}
}
}
}
_handleSingleDataPoint() {
const gl = this.w.globals
const cnf = this.w.config
if (gl.minX === gl.maxX) {
let datetimeObj = new DateTime(this.ctx)
if (cnf.xaxis.type === 'datetime') {
const newMinX = datetimeObj.getDate(gl.minX)
if (cnf.xaxis.labels.datetimeUTC) {
newMinX.setUTCDate(newMinX.getUTCDate() - 2)
} else {
newMinX.setDate(newMinX.getDate() - 2)
}
gl.minX = new Date(newMinX).getTime()
const newMaxX = datetimeObj.getDate(gl.maxX)
if (cnf.xaxis.labels.datetimeUTC) {
newMaxX.setUTCDate(newMaxX.getUTCDate() + 2)
} else {
newMaxX.setDate(newMaxX.getDate() + 2)
}
gl.maxX = new Date(newMaxX).getTime()
} else if (
cnf.xaxis.type === 'numeric' ||
(cnf.xaxis.type === 'category' && !gl.noLabelsProvided)
) {
gl.minX = gl.minX - 2
gl.initialMinX = gl.minX
gl.maxX = gl.maxX + 2
gl.initialMaxX = gl.maxX
}
}
}
_getMinXDiff() {
const gl = this.w.globals
if (gl.isXNumeric) {
// get the least x diff if numeric x axis is present
gl.seriesX.forEach((sX, i) => {
if (sX.length === 1) {
// a small hack to prevent overlapping multiple bars when there is just 1 datapoint in bar series.
// fix #811
sX.push(
gl.seriesX[gl.maxValsInArrayIndex][
gl.seriesX[gl.maxValsInArrayIndex].length - 1
]
)
}
// fix #983 (clone the array to avoid side effects)
const seriesX = sX.slice()
seriesX.sort((a, b) => a - b)
seriesX.forEach((s, j) => {
if (j > 0) {
let xDiff = s - seriesX[j - 1]
if (xDiff > 0) {
gl.minXDiff = Math.min(xDiff, gl.minXDiff)
}
}
})
if (gl.dataPoints === 1 || gl.minXDiff === Number.MAX_VALUE) {
// fixes apexcharts.js #1221
gl.minXDiff = 0.5
}
})
}
}
_setStackedMinMax() {
const gl = this.w.globals
// for stacked charts, we calculate each series's parallel values. i.e, series[0][j] + series[1][j] .... [series[i.length][j]] and get the max out of it
if (!gl.series.length) return
let seriesGroups = gl.seriesGroups
if (!seriesGroups.length) {
seriesGroups = [this.w.config.series.map((serie) => serie.name)]
}
let stackedPoss = {}
let stackedNegs = {}
seriesGroups.forEach((group) => {
stackedPoss[group] = []
stackedNegs[group] = []
const indicesOfSeriesInGroup = this.w.config.series
.map((serie, si) => (group.indexOf(serie.name) > -1 ? si : null))
.filter((f) => f !== null)
indicesOfSeriesInGroup.forEach((i) => {
for (let j = 0; j < gl.series[gl.maxValsInArrayIndex].length; j++) {
if (typeof stackedPoss[group][j] === 'undefined') {
stackedPoss[group][j] = 0
stackedNegs[group][j] = 0
}
let stackSeries =
(this.w.config.chart.stacked && !gl.comboCharts) ||
(this.w.config.chart.stacked &&
gl.comboCharts &&
(!this.w.config.chart.stackOnlyBar ||
this.w.config.series?.[i]?.type === 'bar'))
if (stackSeries) {
if (gl.series[i][j] !== null && Utils.isNumber(gl.series[i][j])) {
gl.series[i][j] > 0
? (stackedPoss[group][j] +=
parseFloat(gl.series[i][j]) + 0.0001)
: (stackedNegs[group][j] += parseFloat(gl.series[i][j]))
}
}
}
})
})
Object.entries(stackedPoss).forEach(([key]) => {
stackedPoss[key].forEach((_, stgi) => {
gl.maxY = Math.max(gl.maxY, stackedPoss[key][stgi])
gl.minY = Math.min(gl.minY, stackedNegs[key][stgi])
})
})
}
}
export default Range
+73
View File
@@ -0,0 +1,73 @@
import Config from './settings/Config'
import Utils from '../utils/Utils'
import CoreUtils from './CoreUtils'
/**
* ApexCharts Responsive Class to override options for different screen sizes.
*
* @module Responsive
**/
export default class Responsive {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
// the opts parameter if not null has to be set overriding everything
// as the opts is set by user externally
checkResponsiveConfig(opts) {
const w = this.w
const cnf = w.config
// check if responsive config exists
if (cnf.responsive.length === 0) return
let res = cnf.responsive.slice()
res
.sort((a, b) =>
a.breakpoint > b.breakpoint ? 1 : b.breakpoint > a.breakpoint ? -1 : 0
)
.reverse()
let config = new Config({})
const iterateResponsiveOptions = (newOptions = {}) => {
let largestBreakpoint = res[0].breakpoint
const width = window.innerWidth > 0 ? window.innerWidth : screen.width
if (width > largestBreakpoint) {
let options = CoreUtils.extendArrayProps(
config,
w.globals.initialConfig,
w
)
newOptions = Utils.extend(options, newOptions)
newOptions = Utils.extend(w.config, newOptions)
this.overrideResponsiveOptions(newOptions)
} else {
for (let i = 0; i < res.length; i++) {
if (width < res[i].breakpoint) {
newOptions = CoreUtils.extendArrayProps(config, res[i].options, w)
newOptions = Utils.extend(w.config, newOptions)
this.overrideResponsiveOptions(newOptions)
}
}
}
}
if (opts) {
let options = CoreUtils.extendArrayProps(config, opts, w)
options = Utils.extend(w.config, options)
options = Utils.extend(options, opts)
iterateResponsiveOptions(options)
} else {
iterateResponsiveOptions({})
}
}
overrideResponsiveOptions(newOptions) {
let newConfig = new Config(newOptions).init({ responsiveOverride: true })
this.w.config = newConfig
}
}
+813
View File
@@ -0,0 +1,813 @@
import Utils from '../utils/Utils'
export default class Scales {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
// http://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
// This routine creates the Y axis values for a graph.
niceScale(yMin, yMax, index = 0) {
const jsPrecision = 1e-11 // JS precision errors
const w = this.w
const gl = w.globals
const xaxisCnf = w.config.xaxis
const yaxisCnf = w.config.yaxis[index]
let gotMin = yaxisCnf.min !== undefined && yaxisCnf.min !== null
let gotMax = yaxisCnf.max !== undefined && yaxisCnf.min !== null
let gotStepSize =
yaxisCnf.stepSize !== undefined && yaxisCnf.stepSize !== null
let gotTickAmount =
yaxisCnf.tickAmount !== undefined && yaxisCnf.tickAmount !== null
// The most ticks we can fit into the svg chart dimensions
const maxTicks =
((gl.isBarHorizontal ? gl.svgWidth : gl.svgHeight) - 100) / 15 // Guestimate
let ticks = gotTickAmount ? yaxisCnf.tickAmount : 10
// In case we have a multi axis chart:
// Ensure subsequent series start with the same tickAmount as series[0],
// because the tick lines are drawn based on series[0]. This does not
// override user defined options for any series.
if (gl.isMultipleYAxis && !gotTickAmount && gl.multiAxisTickAmount > 0) {
ticks = gl.multiAxisTickAmount
gotTickAmount = true
}
if (ticks === 'dataPoints') {
ticks = gl.dataPoints - 1
} else {
// Ensure ticks is an integer
ticks = Math.abs(Math.round(ticks))
}
if (
(yMin === Number.MIN_VALUE && yMax === 0) ||
(!Utils.isNumber(yMin) && !Utils.isNumber(yMax)) ||
(yMin === Number.MIN_VALUE && yMax === -Number.MAX_VALUE)
) {
// when all values are 0
yMin = 0
yMax = ticks
gl.allSeriesCollapsed = false
}
if (yMin > yMax) {
// if somehow due to some wrong config, user sent max less than min,
// adjust the min/max again
console.warn(
'axis.min cannot be greater than axis.max: swapping min and max'
)
let temp = yMax
yMax = yMin
yMin = temp
} else if (yMin === yMax) {
// If yMin and yMax are identical, then
// adjust the yMin and yMax values to actually
// make a graph. Also avoids division by zero errors.
yMin = yMin === 0 ? 0 : yMin - 1 // choose an integer in case yValueDecimals=0
yMax = yMax === 0 ? 2 : yMax + 1 // choose an integer in case yValueDecimals=0
}
// Calculate Min amd Max graphical labels and graph
// increments.
//
// Output will be an array of the Y axis values that
// encompass the Y values.
let result = []
if (ticks < 1) {
ticks = 1
}
let tiks = ticks
// Determine Range
let range = Math.abs(yMax - yMin)
if (yaxisCnf.forceNiceScale) {
// Snap min or max to zero if close
let proximityRatio = 0.15
if (!gotMin && yMin > 0 && yMin / range < proximityRatio) {
yMin = 0
gotMin = true
}
if (!gotMax && yMax < 0 && -yMax / range < proximityRatio) {
yMax = 0
gotMax = true
}
range = Math.abs(yMax - yMin)
}
// Calculate a pretty step value based on ticks
// Initial stepSize
let stepSize = range / tiks
let niceStep = stepSize
let mag = Math.floor(Math.log10(niceStep))
let magPow = Math.pow(10, mag)
// ceil() is used below in conjunction with the values populating
// niceScaleAllowedMagMsd[][] to ensure that (niceStep * tiks)
// produces a range that doesn't clip data points after stretching
// the raw range out a little to match the prospective new range.
let magMsd = Math.ceil(niceStep / magPow)
// See globals.js for info on what niceScaleAllowedMagMsd does
magMsd = gl.niceScaleAllowedMagMsd[gl.yValueDecimal === 0 ? 0 : 1][magMsd]
niceStep = magMsd * magPow
// Initial stepSize
stepSize = niceStep
// Get step value
if (
gl.isBarHorizontal &&
xaxisCnf.stepSize &&
xaxisCnf.type !== 'datetime'
) {
stepSize = xaxisCnf.stepSize
gotStepSize = true
} else if (gotStepSize) {
stepSize = yaxisCnf.stepSize
}
if (gotStepSize) {
if (yaxisCnf.forceNiceScale) {
// Check that given stepSize is sane with respect to the range.
//
// The user can, by setting forceNiceScale = true,
// define a stepSize that will be scaled to useful value before
// it's checked for consistency.
//
// If, for example, the range = 4 and the user defined stepSize = 8
// (or 8000 or 0.0008, etc), then stepSize is inapplicable as
// it is. Reducing it to 0.8 will fit with 5 ticks.
//
if (Math.round(Math.log10(stepSize)) != mag) {
let ref = range / ticks
while (stepSize < ref) {
stepSize *= 10
}
while (stepSize > ref) {
stepSize /= 10
}
}
}
}
// Start applying some rules
if (gotMin && gotMax) {
let crudeStep = range / tiks
// min and max (range) cannot be changed
if (gotTickAmount) {
if (gotStepSize) {
if (Utils.mod(range, stepSize) != 0) {
// stepSize conflicts with range
let gcdStep = Utils.getGCD(stepSize, crudeStep)
// gcdStep is a multiple of range because crudeStep is a multiple.
// gcdStep is also a multiple of stepSize, so it partially honoured
// All three could be equal, which would be very nice
// if the computed stepSize generates too many ticks they will be
// reduced later, unless the number is prime, in which case,
// the chart will display all of them or just one (plus the X axis)
// depending on svg dimensions. Setting forceNiceScale: true will force
// the display of at least the default number of ticks.
if (crudeStep / gcdStep < 10) {
stepSize = gcdStep
} else {
// stepSize conflicts and no reasonable adjustment, but must
// honour tickAmount
stepSize = crudeStep
}
} else {
// stepSize fits
if (Utils.mod(stepSize, crudeStep) == 0) {
// crudeStep is a multiple of stepSize, or vice versa
// we know crudeStep will generate tickAmount ticks
stepSize = crudeStep
} else {
// stepSize conflicts with tickAmount
// if the user is setting up a multi-axis chart and wants
// synced axis ticks then they should not define stepSize
// or ensure there is no conflict between any of their options
// on any axis.
crudeStep = stepSize
// De-prioritizing ticks from now on
gotTickAmount = false
}
}
} else {
// no user stepSize, honour ticks
stepSize = crudeStep
}
} else {
// default ticks in use
if (gotStepSize) {
if (Utils.mod(range, stepSize) == 0) {
// stepSize fits
crudeStep = stepSize
} else {
stepSize = crudeStep
}
} else {
// no user stepSize
tiks = Math.round(range / niceStep)
crudeStep = range / tiks
if (Utils.mod(range, stepSize) != 0) {
// stepSize doesn't fit
let gcdStep = Utils.getGCD(range, niceStep)
if (niceStep / gcdStep < 10) {
crudeStep = gcdStep
}
stepSize = crudeStep
} else {
// stepSize fits
crudeStep = stepSize
}
}
}
tiks = Math.round(range / stepSize)
} else {
// Snap range to ticks
if (!gotMin && !gotMax) {
if (gotTickAmount) {
// Allow a half-stepSize shift if series doesn't cross the X axis
// to ensure graph doesn't clip. Not if it does cross, in order
// to keep the 0 aligned with a grid line in multi axis charts.
let shift = stepSize / (yMax - yMin > yMax ? 1 : 2)
yMin = shift * Math.floor(yMin / shift)
yMax = yMin + stepSize * tiks
} else {
yMin = stepSize * Math.floor(yMin / stepSize)
yMax = stepSize * Math.ceil(yMax / stepSize)
}
} else if (gotMax) {
if (gotTickAmount) {
yMin = yMax - stepSize * tiks
} else {
yMin = stepSize * Math.floor(yMin / stepSize)
}
} else if (gotMin) {
if (gotTickAmount) {
yMax = yMin + stepSize * tiks
} else {
yMax = stepSize * Math.ceil(yMax / stepSize)
}
}
range = Math.abs(yMax - yMin)
// Final check and possible adjustment of stepSize to prevent
// overridding the user's min or max choice.
stepSize = Utils.getGCD(range, stepSize)
tiks = Math.round(range / stepSize)
}
// Shrinkwrap ticks to the range
if (!gotTickAmount && !(gotMin || gotMax)) {
tiks = Math.ceil((range - jsPrecision) / (stepSize + jsPrecision))
// No user tickAmount, or min or max, we are free to adjust to avoid a
// prime number. This helps when reducing ticks for small svg dimensions.
if (tiks > 16 && Utils.getPrimeFactors(tiks).length < 2) {
tiks++
}
}
// Record final tiks for use by other series that call niceScale().
// Note: some don't, like logarithmicScale(), etc.
if (gl.isMultipleYAxis && gl.multiAxisTickAmount == 0) {
gl.multiAxisTickAmount = tiks
}
if (
tiks > maxTicks &&
(!(gotTickAmount || gotStepSize) || yaxisCnf.forceNiceScale)
) {
// Reduce the number of ticks nicely if chart svg dimensions shrink too far.
// The reduced tick set should always be a subset of the full set.
//
// This following products of prime factors method works as follows:
// We compute the prime factors of the full tick count (tiks), then all the
// possible products of those factors in order from smallest to biggest,
// until we find a product P such that: tiks/P < maxTicks.
//
// Example:
// Computing products of the prime factors of 30.
//
// tiks | pf | 1 2 3 4 5 6 <-- compute order
// --------------------------------------------------
// 30 | 5 | 5 5 5 <-- Multiply all
// | 3 | 3 3 3 3 <-- primes in each
// | 2 | 2 2 2 <-- column = P
// --------------------------------------------------
// 15 10 6 5 2 1 <-- tiks/P
//
// tiks = 30 has prime factors [2, 3, 5]
// The loop below computes the products [2,3,5,6,15,30].
// The last product of P = 2*3*5 is skipped since 30/P = 1.
// This yields tiks/P = [15,10,6,5,2,1], checked in order until
// tiks/P < maxTicks.
//
// Pros:
// 1) The ticks in the reduced set are always members of the
// full set of ticks.
// Cons:
// 1) None: if tiks is prime, we get all or one, nothing between, so
// the worst case is to display all, which is the status quo. Really
// only a problem visually for larger tick numbers, say, > 7.
//
let pf = Utils.getPrimeFactors(tiks)
let last = pf.length - 1
let tt = tiks
reduceLoop: for (var xFactors = 0; xFactors < last; xFactors++) {
for (var lowest = 0; lowest <= last - xFactors; lowest++) {
let stop = Math.min(lowest + xFactors, last)
let t = tt
let div = 1
for (var next = lowest; next <= stop; next++) {
div *= pf[next]
}
t /= div
if (t < maxTicks) {
tt = t
break reduceLoop
}
}
}
// Only reduce tiks all the way down to 1 (increase stepSize to range)
// if forceNiceScale = true, to give the user the option if tiks is
// prime and > maxTicks, which may result in premature removal of all but
// the last tick. It will not be immediately obvious why that has occured.
if (tt === tiks && yaxisCnf.forceNiceScale) {
stepSize = range
} else {
stepSize = range / tt
}
}
// build Y label array.
let val = yMin - stepSize
// Ensure we don't under/over shoot due to JS precision errors.
// This also fixes (amongst others):
// https://github.com/apexcharts/apexcharts.js/issues/430
let err = stepSize * jsPrecision
do {
val += stepSize
result.push(Utils.stripNumber(val, 7))
} while (yMax - val > err)
return {
result,
niceMin: result[0],
niceMax: result[result.length - 1],
}
}
linearScale(yMin, yMax, ticks = 10, index = 0, step = undefined) {
let range = Math.abs(yMax - yMin)
ticks = this._adjustTicksForSmallRange(ticks, index, range)
if (ticks === 'dataPoints') {
ticks = this.w.globals.dataPoints - 1
}
if (!step) {
step = range / ticks
}
if (ticks === Number.MAX_VALUE) {
ticks = 5
step = 1
}
let result = []
let v = yMin
while (ticks >= 0) {
result.push(v)
v = v + step
ticks -= 1
}
return {
result,
niceMin: result[0],
niceMax: result[result.length - 1],
}
}
logarithmicScaleNice(yMin, yMax, base) {
// Basic validation to avoid for loop starting at -inf.
if (yMax <= 0) yMax = Math.max(yMin, base)
if (yMin <= 0) yMin = Math.min(yMax, base)
const logs = []
const logMax = Math.ceil(Math.log(yMax) / Math.log(base) + 1) // Get powers of base for our max and min
const logMin = Math.floor(Math.log(yMin) / Math.log(base))
for (let i = logMin; i < logMax; i++) {
logs.push(Math.pow(base, i))
}
return {
result: logs,
niceMin: logs[0],
niceMax: logs[logs.length - 1],
}
}
logarithmicScale(yMin, yMax, base) {
// Basic validation to avoid for loop starting at -inf.
if (yMax <= 0) yMax = Math.max(yMin, base)
if (yMin <= 0) yMin = Math.min(yMax, base)
const logs = []
// Get the logarithmic range.
const logMax = Math.log(yMax) / Math.log(base)
const logMin = Math.log(yMin) / Math.log(base)
// Get the exact logarithmic range.
// (This is the exact number of multiples of the base there are between yMin and yMax).
const logRange = logMax - logMin
// Round the logarithmic range to get the number of ticks we will create.
// If the chosen min/max values are multiples of each other WRT the base, this will be neat.
// If the chosen min/max aren't, we will at least still provide USEFUL ticks.
const ticks = Math.round(logRange)
// Get the logarithmic spacing between ticks.
const logTickSpacing = logRange / ticks
// Create as many ticks as there is range in the logs.
for (
let i = 0, logTick = logMin;
i < ticks;
i++, logTick += logTickSpacing
) {
logs.push(Math.pow(base, logTick))
}
// Add a final tick at the yMax.
logs.push(Math.pow(base, logMax))
return {
result: logs,
niceMin: yMin,
niceMax: yMax,
}
}
_adjustTicksForSmallRange(ticks, index, range) {
let newTicks = ticks
if (
typeof index !== 'undefined' &&
this.w.config.yaxis[index].labels.formatter &&
this.w.config.yaxis[index].tickAmount === undefined
) {
const formattedVal = Number(
this.w.config.yaxis[index].labels.formatter(1)
)
if (Utils.isNumber(formattedVal) && this.w.globals.yValueDecimal === 0) {
newTicks = Math.ceil(range)
}
}
return newTicks < ticks ? newTicks : ticks
}
setYScaleForIndex(index, minY, maxY) {
const gl = this.w.globals
const cnf = this.w.config
let y = gl.isBarHorizontal ? cnf.xaxis : cnf.yaxis[index]
if (typeof gl.yAxisScale[index] === 'undefined') {
gl.yAxisScale[index] = []
}
let diff = Math.abs(maxY - minY)
if (y.logarithmic && diff <= 5) {
gl.invalidLogScale = true
}
if (y.logarithmic && diff > 5) {
gl.allSeriesCollapsed = false
gl.yAxisScale[index] = y.forceNiceScale
? this.logarithmicScaleNice(minY, maxY, y.logBase)
: this.logarithmicScale(minY, maxY, y.logBase)
} else {
if (maxY === -Number.MAX_VALUE || !Utils.isNumber(maxY)) {
// no data in the chart. Either all series collapsed or user passed a blank array
gl.yAxisScale[index] = this.linearScale(
0,
10,
10,
index,
cnf.yaxis[index].stepSize
)
} else {
// there is some data. Turn off the allSeriesCollapsed flag
gl.allSeriesCollapsed = false
gl.yAxisScale[index] = this.niceScale(minY, maxY, index)
}
}
}
setXScale(minX, maxX) {
const w = this.w
const gl = w.globals
let diff = Math.abs(maxX - minX)
if (maxX === -Number.MAX_VALUE || !Utils.isNumber(maxX)) {
// no data in the chart. Either all series collapsed or user passed a blank array
gl.xAxisScale = this.linearScale(0, 10, 10)
} else {
gl.xAxisScale = this.linearScale(
minX,
maxX,
w.config.xaxis.tickAmount
? w.config.xaxis.tickAmount
: diff < 10 && diff > 1
? diff + 1
: 10,
0,
w.config.xaxis.stepSize
)
}
return gl.xAxisScale
}
setMultipleYScales() {
const gl = this.w.globals
const cnf = this.w.config
const minYArr = gl.minYArr.concat([])
const maxYArr = gl.maxYArr.concat([])
let scalesIndices = []
// here, we loop through the yaxis array and find the item which has "seriesName" property
cnf.yaxis.forEach((yaxe, i) => {
let index = i
cnf.series.forEach((s, si) => {
// if seriesName matches and that series is not collapsed, we use that scale
// fix issue #1215
// proceed even if si is in gl.collapsedSeriesIndices
if (s.name === yaxe.seriesName) {
index = si
if (i !== si) {
scalesIndices.push({
index: si,
similarIndex: i,
alreadyExists: true,
})
} else {
scalesIndices.push({
index: si,
})
}
}
})
let minY = minYArr[index]
let maxY = maxYArr[index]
this.setYScaleForIndex(i, minY, maxY)
})
this.sameScaleInMultipleAxes(minYArr, maxYArr, scalesIndices)
}
sameScaleInMultipleAxes(minYArr, maxYArr, scalesIndices) {
const cnf = this.w.config
const gl = this.w.globals
// we got the scalesIndices array in the above code, but we need to filter out the items which doesn't have same scales
let similarIndices = []
scalesIndices.forEach((scale) => {
if (scale.alreadyExists) {
if (typeof similarIndices[scale.index] === 'undefined') {
similarIndices[scale.index] = []
}
similarIndices[scale.index].push(scale.index)
similarIndices[scale.index].push(scale.similarIndex)
}
})
function intersect(a, b) {
return a.filter((value) => b.indexOf(value) !== -1)
}
gl.yAxisSameScaleIndices = similarIndices
similarIndices.forEach((si, i) => {
similarIndices.forEach((sj, j) => {
if (i !== j) {
if (intersect(si, sj).length > 0) {
similarIndices[i] = similarIndices[i].concat(similarIndices[j])
}
}
})
})
// then, we remove duplicates from the similarScale array
let uniqueSimilarIndices = similarIndices.map((item) => {
return item.filter((i, pos) => item.indexOf(i) === pos)
})
// sort further to remove whole duplicate arrays later
let sortedIndices = uniqueSimilarIndices.map((s) => s.sort())
// remove undefined items
similarIndices = similarIndices.filter((s) => !!s)
let indices = sortedIndices.slice()
let stringIndices = indices.map((ind) => JSON.stringify(ind))
indices = indices.filter(
(ind, p) => stringIndices.indexOf(JSON.stringify(ind)) === p
)
let sameScaleMinYArr = []
let sameScaleMaxYArr = []
minYArr.forEach((minYValue, yi) => {
indices.forEach((scale, i) => {
// we compare only the yIndex which exists in the indices array
if (scale.indexOf(yi) > -1) {
if (typeof sameScaleMinYArr[i] === 'undefined') {
sameScaleMinYArr[i] = []
sameScaleMaxYArr[i] = []
}
sameScaleMinYArr[i].push({
key: yi,
value: minYValue,
})
sameScaleMaxYArr[i].push({
key: yi,
value: maxYArr[yi],
})
}
})
})
let sameScaleMin = Array.apply(null, Array(indices.length)).map(
Number.prototype.valueOf,
Number.MIN_VALUE
)
let sameScaleMax = Array.apply(null, Array(indices.length)).map(
Number.prototype.valueOf,
-Number.MAX_VALUE
)
sameScaleMinYArr.forEach((s, i) => {
s.forEach((sc, j) => {
sameScaleMin[i] = Math.min(sc.value, sameScaleMin[i])
})
})
sameScaleMaxYArr.forEach((s, i) => {
s.forEach((sc, j) => {
sameScaleMax[i] = Math.max(sc.value, sameScaleMax[i])
})
})
minYArr.forEach((min, i) => {
sameScaleMaxYArr.forEach((s, si) => {
let minY = sameScaleMin[si]
let maxY = sameScaleMax[si]
if (cnf.chart.stacked) {
// for stacked charts, we need to add the values
maxY = 0
s.forEach((ind, k) => {
// fix incorrectly adjust y scale issue #1215
if (ind.value !== -Number.MAX_VALUE) {
maxY += ind.value
}
if (minY !== Number.MIN_VALUE) {
minY += sameScaleMinYArr[si][k].value
}
})
}
s.forEach((ind, k) => {
if (s[k].key === i) {
if (cnf.yaxis[i].min !== undefined) {
if (typeof cnf.yaxis[i].min === 'function') {
minY = cnf.yaxis[i].min(gl.minY)
} else {
minY = cnf.yaxis[i].min
}
}
if (cnf.yaxis[i].max !== undefined) {
if (typeof cnf.yaxis[i].max === 'function') {
maxY = cnf.yaxis[i].max(gl.maxY)
} else {
maxY = cnf.yaxis[i].max
}
}
this.setYScaleForIndex(i, minY, maxY)
}
})
})
})
}
// experimental feature which scales the y-axis to a min/max based on x-axis range
autoScaleY(ctx, yaxis, e) {
if (!ctx) {
ctx = this
}
const w = ctx.w
if (w.globals.isMultipleYAxis || w.globals.collapsedSeries.length) {
// The autoScale option for multiple y-axis is turned off as it leads to buggy behavior.
// Also, when a series is collapsed, it results in incorrect behavior. Hence turned it off for that too - fixes apexcharts.js#795
console.warn('autoScaleYaxis not supported in a multi-yaxis chart.')
return yaxis
}
const seriesX = w.globals.seriesX[0]
let isStacked = w.config.chart.stacked
yaxis.forEach((yaxe, yi) => {
let firstXIndex = 0
for (let xi = 0; xi < seriesX.length; xi++) {
if (seriesX[xi] >= e.xaxis.min) {
firstXIndex = xi
break
}
}
let initialMin = w.globals.minYArr[yi]
let initialMax = w.globals.maxYArr[yi]
let min, max
let stackedSer = w.globals.stackedSeriesTotals
w.globals.series.forEach((serie, sI) => {
let firstValue = serie[firstXIndex]
if (isStacked) {
firstValue = stackedSer[firstXIndex]
min = max = firstValue
stackedSer.forEach((y, yI) => {
if (seriesX[yI] <= e.xaxis.max && seriesX[yI] >= e.xaxis.min) {
if (y > max && y !== null) max = y
if (serie[yI] < min && serie[yI] !== null) min = serie[yI]
}
})
} else {
min = max = firstValue
serie.forEach((y, yI) => {
if (seriesX[yI] <= e.xaxis.max && seriesX[yI] >= e.xaxis.min) {
let valMin = y
let valMax = y
w.globals.series.forEach((wS, wSI) => {
if (y !== null) {
valMin = Math.min(wS[yI], valMin)
valMax = Math.max(wS[yI], valMax)
}
})
if (valMax > max && valMax !== null) max = valMax
if (valMin < min && valMin !== null) min = valMin
}
})
}
if (min === undefined && max === undefined) {
min = initialMin
max = initialMax
}
min *= min < 0 ? 1.1 : 0.9
max *= max < 0 ? 0.9 : 1.1
if (min === 0 && max === 0) {
min = -1
max = 1
}
if (max < 0 && max < initialMax) {
max = initialMax
}
if (min < 0 && min > initialMin) {
min = initialMin
}
if (yaxis.length > 1) {
yaxis[sI].min = yaxe.min === undefined ? min : yaxe.min
yaxis[sI].max = yaxe.max === undefined ? max : yaxe.max
} else {
yaxis[0].min = yaxe.min === undefined ? min : yaxe.min
yaxis[0].max = yaxe.max === undefined ? max : yaxe.max
}
})
})
return yaxis
}
}
+479
View File
@@ -0,0 +1,479 @@
import Graphics from './Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts Series Class for interaction with the Series of the chart.
*
* @module Series
**/
export default class Series {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.legendInactiveClass = 'legend-mouseover-inactive'
}
getAllSeriesEls() {
return this.w.globals.dom.baseEl.getElementsByClassName(`apexcharts-series`)
}
getSeriesByName(seriesName) {
return this.w.globals.dom.baseEl.querySelector(
`.apexcharts-inner .apexcharts-series[seriesName='${Utils.escapeString(
seriesName
)}']`
)
}
isSeriesHidden(seriesName) {
const targetElement = this.getSeriesByName(seriesName)
let realIndex = parseInt(targetElement.getAttribute('data:realIndex'), 10)
let isHidden = targetElement.classList.contains(
'apexcharts-series-collapsed'
)
return { isHidden, realIndex }
}
addCollapsedClassToSeries(elSeries, index) {
const w = this.w
function iterateOnAllCollapsedSeries(series) {
for (let cs = 0; cs < series.length; cs++) {
if (series[cs].index === index) {
elSeries.node.classList.add('apexcharts-series-collapsed')
}
}
}
iterateOnAllCollapsedSeries(w.globals.collapsedSeries)
iterateOnAllCollapsedSeries(w.globals.ancillaryCollapsedSeries)
}
toggleSeries(seriesName) {
let isSeriesHidden = this.isSeriesHidden(seriesName)
this.ctx.legend.legendHelpers.toggleDataSeries(
isSeriesHidden.realIndex,
isSeriesHidden.isHidden
)
return isSeriesHidden.isHidden
}
showSeries(seriesName) {
let isSeriesHidden = this.isSeriesHidden(seriesName)
if (isSeriesHidden.isHidden) {
this.ctx.legend.legendHelpers.toggleDataSeries(
isSeriesHidden.realIndex,
true
)
}
}
hideSeries(seriesName) {
let isSeriesHidden = this.isSeriesHidden(seriesName)
if (!isSeriesHidden.isHidden) {
this.ctx.legend.legendHelpers.toggleDataSeries(
isSeriesHidden.realIndex,
false
)
}
}
resetSeries(
shouldUpdateChart = true,
shouldResetZoom = true,
shouldResetCollapsed = true
) {
const w = this.w
let series = Utils.clone(w.globals.initialSeries)
w.globals.previousPaths = []
if (shouldResetCollapsed) {
w.globals.collapsedSeries = []
w.globals.ancillaryCollapsedSeries = []
w.globals.collapsedSeriesIndices = []
w.globals.ancillaryCollapsedSeriesIndices = []
} else {
series = this.emptyCollapsedSeries(series)
}
w.config.series = series
if (shouldUpdateChart) {
if (shouldResetZoom) {
w.globals.zoomed = false
this.ctx.updateHelpers.revertDefaultAxisMinMax()
}
this.ctx.updateHelpers._updateSeries(
series,
w.config.chart.animations.dynamicAnimation.enabled
)
}
}
emptyCollapsedSeries(series) {
const w = this.w
for (let i = 0; i < series.length; i++) {
if (w.globals.collapsedSeriesIndices.indexOf(i) > -1) {
series[i].data = []
}
}
return series
}
toggleSeriesOnHover(e, targetElement) {
const w = this.w
if (!targetElement) targetElement = e.target
let allSeriesEls = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-series, .apexcharts-datalabels`
)
if (e.type === 'mousemove') {
let seriesCnt = parseInt(targetElement.getAttribute('rel'), 10) - 1
let seriesEl = null
let dataLabelEl = null
if (w.globals.axisCharts || w.config.chart.type === 'radialBar') {
if (w.globals.axisCharts) {
seriesEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${seriesCnt}']`
)
dataLabelEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-datalabels[data\\:realIndex='${seriesCnt}']`
)
} else {
seriesEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[rel='${seriesCnt + 1}']`
)
}
} else {
seriesEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[rel='${seriesCnt + 1}'] path`
)
}
for (let se = 0; se < allSeriesEls.length; se++) {
allSeriesEls[se].classList.add(this.legendInactiveClass)
}
if (seriesEl !== null) {
if (!w.globals.axisCharts) {
seriesEl.parentNode.classList.remove(this.legendInactiveClass)
}
seriesEl.classList.remove(this.legendInactiveClass)
if (dataLabelEl !== null) {
dataLabelEl.classList.remove(this.legendInactiveClass)
}
}
} else if (e.type === 'mouseout') {
for (let se = 0; se < allSeriesEls.length; se++) {
allSeriesEls[se].classList.remove(this.legendInactiveClass)
}
}
}
highlightRangeInSeries(e, targetElement) {
const w = this.w
const allHeatMapElements = w.globals.dom.baseEl.getElementsByClassName(
'apexcharts-heatmap-rect'
)
const activeInactive = (action) => {
for (let i = 0; i < allHeatMapElements.length; i++) {
allHeatMapElements[i].classList[action](this.legendInactiveClass)
}
}
const removeInactiveClassFromHoveredRange = (range) => {
for (let i = 0; i < allHeatMapElements.length; i++) {
const val = parseInt(allHeatMapElements[i].getAttribute('val'), 10)
if (val >= range.from && val <= range.to) {
allHeatMapElements[i].classList.remove(this.legendInactiveClass)
}
}
}
if (e.type === 'mousemove') {
let seriesCnt = parseInt(targetElement.getAttribute('rel'), 10) - 1
activeInactive('add')
const range = w.config.plotOptions.heatmap.colorScale.ranges[seriesCnt]
removeInactiveClassFromHoveredRange(range)
} else if (e.type === 'mouseout') {
activeInactive('remove')
}
}
getActiveConfigSeriesIndex(order = 'asc', chartTypes = []) {
const w = this.w
let activeIndex = 0
if (w.config.series.length > 1) {
// active series flag is required to know if user has not deactivated via legend click
let activeSeriesIndex = w.config.series.map((s, index) => {
const checkChartType = () => {
if (w.globals.comboCharts) {
return (
chartTypes.length === 0 ||
(chartTypes.length &&
chartTypes.indexOf(w.config.series[index].type) > -1)
)
}
return true
}
const hasData =
s.data &&
s.data.length > 0 &&
w.globals.collapsedSeriesIndices.indexOf(index) === -1
return hasData && checkChartType() ? index : -1
})
for (
let a = order === 'asc' ? 0 : activeSeriesIndex.length - 1;
order === 'asc' ? a < activeSeriesIndex.length : a >= 0;
order === 'asc' ? a++ : a--
) {
if (activeSeriesIndex[a] !== -1) {
activeIndex = activeSeriesIndex[a]
break
}
}
}
return activeIndex
}
getBarSeriesIndices() {
const w = this.w
if (w.globals.comboCharts) {
return this.w.config.series
.map((s, i) => {
return s.type === 'bar' || s.type === 'column' ? i : -1
})
.filter((i) => {
return i !== -1
})
}
return this.w.config.series.map((s, i) => {
return i
})
}
getPreviousPaths() {
let w = this.w
w.globals.previousPaths = []
function pushPaths(seriesEls, i, type) {
let paths = seriesEls[i].childNodes
let dArr = {
type,
paths: [],
realIndex: seriesEls[i].getAttribute('data:realIndex')
}
for (let j = 0; j < paths.length; j++) {
if (paths[j].hasAttribute('pathTo')) {
let d = paths[j].getAttribute('pathTo')
dArr.paths.push({
d
})
}
}
w.globals.previousPaths.push(dArr)
}
const getPaths = (chartType) => {
return w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-${chartType}-series .apexcharts-series`
)
}
const chartTypes = [
'line',
'area',
'bar',
'rangebar',
'rangeArea',
'candlestick',
'radar'
]
chartTypes.forEach((type) => {
const paths = getPaths(type)
for (let p = 0; p < paths.length; p++) {
pushPaths(paths, p, type)
}
})
this.handlePrevBubbleScatterPaths('bubble')
this.handlePrevBubbleScatterPaths('scatter')
let heatTreeSeries = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-${w.config.chart.type} .apexcharts-series`
)
if (heatTreeSeries.length > 0) {
for (let h = 0; h < heatTreeSeries.length; h++) {
let seriesEls = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-${w.config.chart.type} .apexcharts-series[data\\:realIndex='${h}'] rect`
)
let dArr = []
for (let i = 0; i < seriesEls.length; i++) {
const getAttr = (x) => {
return seriesEls[i].getAttribute(x)
}
const rect = {
x: parseFloat(getAttr('x')),
y: parseFloat(getAttr('y')),
width: parseFloat(getAttr('width')),
height: parseFloat(getAttr('height'))
}
dArr.push({
rect,
color: seriesEls[i].getAttribute('color')
})
}
w.globals.previousPaths.push(dArr)
}
}
if (!w.globals.axisCharts) {
// for non-axis charts (i.e., circular charts, pathFrom is not usable. We need whole series)
w.globals.previousPaths = w.globals.series
}
}
handlePrevBubbleScatterPaths(type) {
const w = this.w
let paths = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-${type}-series .apexcharts-series`
)
if (paths.length > 0) {
for (let s = 0; s < paths.length; s++) {
let seriesEls = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-${type}-series .apexcharts-series[data\\:realIndex='${s}'] circle`
)
let dArr = []
for (let i = 0; i < seriesEls.length; i++) {
dArr.push({
x: seriesEls[i].getAttribute('cx'),
y: seriesEls[i].getAttribute('cy'),
r: seriesEls[i].getAttribute('r')
})
}
w.globals.previousPaths.push(dArr)
}
}
}
clearPreviousPaths() {
const w = this.w
w.globals.previousPaths = []
w.globals.allSeriesCollapsed = false
}
handleNoData() {
const w = this.w
const me = this
const noDataOpts = w.config.noData
const graphics = new Graphics(me.ctx)
let x = w.globals.svgWidth / 2
let y = w.globals.svgHeight / 2
let textAnchor = 'middle'
w.globals.noData = true
w.globals.animationEnded = true
if (noDataOpts.align === 'left') {
x = 10
textAnchor = 'start'
} else if (noDataOpts.align === 'right') {
x = w.globals.svgWidth - 10
textAnchor = 'end'
}
if (noDataOpts.verticalAlign === 'top') {
y = 50
} else if (noDataOpts.verticalAlign === 'bottom') {
y = w.globals.svgHeight - 50
}
x = x + noDataOpts.offsetX
y = y + parseInt(noDataOpts.style.fontSize, 10) + 2 + noDataOpts.offsetY
if (noDataOpts.text !== undefined && noDataOpts.text !== '') {
let titleText = graphics.drawText({
x,
y,
text: noDataOpts.text,
textAnchor,
fontSize: noDataOpts.style.fontSize,
fontFamily: noDataOpts.style.fontFamily,
foreColor: noDataOpts.style.color,
opacity: 1,
class: 'apexcharts-text-nodata'
})
w.globals.dom.Paper.add(titleText)
}
}
// When user clicks on legends, the collapsed series is filled with [0,0,0,...,0]
// This is because we don't want to alter the series' length as it is used at many places
setNullSeriesToZeroValues(series) {
let w = this.w
for (let sl = 0; sl < series.length; sl++) {
if (series[sl].length === 0) {
for (let j = 0; j < series[w.globals.maxValsInArrayIndex].length; j++) {
series[sl].push(0)
}
}
}
return series
}
hasAllSeriesEqualX() {
let equalLen = true
const w = this.w
const filteredSerX = this.filteredSeriesX()
for (let i = 0; i < filteredSerX.length - 1; i++) {
if (filteredSerX[i][0] !== filteredSerX[i + 1][0]) {
equalLen = false
break
}
}
w.globals.allSeriesHasEqualX = equalLen
return equalLen
}
filteredSeriesX() {
const w = this.w
const filteredSeriesX = w.globals.seriesX.map((ser) =>
ser.length > 0 ? ser : []
)
return filteredSeriesX
}
}
+241
View File
@@ -0,0 +1,241 @@
import Utils from '../utils/Utils'
/**
* ApexCharts Theme Class for setting the colors and palettes.
*
* @module Theme
**/
export default class Theme {
constructor(ctx) {
this.ctx = ctx
this.colors = []
this.w = ctx.w
const w = this.w
this.isColorFn = false
this.isHeatmapDistributed =
(w.config.chart.type === 'treemap' &&
w.config.plotOptions.treemap.distributed) ||
(w.config.chart.type === 'heatmap' &&
w.config.plotOptions.heatmap.distributed)
this.isBarDistributed =
w.config.plotOptions.bar.distributed &&
(w.config.chart.type === 'bar' || w.config.chart.type === 'rangeBar')
}
init() {
this.setDefaultColors()
}
setDefaultColors() {
let w = this.w
let utils = new Utils()
w.globals.dom.elWrap.classList.add(
`apexcharts-theme-${w.config.theme.mode}`
)
if (w.config.colors === undefined || w.config.colors?.length === 0) {
w.globals.colors = this.predefined()
} else {
w.globals.colors = w.config.colors
// if user provided a function in colors, we need to eval here
if (
Array.isArray(w.config.colors) &&
w.config.colors.length > 0 &&
typeof w.config.colors[0] === 'function'
) {
w.globals.colors = w.config.series.map((s, i) => {
let c = w.config.colors[i]
if (!c) c = w.config.colors[0]
if (typeof c === 'function') {
this.isColorFn = true
return c({
value: w.globals.axisCharts
? w.globals.series[i][0]
? w.globals.series[i][0]
: 0
: w.globals.series[i],
seriesIndex: i,
dataPointIndex: i,
w,
})
}
return c
})
}
}
// user defined colors in series array
w.globals.seriesColors.map((c, i) => {
if (c) {
w.globals.colors[i] = c
}
})
if (w.config.theme.monochrome.enabled) {
let monoArr = []
let glsCnt = w.globals.series.length
if (this.isBarDistributed || this.isHeatmapDistributed) {
glsCnt = w.globals.series[0].length * w.globals.series.length
}
let mainColor = w.config.theme.monochrome.color
let part = 1 / (glsCnt / w.config.theme.monochrome.shadeIntensity)
let shade = w.config.theme.monochrome.shadeTo
let percent = 0
for (let gsl = 0; gsl < glsCnt; gsl++) {
let newColor
if (shade === 'dark') {
newColor = utils.shadeColor(percent * -1, mainColor)
percent = percent + part
} else {
newColor = utils.shadeColor(percent, mainColor)
percent = percent + part
}
monoArr.push(newColor)
}
w.globals.colors = monoArr.slice()
}
const defaultColors = w.globals.colors.slice()
// if user specified fewer colors than no. of series, push the same colors again
this.pushExtraColors(w.globals.colors)
const colorTypes = ['fill', 'stroke']
colorTypes.forEach((c) => {
if (w.config[c].colors === undefined) {
w.globals[c].colors = this.isColorFn ? w.config.colors : defaultColors
} else {
w.globals[c].colors = w.config[c].colors.slice()
}
this.pushExtraColors(w.globals[c].colors)
})
if (w.config.dataLabels.style.colors === undefined) {
w.globals.dataLabels.style.colors = defaultColors
} else {
w.globals.dataLabels.style.colors =
w.config.dataLabels.style.colors.slice()
}
this.pushExtraColors(w.globals.dataLabels.style.colors, 50)
if (w.config.plotOptions.radar.polygons.fill.colors === undefined) {
w.globals.radarPolygons.fill.colors = [
w.config.theme.mode === 'dark' ? '#424242' : 'none',
]
} else {
w.globals.radarPolygons.fill.colors =
w.config.plotOptions.radar.polygons.fill.colors.slice()
}
this.pushExtraColors(w.globals.radarPolygons.fill.colors, 20)
// The point colors
if (w.config.markers.colors === undefined) {
w.globals.markers.colors = defaultColors
} else {
w.globals.markers.colors = w.config.markers.colors.slice()
}
this.pushExtraColors(w.globals.markers.colors)
}
// When the number of colors provided is less than the number of series, this method
// will push same colors to the list
// params:
// distributed is only valid for distributed column/bar charts
pushExtraColors(colorSeries, length, distributed = null) {
let w = this.w
let len = length || w.globals.series.length
if (distributed === null) {
distributed =
this.isBarDistributed ||
this.isHeatmapDistributed ||
(w.config.chart.type === 'heatmap' &&
w.config.plotOptions.heatmap.colorScale.inverse)
}
if (distributed && w.globals.series.length) {
len =
w.globals.series[w.globals.maxValsInArrayIndex].length *
w.globals.series.length
}
if (colorSeries.length < len) {
let diff = len - colorSeries.length
for (let i = 0; i < diff; i++) {
colorSeries.push(colorSeries[i])
}
}
}
updateThemeOptions(options) {
options.chart = options.chart || {}
options.tooltip = options.tooltip || {}
const mode = options.theme.mode || 'light'
const palette = options.theme.palette
? options.theme.palette
: mode === 'dark'
? 'palette4'
: 'palette1'
const foreColor = options.chart.foreColor
? options.chart.foreColor
: mode === 'dark'
? '#f6f7f8'
: '#373d3f'
options.tooltip.theme = mode
options.chart.foreColor = foreColor
options.theme.palette = palette
return options
}
predefined() {
let palette = this.w.config.theme.palette
// D6E3F8, FCEFEF, DCE0D9, A5978B, EDDDD4, D6E3F8, FEF5EF
switch (palette) {
case 'palette1':
this.colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0']
break
case 'palette2':
this.colors = ['#3f51b5', '#03a9f4', '#4caf50', '#f9ce1d', '#FF9800']
break
case 'palette3':
this.colors = ['#33b2df', '#546E7A', '#d4526e', '#13d8aa', '#A5978B']
break
case 'palette4':
this.colors = ['#4ecdc4', '#c7f464', '#81D4FA', '#fd6a6a', '#546E7A']
break
case 'palette5':
this.colors = ['#2b908f', '#f9a3a4', '#90ee7e', '#fa4443', '#69d2e7']
break
case 'palette6':
this.colors = ['#449DD1', '#F86624', '#EA3546', '#662E9B', '#C5D86D']
break
case 'palette7':
this.colors = ['#D7263D', '#1B998B', '#2E294E', '#F46036', '#E2C044']
break
case 'palette8':
this.colors = ['#662E9B', '#F86624', '#F9C80E', '#EA3546', '#43BCCD']
break
case 'palette9':
this.colors = ['#5C4742', '#A5978B', '#8D5B4C', '#5A2A27', '#C4BBAF']
break
case 'palette10':
this.colors = ['#A300D6', '#7D02EB', '#5653FE', '#2983FF', '#00B1F2']
break
default:
this.colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0']
break
}
return this.colors
}
}
+927
View File
@@ -0,0 +1,927 @@
import DateTime from '../utils/DateTime'
import Dimensions from './dimensions/Dimensions'
import Graphics from './Graphics'
import Utils from '../utils/Utils'
const MINUTES_IN_DAY = 24 * 60
const SECONDS_IN_DAY = MINUTES_IN_DAY * 60
const MIN_ZOOM_DAYS = 10 / SECONDS_IN_DAY
/**
* ApexCharts TimeScale Class for generating time ticks for x-axis.
*
* @module TimeScale
**/
class TimeScale {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.timeScaleArray = []
this.utc = this.w.config.xaxis.labels.datetimeUTC
}
calculateTimeScaleTicks(minX, maxX) {
let w = this.w
// null check when no series to show
if (w.globals.allSeriesCollapsed) {
w.globals.labels = []
w.globals.timescaleLabels = []
return []
}
let dt = new DateTime(this.ctx)
const daysDiff = (maxX - minX) / (1000 * SECONDS_IN_DAY)
this.determineInterval(daysDiff)
w.globals.disableZoomIn = false
w.globals.disableZoomOut = false
if (daysDiff < MIN_ZOOM_DAYS) {
w.globals.disableZoomIn = true
} else if (daysDiff > 50000) {
w.globals.disableZoomOut = true
}
const timeIntervals = dt.getTimeUnitsfromTimestamp(minX, maxX, this.utc)
const daysWidthOnXAxis = w.globals.gridWidth / daysDiff
const hoursWidthOnXAxis = daysWidthOnXAxis / 24
const minutesWidthOnXAxis = hoursWidthOnXAxis / 60
const secondsWidthOnXAxis = minutesWidthOnXAxis / 60
let numberOfHours = Math.floor(daysDiff * 24)
let numberOfMinutes = Math.floor(daysDiff * MINUTES_IN_DAY)
let numberOfSeconds = Math.floor(daysDiff * SECONDS_IN_DAY)
let numberOfDays = Math.floor(daysDiff)
let numberOfMonths = Math.floor(daysDiff / 30)
let numberOfYears = Math.floor(daysDiff / 365)
const firstVal = {
minMillisecond: timeIntervals.minMillisecond,
minSecond: timeIntervals.minSecond,
minMinute: timeIntervals.minMinute,
minHour: timeIntervals.minHour,
minDate: timeIntervals.minDate,
minMonth: timeIntervals.minMonth,
minYear: timeIntervals.minYear,
}
let currentMillisecond = firstVal.minMillisecond
let currentSecond = firstVal.minSecond
let currentMinute = firstVal.minMinute
let currentHour = firstVal.minHour
let currentMonthDate = firstVal.minDate
let currentDate = firstVal.minDate
let currentMonth = firstVal.minMonth
let currentYear = firstVal.minYear
const params = {
firstVal,
currentMillisecond,
currentSecond,
currentMinute,
currentHour,
currentMonthDate,
currentDate,
currentMonth,
currentYear,
daysWidthOnXAxis,
hoursWidthOnXAxis,
minutesWidthOnXAxis,
secondsWidthOnXAxis,
numberOfSeconds,
numberOfMinutes,
numberOfHours,
numberOfDays,
numberOfMonths,
numberOfYears,
}
switch (this.tickInterval) {
case 'years': {
this.generateYearScale(params)
break
}
case 'months':
case 'half_year': {
this.generateMonthScale(params)
break
}
case 'months_days':
case 'months_fortnight':
case 'days':
case 'week_days': {
this.generateDayScale(params)
break
}
case 'hours': {
this.generateHourScale(params)
break
}
case 'minutes_fives':
case 'minutes':
this.generateMinuteScale(params)
break
case 'seconds_tens':
case 'seconds_fives':
case 'seconds':
this.generateSecondScale(params)
break
}
// first, we will adjust the month values index
// as in the upper function, it is starting from 0
// we will start them from 1
const adjustedMonthInTimeScaleArray = this.timeScaleArray.map((ts) => {
let defaultReturn = {
position: ts.position,
unit: ts.unit,
year: ts.year,
day: ts.day ? ts.day : 1,
hour: ts.hour ? ts.hour : 0,
month: ts.month + 1,
}
if (ts.unit === 'month') {
return {
...defaultReturn,
day: 1,
value: ts.value + 1,
}
} else if (ts.unit === 'day' || ts.unit === 'hour') {
return {
...defaultReturn,
value: ts.value,
}
} else if (ts.unit === 'minute') {
return {
...defaultReturn,
value: ts.value,
minute: ts.value,
}
} else if (ts.unit === 'second') {
return {
...defaultReturn,
value: ts.value,
minute: ts.minute,
second: ts.second,
}
}
return ts
})
const filteredTimeScale = adjustedMonthInTimeScaleArray.filter((ts) => {
let modulo = 1
let ticks = Math.ceil(w.globals.gridWidth / 120)
let value = ts.value
if (w.config.xaxis.tickAmount !== undefined) {
ticks = w.config.xaxis.tickAmount
}
if (adjustedMonthInTimeScaleArray.length > ticks) {
modulo = Math.floor(adjustedMonthInTimeScaleArray.length / ticks)
}
let shouldNotSkipUnit = false // there is a big change in unit i.e days to months
let shouldNotPrint = false // should skip these values
switch (this.tickInterval) {
case 'years':
// make years label denser
if (ts.unit === 'year') {
shouldNotSkipUnit = true
}
break
case 'half_year':
modulo = 7
if (ts.unit === 'year') {
shouldNotSkipUnit = true
}
break
case 'months':
modulo = 1
if (ts.unit === 'year') {
shouldNotSkipUnit = true
}
break
case 'months_fortnight':
modulo = 15
if (ts.unit === 'year' || ts.unit === 'month') {
shouldNotSkipUnit = true
}
if (value === 30) {
shouldNotPrint = true
}
break
case 'months_days':
modulo = 10
if (ts.unit === 'month') {
shouldNotSkipUnit = true
}
if (value === 30) {
shouldNotPrint = true
}
break
case 'week_days':
modulo = 8
if (ts.unit === 'month') {
shouldNotSkipUnit = true
}
break
case 'days':
modulo = 1
if (ts.unit === 'month') {
shouldNotSkipUnit = true
}
break
case 'hours':
if (ts.unit === 'day') {
shouldNotSkipUnit = true
}
break
case 'minutes_fives':
if (value % 5 !== 0) {
shouldNotPrint = true
}
break
case 'seconds_tens':
if (value % 10 !== 0) {
shouldNotPrint = true
}
break
case 'seconds_fives':
if (value % 5 !== 0) {
shouldNotPrint = true
}
break
}
if (
this.tickInterval === 'hours' ||
this.tickInterval === 'minutes_fives' ||
this.tickInterval === 'seconds_tens' ||
this.tickInterval === 'seconds_fives'
) {
if (!shouldNotPrint) {
return true
}
} else {
if ((value % modulo === 0 || shouldNotSkipUnit) && !shouldNotPrint) {
return true
}
}
})
return filteredTimeScale
}
recalcDimensionsBasedOnFormat(filteredTimeScale, inverted) {
const w = this.w
const reformattedTimescaleArray = this.formatDates(filteredTimeScale)
const removedOverlappingTS = this.removeOverlappingTS(
reformattedTimescaleArray
)
w.globals.timescaleLabels = removedOverlappingTS.slice()
// at this stage, we need to re-calculate coords of the grid as timeline labels may have altered the xaxis labels coords
// The reason we can't do this prior to this stage is because timeline labels depends on gridWidth, and as the ticks are calculated based on available gridWidth, there can be unknown number of ticks generated for different minX and maxX
// Dependency on Dimensions(), need to refactor correctly
// TODO - find an alternate way to avoid calling this Heavy method twice
let dimensions = new Dimensions(this.ctx)
dimensions.plotCoords()
}
determineInterval(daysDiff) {
const yearsDiff = daysDiff / 365
const hoursDiff = daysDiff * 24
const minutesDiff = hoursDiff * 60
const secondsDiff = minutesDiff * 60
switch (true) {
case yearsDiff > 5:
this.tickInterval = 'years'
break
case daysDiff > 800:
this.tickInterval = 'half_year'
break
case daysDiff > 180:
this.tickInterval = 'months'
break
case daysDiff > 90:
this.tickInterval = 'months_fortnight'
break
case daysDiff > 60:
this.tickInterval = 'months_days'
break
case daysDiff > 30:
this.tickInterval = 'week_days'
break
case daysDiff > 2:
this.tickInterval = 'days'
break
case hoursDiff > 2.4:
this.tickInterval = 'hours'
break
case minutesDiff > 15:
this.tickInterval = 'minutes_fives'
break
case minutesDiff > 5:
this.tickInterval = 'minutes'
break
case minutesDiff > 1:
this.tickInterval = 'seconds_tens'
break
case secondsDiff > 20:
this.tickInterval = 'seconds_fives'
break
default:
this.tickInterval = 'seconds'
break
}
}
generateYearScale({
firstVal,
currentMonth,
currentYear,
daysWidthOnXAxis,
numberOfYears,
}) {
let firstTickValue = firstVal.minYear
let firstTickPosition = 0
const dt = new DateTime(this.ctx)
let unit = 'year'
if (firstVal.minDate > 1 || firstVal.minMonth > 0) {
let remainingDays = dt.determineRemainingDaysOfYear(
firstVal.minYear,
firstVal.minMonth,
firstVal.minDate
)
// remainingDaysofFirstMonth is used to reacht the 2nd tick position
let remainingDaysOfFirstYear =
dt.determineDaysOfYear(firstVal.minYear) - remainingDays + 1
// calculate the first tick position
firstTickPosition = remainingDaysOfFirstYear * daysWidthOnXAxis
firstTickValue = firstVal.minYear + 1
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: firstTickValue,
unit,
year: firstTickValue,
month: Utils.monthMod(currentMonth + 1),
})
} else if (firstVal.minDate === 1 && firstVal.minMonth === 0) {
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: firstTickValue,
unit,
year: currentYear,
month: Utils.monthMod(currentMonth + 1),
})
}
let year = firstTickValue
let pos = firstTickPosition
// keep drawing rest of the ticks
for (let i = 0; i < numberOfYears; i++) {
year++
pos = dt.determineDaysOfYear(year - 1) * daysWidthOnXAxis + pos
this.timeScaleArray.push({
position: pos,
value: year,
unit,
year,
month: 1,
})
}
}
generateMonthScale({
firstVal,
currentMonthDate,
currentMonth,
currentYear,
daysWidthOnXAxis,
numberOfMonths,
}) {
let firstTickValue = currentMonth
let firstTickPosition = 0
const dt = new DateTime(this.ctx)
let unit = 'month'
let yrCounter = 0
if (firstVal.minDate > 1) {
// remainingDaysofFirstMonth is used to reacht the 2nd tick position
let remainingDaysOfFirstMonth =
dt.determineDaysOfMonths(currentMonth + 1, firstVal.minYear) -
currentMonthDate +
1
// calculate the first tick position
firstTickPosition = remainingDaysOfFirstMonth * daysWidthOnXAxis
firstTickValue = Utils.monthMod(currentMonth + 1)
let year = currentYear + yrCounter
let month = Utils.monthMod(firstTickValue)
let value = firstTickValue
// it's Jan, so update the year
if (firstTickValue === 0) {
unit = 'year'
value = year
month = 1
yrCounter += 1
year = year + yrCounter
}
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value,
unit,
year,
month,
})
} else {
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: firstTickValue,
unit,
year: currentYear,
month: Utils.monthMod(currentMonth),
})
}
let month = firstTickValue + 1
let pos = firstTickPosition
// keep drawing rest of the ticks
for (let i = 0, j = 1; i < numberOfMonths; i++, j++) {
month = Utils.monthMod(month)
if (month === 0) {
unit = 'year'
yrCounter += 1
} else {
unit = 'month'
}
let year = this._getYear(currentYear, month, yrCounter)
pos = dt.determineDaysOfMonths(month, year) * daysWidthOnXAxis + pos
let monthVal = month === 0 ? year : month
this.timeScaleArray.push({
position: pos,
value: monthVal,
unit,
year,
month: month === 0 ? 1 : month,
})
month++
}
}
generateDayScale({
firstVal,
currentMonth,
currentYear,
hoursWidthOnXAxis,
numberOfDays,
}) {
const dt = new DateTime(this.ctx)
let unit = 'day'
let firstTickValue = firstVal.minDate + 1
let date = firstTickValue
const changeMonth = (dateVal, month, year) => {
let monthdays = dt.determineDaysOfMonths(month + 1, year)
if (dateVal > monthdays) {
month = month + 1
date = 1
unit = 'month'
val = month
return month
}
return month
}
let remainingHours = 24 - firstVal.minHour
let yrCounter = 0
// calculate the first tick position
let firstTickPosition = remainingHours * hoursWidthOnXAxis
let val = firstTickValue
let month = changeMonth(date, currentMonth, currentYear)
if (firstVal.minHour === 0 && firstVal.minDate === 1) {
// the first value is the first day of month
firstTickPosition = 0
val = Utils.monthMod(firstVal.minMonth)
unit = 'month'
date = firstVal.minDate
// numberOfDays++
// removed the above line to fix https://github.com/apexcharts/apexcharts.js/issues/305#issuecomment-1019520513
} else if (
firstVal.minDate !== 1 &&
firstVal.minHour === 0 &&
firstVal.minMinute === 0
) {
// fixes apexcharts/apexcharts.js/issues/1730
firstTickPosition = 0
firstTickValue = firstVal.minDate
date = firstTickValue
val = firstTickValue
// in case it's the last date of month, we need to check it
month = changeMonth(date, currentMonth, currentYear)
}
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: val,
unit,
year: this._getYear(currentYear, month, yrCounter),
month: Utils.monthMod(month),
day: date,
})
let pos = firstTickPosition
// keep drawing rest of the ticks
for (let i = 0; i < numberOfDays; i++) {
date += 1
unit = 'day'
month = changeMonth(
date,
month,
this._getYear(currentYear, month, yrCounter)
)
let year = this._getYear(currentYear, month, yrCounter)
pos = 24 * hoursWidthOnXAxis + pos
let value = date === 1 ? Utils.monthMod(month) : date
this.timeScaleArray.push({
position: pos,
value,
unit,
year,
month: Utils.monthMod(month),
day: value,
})
}
}
generateHourScale({
firstVal,
currentDate,
currentMonth,
currentYear,
minutesWidthOnXAxis,
numberOfHours,
}) {
const dt = new DateTime(this.ctx)
let yrCounter = 0
let unit = 'hour'
const changeDate = (dateVal, month) => {
let monthdays = dt.determineDaysOfMonths(month + 1, currentYear)
if (dateVal > monthdays) {
date = 1
month = month + 1
}
return { month, date }
}
const changeMonth = (dateVal, month) => {
let monthdays = dt.determineDaysOfMonths(month + 1, currentYear)
if (dateVal > monthdays) {
month = month + 1
return month
}
return month
}
// factor in minSeconds as well
let remainingMins = 60 - (firstVal.minMinute + firstVal.minSecond / 60.0)
let firstTickPosition = remainingMins * minutesWidthOnXAxis
let firstTickValue = firstVal.minHour + 1
let hour = firstTickValue
if (remainingMins === 60) {
firstTickPosition = 0
firstTickValue = firstVal.minHour
hour = firstTickValue
}
let date = currentDate
// we need to apply date switching logic here as well, to avoid duplicated labels
if (hour >= 24) {
hour = 0
date += 1
unit = 'day'
}
const checkNextMonth = changeDate(date, currentMonth)
let month = checkNextMonth.month
month = changeMonth(date, month)
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: firstTickValue,
unit,
day: date,
hour,
year: currentYear,
month: Utils.monthMod(month),
})
hour++
let pos = firstTickPosition
// keep drawing rest of the ticks
for (let i = 0; i < numberOfHours; i++) {
unit = 'hour'
if (hour >= 24) {
hour = 0
date += 1
unit = 'day'
const checkNextMonth = changeDate(date, month)
month = checkNextMonth.month
month = changeMonth(date, month)
}
let year = this._getYear(currentYear, month, yrCounter)
pos = 60 * minutesWidthOnXAxis + pos
let val = hour === 0 ? date : hour
this.timeScaleArray.push({
position: pos,
value: val,
unit,
hour,
day: date,
year,
month: Utils.monthMod(month),
})
hour++
}
}
generateMinuteScale({
currentMillisecond,
currentSecond,
currentMinute,
currentHour,
currentDate,
currentMonth,
currentYear,
minutesWidthOnXAxis,
secondsWidthOnXAxis,
numberOfMinutes,
}) {
let yrCounter = 0
let unit = 'minute'
let remainingSecs = 60 - currentSecond
let firstTickPosition =
(remainingSecs - currentMillisecond / 1000) * secondsWidthOnXAxis
let minute = currentMinute + 1
let date = currentDate
let month = currentMonth
let year = currentYear
let hour = currentHour
let pos = firstTickPosition
for (let i = 0; i < numberOfMinutes; i++) {
if (minute >= 60) {
minute = 0
hour += 1
if (hour === 24) {
hour = 0
}
}
this.timeScaleArray.push({
position: pos,
value: minute,
unit,
hour,
minute,
day: date,
year: this._getYear(year, month, yrCounter),
month: Utils.monthMod(month),
})
pos += minutesWidthOnXAxis
minute++
}
}
generateSecondScale({
currentMillisecond,
currentSecond,
currentMinute,
currentHour,
currentDate,
currentMonth,
currentYear,
secondsWidthOnXAxis,
numberOfSeconds,
}) {
let yrCounter = 0
let unit = 'second'
const remainingMillisecs = 1000 - currentMillisecond
let firstTickPosition = (remainingMillisecs / 1000) * secondsWidthOnXAxis
let second = currentSecond + 1
let minute = currentMinute
let date = currentDate
let month = currentMonth
let year = currentYear
let hour = currentHour
let pos = firstTickPosition
for (let i = 0; i < numberOfSeconds; i++) {
if (second >= 60) {
minute++
second = 0
if (minute >= 60) {
hour++
minute = 0
if (hour === 24) {
hour = 0
}
}
}
this.timeScaleArray.push({
position: pos,
value: second,
unit,
hour,
minute,
second,
day: date,
year: this._getYear(year, month, yrCounter),
month: Utils.monthMod(month),
})
pos += secondsWidthOnXAxis
second++
}
}
createRawDateString(ts, value) {
let raw = ts.year
if (ts.month === 0) {
// invalid month, correct it
ts.month = 1
}
raw += '-' + ('0' + ts.month.toString()).slice(-2)
// unit is day
if (ts.unit === 'day') {
raw += ts.unit === 'day' ? '-' + ('0' + value).slice(-2) : '-01'
} else {
raw += '-' + ('0' + (ts.day ? ts.day : '1')).slice(-2)
}
// unit is hour
if (ts.unit === 'hour') {
raw += ts.unit === 'hour' ? 'T' + ('0' + value).slice(-2) : 'T00'
} else {
raw += 'T' + ('0' + (ts.hour ? ts.hour : '0')).slice(-2)
}
if (ts.unit === 'minute') {
raw += ':' + ('0' + value).slice(-2)
} else {
raw += ':' + (ts.minute ? ('0' + ts.minute).slice(-2) : '00')
}
if (ts.unit === 'second') {
raw += ':' + ('0' + value).slice(-2)
} else {
raw += ':00'
}
if (this.utc) {
raw += '.000Z'
}
return raw
}
formatDates(filteredTimeScale) {
const w = this.w
const reformattedTimescaleArray = filteredTimeScale.map((ts) => {
let value = ts.value.toString()
let dt = new DateTime(this.ctx)
const raw = this.createRawDateString(ts, value)
let dateToFormat = dt.getDate(dt.parseDate(raw))
if (!this.utc) {
// Fixes #1726, #1544, #1485, #1255
dateToFormat = dt.getDate(dt.parseDateWithTimezone(raw))
}
if (w.config.xaxis.labels.format === undefined) {
let customFormat = 'dd MMM'
const dtFormatter = w.config.xaxis.labels.datetimeFormatter
if (ts.unit === 'year') customFormat = dtFormatter.year
if (ts.unit === 'month') customFormat = dtFormatter.month
if (ts.unit === 'day') customFormat = dtFormatter.day
if (ts.unit === 'hour') customFormat = dtFormatter.hour
if (ts.unit === 'minute') customFormat = dtFormatter.minute
if (ts.unit === 'second') customFormat = dtFormatter.second
value = dt.formatDate(dateToFormat, customFormat)
} else {
value = dt.formatDate(dateToFormat, w.config.xaxis.labels.format)
}
return {
dateString: raw,
position: ts.position,
value,
unit: ts.unit,
year: ts.year,
month: ts.month,
}
})
return reformattedTimescaleArray
}
removeOverlappingTS(arr) {
const graphics = new Graphics(this.ctx)
let equalLabelLengthFlag = false // These labels got same length?
let constantLabelWidth // If true, what is the constant length to use
if (
arr.length > 0 && // check arr length
arr[0].value && // check arr[0] contains value
arr.every((lb) => lb.value.length === arr[0].value.length) // check every arr label value is the same as the first one
) {
equalLabelLengthFlag = true // These labels got same length
constantLabelWidth = graphics.getTextRects(arr[0].value).width // The constant label width to use
}
let lastDrawnIndex = 0
let filteredArray = arr.map((item, index) => {
if (index > 0 && this.w.config.xaxis.labels.hideOverlappingLabels) {
const prevLabelWidth = !equalLabelLengthFlag // if vary in label length
? graphics.getTextRects(arr[lastDrawnIndex].value).width // get individual length
: constantLabelWidth // else: use constant length
const prevPos = arr[lastDrawnIndex].position
const pos = item.position
if (pos > prevPos + prevLabelWidth + 10) {
lastDrawnIndex = index
return item
} else {
return null
}
} else {
return item
}
})
filteredArray = filteredArray.filter((f) => f !== null)
return filteredArray
}
_getYear(currentYear, month, yrCounter) {
return currentYear + Math.floor(month / 12) + yrCounter
}
}
export default TimeScale
+52
View File
@@ -0,0 +1,52 @@
import Graphics from './Graphics'
export default class TitleSubtitle {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
draw() {
this.drawTitleSubtitle('title')
this.drawTitleSubtitle('subtitle')
}
drawTitleSubtitle(type) {
let w = this.w
const tsConfig = type === 'title' ? w.config.title : w.config.subtitle
let x = w.globals.svgWidth / 2
let y = tsConfig.offsetY
let textAnchor = 'middle'
if (tsConfig.align === 'left') {
x = 10
textAnchor = 'start'
} else if (tsConfig.align === 'right') {
x = w.globals.svgWidth - 10
textAnchor = 'end'
}
x = x + tsConfig.offsetX
y = y + parseInt(tsConfig.style.fontSize, 10) + tsConfig.margin / 2
if (tsConfig.text !== undefined) {
let graphics = new Graphics(this.ctx)
let titleText = graphics.drawText({
x,
y,
text: tsConfig.text,
textAnchor,
fontSize: tsConfig.style.fontSize,
fontFamily: tsConfig.style.fontFamily,
fontWeight: tsConfig.style.fontWeight,
foreColor: tsConfig.style.color,
opacity: 1
})
titleText.node.setAttribute('class', `apexcharts-${type}-text`)
w.globals.dom.Paper.add(titleText)
}
}
}

Some files were not shown because too many files have changed in this diff Show More