UNPKG

apexcharts

Version:

A JavaScript Chart Library

404 lines (350 loc) 10.8 kB
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', }) let bounds = { xMin: Infinity, yMin: Infinity, xMax: -Infinity, yMax: -Infinity, } node.forEach((r, j) => { const x1 = r[0] const y1 = r[1] const x2 = r[2] const y2 = r[3] bounds.xMin = Math.min(bounds.xMin, x1) bounds.yMin = Math.min(bounds.yMin, y1) bounds.xMax = Math.max(bounds.xMax, x2) bounds.yMax = Math.max(bounds.yMax, y2) let colorProps = this.helpers.getShadeColor( w.config.chart.type, i, j, this.negRange ) let color = colorProps.color let pathFill = fill.fillPath({ color, seriesNumber: i, dataPointIndex: j, }) 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, fill: pathFill, }) elRect.node.classList.add('apexcharts-treemap-rect') 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 = null if (w.globals.series[i][j]) { 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) } }) const seriesTitle = w.config.plotOptions.treemap.seriesTitle if (w.config.series.length > 1 && seriesTitle && seriesTitle.show) { const sName = w.config.series[i].name || '' if (sName && bounds.xMin < Infinity && bounds.yMin < Infinity) { const { offsetX, offsetY, borderColor, borderWidth, borderRadius, style, } = seriesTitle const textColor = style.color || w.config.chart.foreColor const padding = { left: style.padding.left, right: style.padding.right, top: style.padding.top, bottom: style.padding.bottom, } const textSize = graphics.getTextRects( sName, style.fontSize, style.fontFamily ) const labelRectWidth = textSize.width + padding.left + padding.right const labelRectHeight = textSize.height + padding.top + padding.bottom // Position const labelX = bounds.xMin + (offsetX || 0) const labelY = bounds.yMin + (offsetY || 0) // Draw background rect const elLabelRect = graphics.drawRect( labelX, labelY, labelRectWidth, labelRectHeight, borderRadius, style.background, 1, borderWidth, borderColor ) const elLabelText = graphics.drawText({ x: labelX + padding.left, y: labelY + padding.top + textSize.height * 0.75, text: sName, fontSize: style.fontSize, fontFamily: style.fontFamily, fontWeight: style.fontWeight, foreColor: textColor, cssClass: style.cssClass || '', }) elSeries.add(elLabelRect) elSeries.add(elLabelText) } } elSeries.add(elDataLabelWrap) ret.add(elSeries) }) return ret } // This calculates a font-size based upon // average label length and the size of the box 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) { 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, fromRect, toRect, speed, () => { animations.animationCompleted(el) }) } }