preline
Version:
Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
624 lines (562 loc) • 20.3 kB
text/typescript
/*
* @version: 4.2.0
* @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 = `<span class="apexcharts-tooltip-text-y-label ${labelClasses} ${labelExtClasses}">${groupData.left}</span>`;
seriesGroups += `<div class="apexcharts-tooltip-series-group !flex ${
hasTextLabel ? 'justify-between!' : ''
} order-${i + 1} ${seriesClasses} ${seriesExtClasses}">
<span class="flex items-center">
<span class="apexcharts-tooltip-marker ${markerClasses} ${markerExtClasses}" style="background: ${
colors[i]
}"></span>
<div class="apexcharts-tooltip-text">
<div class="apexcharts-tooltip-y-group py-0.5!">
<span class="apexcharts-tooltip-text-y-value ${valueClasses} ${valueExtClasses}">${groupData.right}</span>
</div>
</div>
</span>
${labelMarkup}
</div>`;
});
return `<div class="${
mode === 'dark' ? 'dark ' : ''
}${wrapperClasses} ${wrapperExtClasses}">
<div class="apexcharts-tooltip-title ${titleClasses} ${titleExtClasses}">${title}</div>
${seriesGroups}
</div>`;
}
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 `<div class="${
mode === 'dark' ? 'dark ' : ''
}${wrapperClasses} ${wrapperExtClasses}">
<div class="apexcharts-tooltip-series-group flex!">
<span class="apexcharts-tooltip-marker ${markerClasses} ${markerExtClasses}" style="${markerStyles}"></span>
<span class="flex items-center">
<div class="apexcharts-tooltip-text">
<div class="apexcharts-tooltip-y-group py-0.5!">
<span class="apexcharts-tooltip-text-y-value ${valueClasses} ${valueExtClasses}">${name}${divider}</span>
</div>
</div>
</span>
<span class="apexcharts-tooltip-text-y-value ${valueClasses} ${valueExtClasses}">${val}</span>
</div>
</div>`;
}
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
? `<svg class="inline-block size-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>`
: `<svg class="inline-block size-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 17 13.5 8.5 8.5 13.5 2 7" /><polyline points="16 17 22 17 22 11" /></svg>`;
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 = `<span class="apexcharts-tooltip-text-y-label ${labelClasses} ${labelExtClasses}">${newCategory} ${
label || ''
}</span>`;
const valueMarkup =
altValue ||
`<span class="apexcharts-tooltip-text-y-value ${valueClasses} ${valueExtClasses}">${valuePrefix}${
val >= 1000 && isValueDivided
? `${val / 1000}${thousandsShortName}`
: val
}${valuePostfix}${labelDivider}</span>`;
seriesGroups += `<div class="apexcharts-tooltip-series-group ${seriesClasses} !flex order-${
i + 1
} ${seriesExtClasses}">
<span class="flex items-center">
<span class="apexcharts-tooltip-marker ${markerClasses} ${markerExtClasses}" style="background: ${
colors[i]
}"></span>
<div class="apexcharts-tooltip-text">
<div class="apexcharts-tooltip-y-group py-0.5!">
${valueMarkup}
</div>
</div>
</span>
${hasTextLabel ? labelMarkup : ''}
</div>`;
});
return `<div class="${
mode === 'dark' ? 'dark ' : ''
}${wrapperClasses} ${wrapperExtClasses}">
<div class="apexcharts-tooltip-title ${titleClasses} ${titleExtClasses}">
<span>${title}</span>
<span class="flex items-center gap-x-1 ${
!isDifferenceIsNull
? isGrowing
? 'text-green-600'
: 'text-red-600'
: ''
} ms-2">
${!isDifferenceIsNull ? icon : ''}
<span class="inline-block text-sm">
${difference.toFixed(1)}%
</span>
</span>
</div>
${seriesGroups}
</div>`;
}
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
? `<svg class="inline-block size-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>`
: `<svg class="inline-block size-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 17 13.5 8.5 8.5 13.5 2 7" /><polyline points="16 17 22 17 22 11" /></svg>`;
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 = `<span class="apexcharts-tooltip-text-y-label ${labelClasses} ${labelExtClasses}">${valuePrefix}${
val >= 1000 && isValueDivided ? `${val / 1000}${thousandsShortName}` : val
}${valuePostfix}</span>`;
seriesGroups += `<div class="apexcharts-tooltip-series-group !flex ${seriesClasses} order-${
i + 1
} ${seriesExtClasses}">
<span class="flex items-center">
<span class="apexcharts-tooltip-marker ${markerClasses} ${markerExtClasses}" style="background: ${
colors[i]
}"></span>
<div class="apexcharts-tooltip-text text-xs">
<div class="apexcharts-tooltip-y-group py-0.5!">
<span class="apexcharts-tooltip-text-y-value ${valueClasses} ${valueExtClasses}">${newCategory} ${
label || ''
}${labelDivider}</span>
</div>
</div>
</span>
${hasTextLabel ? labelMarkup : ''}
</div>`;
});
return `<div class="${
mode === 'dark' ? 'dark ' : ''
}${wrapperClasses} ${wrapperExtClasses}">
<div class="apexcharts-tooltip-title ${titleClasses} ${titleExtClasses}">
<span>${title}</span>
<span class="flex items-center gap-x-1 ${
!isDifferenceIsNull
? isGrowing
? 'text-green-600'
: 'text-red-600'
: ''
} ms-2">
${!isDifferenceIsNull ? icon : ''}
<span class="inline-block text-sm">
${difference.toFixed(1)}%
</span>
</span>
</div>
${seriesGroups}
</div>`;
}
function buildTooltipForDonut(
{ series, seriesIndex, w }: IChartDonutProps,
textColor: string[],
) {
const { globals } = w;
const { colors } = globals;
return `<div class="apexcharts-tooltip-series-group" style="background-color: ${
colors[seriesIndex]
}; display: block;">
<div class="apexcharts-tooltip-text" style="font-family: Helvetica, Arial, sans-serif; font-size: 12px;">
<div class="apexcharts-tooltip-y-group" style="color: ${
textColor[seriesIndex]
}">
<span class="apexcharts-tooltip-text-y-label">${
globals.labels[seriesIndex]
}: </span>
<span class="apexcharts-tooltip-text-y-value">${
series[seriesIndex]
}</span>
<div class="apexcharts-tooltip-y-group" style="color: ${
textColor[seriesIndex]
}">
<span class="apexcharts-tooltip-text-y-label">${
globals.labels[seriesIndex]
}: </span>
<span class="apexcharts-tooltip-text-y-value">${
series[seriesIndex]
}</span>
</div>
</div>
</div>`;
}
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));
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<HTMLElement>('.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<SVGPathElement>('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,
};