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, 0, '#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) } } const 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 }) 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, 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, text, x1, y1, x2, y2) { const graphics = new Graphics(this.ctx) const textRect = graphics.getTextRects(text) //if the label fits better sideways then rotate it if (textRect.width + 5 > x2 - x1 && textRect.width <= y2 - y1) { let labelRotatingCenter = graphics.rotateAroundCenter(elText.node) elText.node.setAttribute( 'transform', `rotate(-90 ${labelRotatingCenter.x} ${labelRotatingCenter.y})` ) } } 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) } ) } }