404 lines
10 KiB
JavaScript
404 lines
10 KiB
JavaScript
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)
|
|
})
|
|
}
|
|
}
|