/*
* @version: 3.2.3
* @author: Preline Labs Ltd.
* @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
* Copyright 2024 Preline Labs Ltd.
*/
import {
IBuildTooltipHelperOptions,
IBuildTooltipHelperSingleOptions,
IChartDonutProps,
IChartProps,
IChartPropsSeries
} from './interfaces'
import ApexCharts from 'apexcharts'
function buildTooltip(props: IChartProps, options: IBuildTooltipHelperOptions) {
const {
title,
valuePrefix = '$',
isValueDivided = true,
valuePostfix = '',
hasTextLabel = false,
invertGroup = false,
labelDivider = '',
wrapperClasses = 'bg-base-100 min-w-28 text-base-content/80 rounded-lg !border-none',
wrapperExtClasses = '',
seriesClasses = 'text-xs items-center',
seriesExtClasses = '',
titleClasses = '!text-sm !font-semibold !bg-base-100 !border-base-content/40 text-base-content rounded-t-lg !px-2.5',
titleExtClasses = '',
markerClasses = '!w-2.5 !h-2.5 !me-1.5 rtl:!mr-0 !rounded-full',
markerExtClasses = '',
valueClasses = '!font-medium text-base-content/80 !ms-auto',
valueExtClasses = '',
labelClasses = 'text-base-content',
labelExtClasses = '',
thousandsShortName = 'k'
} = options
const { dataPointIndex } = props
const { colors } = props.ctx.opts
const series = props.ctx.opts.series as IChartPropsSeries[]
let seriesGroups = ''
series.forEach((_, i) => {
const val =
props.series[i][dataPointIndex] ||
(typeof series[i].data[dataPointIndex] !== 'object'
? series[i].data[dataPointIndex]
: props.series[i][dataPointIndex])
const label = series[i].name
const groupData = invertGroup
? {
left: `${hasTextLabel ? label : ''}${labelDivider}`,
right: `${valuePrefix}${val >= 1000 && isValueDivided ? `${val / 1000}${thousandsShortName}` : val}${valuePostfix}`
}
: {
left: `${valuePrefix}${val >= 1000 && isValueDivided ? `${val / 1000}${thousandsShortName}` : val}${valuePostfix}`,
right: `${hasTextLabel ? label : ''}${labelDivider}`
}
const labelMarkup = `${groupData.left}`
seriesGroups += `
`
})
return ``
}
function buildHeatmapTooltip(props: IChartProps, options: IBuildTooltipHelperSingleOptions) {
const {
valuePrefix = '$',
valuePostfix = '',
divider = '',
wrapperClasses = 'ms-0.5 mb-2 bg-base-100 border border-base-content/20 text-base-content rounded-box shadow-md',
wrapperExtClasses = '',
markerClasses = '!w-2.5 !h-2.5 !me-1.5',
markerStyles = '',
markerExtClasses = '!rounded-xs',
valueClasses = '!font-medium text-base-content !ms-auto',
valueExtClasses = ''
} = options
const { dataPointIndex, seriesIndex, series } = props
const { name } = props.ctx.opts.series[seriesIndex] as IChartPropsSeries
const val = `${valuePrefix}${series[seriesIndex][dataPointIndex]}${valuePostfix}`
return ``
}
function buildTooltipCompareTwo(props: IChartProps, options: IBuildTooltipHelperOptions) {
const {
title,
valuePrefix = '$',
isValueDivided = true,
valuePostfix = '',
hasCategory = true,
hasTextLabel = false,
labelDivider = '',
wrapperClasses = 'bg-base-100 min-w-48 text-base-content/80 rounded-lg !border-none',
wrapperExtClasses = '',
seriesClasses = 'text-xs items-center !justify-between',
seriesExtClasses = '',
titleClasses = '!text-sm !font-semibold !bg-base-100 !border-base-content/40 text-base-content rounded-t-lg !px-2.5',
titleExtClasses = 'flex justify-between',
markerClasses = '!w-2.5 !h-2.5 !me-1.5 !rounded-full',
markerExtClasses = '',
valueClasses = '!font-medium text-base-content/80 !ms-auto',
valueExtClasses = '',
labelClasses = 'text-base-content !font-medium',
labelExtClasses = '',
thousandsShortName = 'k'
} = options
const { dataPointIndex } = props
const { categories } = props.ctx.opts.xaxis
const { colors } = props.ctx.opts
const series = props.ctx.opts.series as IChartPropsSeries[]
let seriesGroups = ''
const s0 = series[0].data[dataPointIndex]
const s1 = series[1].data[dataPointIndex]
const category = categories[dataPointIndex].split(' ')
const newCategory = hasCategory
? `${category[0]}${category[1] ? ' ' : ''}${category[1] ? category[1].slice(0, 3) : ''}`
: ''
// const isGrowing = s0 > s1;
// const isDifferenceIsNull = s0 / s1 === 1;
// const difference = isDifferenceIsNull ? 0 : (s0 / s1) * 100;
// TODO: test this before deleting the code above
const isPrevZero = s1 === 0
const difference = isPrevZero ? 0 : ((s0 - s1) / Math.abs(s1)) * 100
const isDifferenceIsNull = difference === 0
const isGrowing = difference > 0
const icon = isGrowing
? ``
: ``
series.forEach((_, i) => {
const val =
props.series[i][dataPointIndex] ||
(typeof series[i].data[dataPointIndex] !== 'object'
? series[i].data[dataPointIndex]
: props.series[i][dataPointIndex])
const label = series[i].name
const altValue = series[i].altValue || null
const labelMarkup = `${newCategory} ${label || ''}`
const valueMarkup =
altValue ||
`${valuePrefix}${val >= 1000 && isValueDivided ? `${val / 1000}${thousandsShortName}` : val}${valuePostfix}${labelDivider}`
seriesGroups += ``
})
return `
${title}
${!isDifferenceIsNull ? icon : ''}
${difference.toFixed(1)}%
${seriesGroups}
`
}
function buildTooltipCompareTwoAlt(props: IChartProps, options: IBuildTooltipHelperOptions) {
const {
title,
valuePrefix = '$',
isValueDivided = true,
valuePostfix = '',
hasCategory = true,
hasTextLabel = false,
labelDivider = '',
wrapperClasses = 'bg-base-100 min-w-48 text-base-content/80 rounded-lg !border-none',
wrapperExtClasses = '',
seriesClasses = 'text-xs items-center !justify-between',
seriesExtClasses = '',
titleClasses = '!text-sm !font-semibold !bg-base-100 !border-base-content/40 text-base-content rounded-t-lg flex !justify-between !px-2.5',
titleExtClasses = '',
markerClasses = '!w-2.5 !h-2.5 !me-1.5 !rounded-full',
markerExtClasses = '',
valueClasses = '!font-medium text-base-content/80 !ms-auto',
valueExtClasses = '',
labelClasses = 'text-base-content !font-medium',
labelExtClasses = '',
thousandsShortName = 'k'
} = options
const { dataPointIndex } = props
const { categories } = props.ctx.opts.xaxis
const { colors } = props.ctx.opts
const series = props.ctx.opts.series as IChartPropsSeries[]
let seriesGroups = ''
const s0 = series[0].data[dataPointIndex]
const s1 = series[1].data[dataPointIndex]
const category = categories[dataPointIndex].split(' ')
const newCategory = hasCategory
? `${category[0]}${category[1] ? ' ' : ''}${category[1] ? category[1].slice(0, 3) : ''}`
: ''
// const isGrowing = s0 > s1;
// const isDifferenceIsNull = s0 / s1 === 1;
// const difference = isDifferenceIsNull ? 0 : (s0 / s1) * 100;
// TODO: test this before deleting the code above
const isPrevZero = s1 === 0
const difference = isPrevZero ? 0 : ((s0 - s1) / Math.abs(s1)) * 100
const isDifferenceIsNull = difference === 0
const isGrowing = difference > 0
const icon = isGrowing
? ``
: ``
series.forEach((_, i) => {
const val =
props.series[i][dataPointIndex] ||
(typeof series[i].data[dataPointIndex] !== 'object'
? series[i].data[dataPointIndex]
: props.series[i][dataPointIndex])
const label = series[i].name
const labelMarkup = `${valuePrefix}${val >= 1000 && isValueDivided ? `${val / 1000}${thousandsShortName}` : val}${valuePostfix}`
seriesGroups += ``
})
return `
${title}
${!isDifferenceIsNull ? icon : ''}
${difference.toFixed(1)}%
${seriesGroups}
`
}
function buildTooltipForDonut({ series, seriesIndex, w }: IChartDonutProps, textColor: string[]) {
const { globals } = w
const { colors } = globals
return ``
}
function buildChart(id: string, shared: Function) {
const $chart = document.querySelector(id)
let chart: any = null
if (!$chart) return false
const optionsFn = () => shared()
// Initialize and render the chart
if ($chart) {
chart = new ApexCharts($chart, optionsFn())
chart.render()
}
return chart
}
function fullBarHoverEffect(
chartCtx: ApexCharts & {
el: HTMLElement
w: { config: { xaxis?: { categories?: any[] } } }
},
{ shadowClasses = 'fill-gray-200' }: { shadowClasses?: string } = {}
): void {
const grid = chartCtx.el.querySelector('.apexcharts-grid')
const svg = chartCtx.el.querySelector('svg')
if (!grid || !svg) return
const categories: any[] = chartCtx.w.config.xaxis?.categories || []
if (categories.length === 0) return
let shadowRect: SVGRectElement | null = null
let isVisible = false
let isRemoving = false
function cleanup() {
shadowRect?.remove()
shadowRect = null
isVisible = false
isRemoving = false
}
function showForIndex(index: number) {
const seriesGroup = chartCtx.el.querySelector('.apexcharts-bar-series')
if (!seriesGroup) return
const bars = seriesGroup.querySelectorAll('path')
const bar = bars[index]
if (!bar) return
const bbox = bar.getBBox()
const x = bbox.x
const y = bbox.y
const width = bbox.width
if (y <= 0) return
if (!shadowRect) {
shadowRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
shadowRect.setAttribute('y', '0')
shadowRect.setAttribute('class', shadowClasses)
bar.parentNode?.insertBefore(shadowRect, bar)
}
shadowRect.setAttribute('x', x.toString())
shadowRect.setAttribute('width', width.toString())
shadowRect.setAttribute('height', y.toString())
requestAnimationFrame(() => {
shadowRect?.classList.add('opacity-100')
})
isVisible = true
isRemoving = false
}
function hide() {
if (!shadowRect || !isVisible || isRemoving) return
isRemoving = true
shadowRect.classList.remove('opacity-100')
cleanup()
}
svg.addEventListener('mousemove', (e: MouseEvent) => {
const gridRect = grid.getBoundingClientRect()
if (
e.clientX < gridRect.left ||
e.clientX > gridRect.right ||
e.clientY < gridRect.top ||
e.clientY > gridRect.bottom
) {
hide()
return
}
const relativeX = e.clientX - gridRect.left
const ratio = relativeX / gridRect.width
const index = Math.floor(ratio * categories.length)
if (index < 0 || index >= categories.length) {
hide()
return
}
showForIndex(index)
})
svg.addEventListener('mouseleave', hide)
}
export {
buildChart,
buildHeatmapTooltip,
buildTooltip,
buildTooltipCompareTwo,
buildTooltipCompareTwoAlt,
buildTooltipForDonut,
fullBarHoverEffect
}