/* * @version: 4.1.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 { EventWithProps } from '../types'; import ApexCharts from 'apexcharts'; function buildTooltip(props: IChartProps, options: IBuildTooltipHelperOptions) { const { title, mode, valuePrefix = '$', isValueDivided = true, valuePostfix = '', hasTextLabel = false, invertGroup = false, labelDivider = '', wrapperClasses = 'ms-0.5 mb-2 bg-white border border-gray-200 text-gray-800 rounded-lg shadow-md dark:bg-neutral-800 dark:border-neutral-700', wrapperExtClasses = '', seriesClasses = 'text-xs', seriesExtClasses = '', titleClasses = 'font-semibold !text-sm !bg-white !border-gray-200 text-gray-800 rounded-t-lg dark:!bg-neutral-800 dark:!border-neutral-700 dark:text-neutral-200', titleExtClasses = '', markerClasses = '!w-2.5 !h-2.5 !me-1.5', markerExtClasses = '!rounded-xs', valueClasses = '!font-medium text-gray-500 !ms-auto dark:text-neutral-400', valueExtClasses = '', labelClasses = 'text-gray-500 dark:text-neutral-400', labelExtClasses = '', thousandsShortName = 'k', } = options; const { dataPointIndex } = props; // const { colors } = props.ctx.opts; const { colors } = props.w.globals; 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 += `
${groupData.right}
${labelMarkup}
`; }); return `
${title}
${seriesGroups}
`; } function buildHeatmapTooltip( props: IChartProps, options: IBuildTooltipHelperSingleOptions, ) { const { mode, valuePrefix = '$', valuePostfix = '', divider = '', wrapperClasses = 'ms-0.5 mb-2 bg-white border border-gray-200 text-gray-800 rounded-lg shadow-md dark:bg-neutral-800 dark:border-neutral-700', wrapperExtClasses = '', markerClasses = '!w-2.5 !h-2.5 !me-1.5', markerStyles = '', markerExtClasses = '!rounded-xs', valueClasses = '!font-medium text-gray-500 !ms-auto dark:text-neutral-400', valueExtClasses = '', } = options; const { dataPointIndex, seriesIndex, series } = props; const { name } = props.ctx.opts.series[seriesIndex] as IChartPropsSeries; const val = `${valuePrefix}${ series[seriesIndex][dataPointIndex] }${valuePostfix}`; return `
${name}${divider}
${val}
`; } function buildTooltipCompareTwo( props: IChartProps, options: IBuildTooltipHelperOptions, ) { const { title, mode, valuePrefix = '$', isValueDivided = true, valuePostfix = '', hasCategory = true, hasTextLabel = false, labelDivider = '', wrapperClasses = 'ms-0.5 mb-2 bg-white border border-gray-200 text-gray-800 rounded-lg shadow-md dark:bg-neutral-800 dark:border-neutral-700', wrapperExtClasses = '', seriesClasses = '!justify-between w-full text-xs', seriesExtClasses = '', titleClasses = 'flex justify-between font-semibold !text-sm !bg-white !border-gray-200 text-gray-800 rounded-t-lg dark:!bg-neutral-800 dark:!border-neutral-700 dark:text-neutral-200', titleExtClasses = '', markerClasses = '!w-2.5 !h-2.5 !me-1.5', markerExtClasses = '!rounded-xs', valueClasses = '!font-medium text-gray-500 !ms-auto dark:text-neutral-400', valueExtClasses = '', labelClasses = 'text-gray-500 dark:text-neutral-400 ms-2', labelExtClasses = '', thousandsShortName = 'k', } = options; const { dataPointIndex } = props; const { categories } = props.ctx.opts.xaxis; // const { colors } = props.ctx.opts; const { colors } = props.w.globals; 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 += `
${valueMarkup}
${hasTextLabel ? labelMarkup : ''}
`; }); return `
${title} ${!isDifferenceIsNull ? icon : ''} ${difference.toFixed(1)}%
${seriesGroups}
`; } function buildTooltipCompareTwoAlt( props: IChartProps, options: IBuildTooltipHelperOptions, ) { const { title, mode, valuePrefix = '$', isValueDivided = true, valuePostfix = '', hasCategory = true, hasTextLabel = false, labelDivider = '', wrapperClasses = 'ms-0.5 mb-2 bg-white border border-gray-200 text-gray-800 rounded-lg shadow-md dark:bg-neutral-800 dark:border-neutral-700', wrapperExtClasses = '', seriesClasses = '!justify-between w-full text-xs', seriesExtClasses = '', titleClasses = 'flex justify-between font-semibold !text-sm !bg-white !border-gray-200 text-gray-800 rounded-t-lg dark:!bg-neutral-800 dark:!border-neutral-700 dark:text-neutral-200', titleExtClasses = '', markerClasses = '!w-2.5 !h-2.5 !me-1.5', markerExtClasses = '!rounded-xs', valueClasses = '!font-medium text-gray-500 !ms-auto dark:text-neutral-400', valueExtClasses = '', labelClasses = 'text-gray-500 dark:text-neutral-400 ms-2', labelExtClasses = '', thousandsShortName = 'k', } = options; const { dataPointIndex } = props; const { categories } = props.ctx.opts.xaxis; // const { colors } = props.ctx.opts; const { colors } = props.w.globals; 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 += `
${newCategory} ${ label || '' }${labelDivider}
${hasTextLabel ? labelMarkup : ''}
`; }); return `
${title} ${!isDifferenceIsNull ? icon : ''} ${difference.toFixed(1)}%
${seriesGroups}
`; } function buildTooltipForDonut( { series, seriesIndex, w }: IChartDonutProps, textColor: string[], ) { const { globals } = w; const { colors } = globals; return `
${ globals.labels[seriesIndex] }: ${ series[seriesIndex] }
${ globals.labels[seriesIndex] }: ${ series[seriesIndex] }
`; } function buildChart( id: string, shared: Function, light: string | Function, dark: string | Function, ) { const $chart = document.querySelector(id); let chart: any = null; if (!$chart) return false; const tabpanel = $chart.closest('[role="tabpanel"]'); let modeFromBodyClass: string | null = null; Array.from(document.querySelector('html').classList).forEach((cl) => { if (['dark', 'light', 'default'].includes(cl)) modeFromBodyClass = cl; }); const optionsFn = ( mode = modeFromBodyClass || localStorage.getItem('hs_theme'), ) => { if ( mode === 'dark' || (mode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) ) { return window._.merge( shared('dark'), typeof dark === 'function' ? dark() : dark, ); } else { return window._.merge( shared('light'), typeof light === 'function' ? light() : light, ); } }; if ($chart) { let isInitialLoad = true; chart = new ApexCharts($chart, optionsFn()); chart.render(); setTimeout(() => { isInitialLoad = false; }, 100); let hasInitProgressApplied = false; const handleThemeChange = (evt: EventWithProps) => { if (isInitialLoad) return; chart.updateOptions(optionsFn(evt.detail)); // If the chart is in an iframe, dispatch a resize event to the parent window if (window.self !== window.top) window.dispatchEvent(new Event('resize')); }; const applyOptionsChange = (detail: any) => { let modeFromBodyClass; const target = detail?.target ?? document.querySelector('html'); Array.from(target.classList).forEach((cl: string) => { if (['dark', 'light', 'default'].includes(cl)) modeFromBodyClass = cl; }); handleThemeChange({ detail: modeFromBodyClass || localStorage.getItem('hs_theme'), } as EventWithProps); }; const handleOptionsChange = (evt: EventWithProps) => { const detail = evt.detail as any; if (detail && detail.isClipboardInit) return; applyOptionsChange(detail); }; const handleClipboardInitProgress = (evt: EventWithProps) => { if (hasInitProgressApplied) return; const detail = evt.detail as any; const target = detail?.target; if (!target || !$chart || !target.contains($chart)) return; hasInitProgressApplied = true; if (isInitialLoad) { setTimeout(() => applyOptionsChange(detail), 120); } else { applyOptionsChange(detail); } }; window.addEventListener('on-hs-appearance-change', handleThemeChange); window.addEventListener( 'on-hs-color-theme-change', (evt: EventWithProps) => { setTimeout(() => handleOptionsChange(evt), 50); }, ); window.addEventListener('on-hs-font-change', (evt: EventWithProps) => { setTimeout(() => handleOptionsChange(evt), 50); }); window.addEventListener('on-hs-brand-change', (evt: EventWithProps) => { setTimeout(() => handleOptionsChange(evt), 50); }); window.addEventListener( 'on-hs-clipboard-init-progress', handleClipboardInitProgress, ); if (tabpanel) { tabpanel.addEventListener('on-hs-appearance-change', handleThemeChange); } } 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); } function cssVarToValue(name: string, context: HTMLElement = document.documentElement): string | null { const value = getComputedStyle(context).getPropertyValue(name); if (!value) return null; return value.trim(); } export { buildChart, buildHeatmapTooltip, buildTooltip, buildTooltipCompareTwo, buildTooltipCompareTwoAlt, buildTooltipForDonut, fullBarHoverEffect, cssVarToValue };