wps_js.hex_to_rgba = function (hex, opacity) { const defaultColor = '#3288D7'; if (typeof hex !== 'string' || hex[0] !== '#' || (hex.length !== 7 && hex.length !== 4)) { hex = defaultColor; } if (hex.length === 4) { hex = '#' + hex[1].repeat(2) + hex[2].repeat(2) + hex[3].repeat(2); } hex = hex.replace('#', ''); let hex_to_rgba_r = parseInt(hex.substring(0, 2), 16); let hex_to_rgba_g = parseInt(hex.substring(2, 4), 16); let hex_to_rgba_b = parseInt(hex.substring(4, 6), 16); return wps_js.rgba_to_hex(hex_to_rgba_r, hex_to_rgba_g, hex_to_rgba_b, opacity); } wps_js.rgba_to_hex = function (r, g, b, a) { let hex_r = r.toString(16).padStart(2, '0'); let hex_g = g.toString(16).padStart(2, '0'); let hex_b = b.toString(16).padStart(2, '0'); let hex_a = Math.round(a * 255).toString(16).padStart(2, '0'); return `#${hex_r}${hex_g}${hex_b}${hex_a}`; } const wpsBuildTicks = (scale) => { const ticks = scale.getTicks(); if (ticks.length > scale.options.ticks.maxTicksLimit) { const range = scale.max - scale.min; const desiredTicks = scale.options.ticks.maxTicksLimit || 1000; let stepSize = Math.ceil(range / desiredTicks); scale.options.ticks.stepSize = Math.max(1, stepSize); } else if (!scale.options.ticks.stepSize || scale.options.ticks.stepSize < 1) { scale.options.ticks.stepSize = 1; } } const chartColors = { 'total': '#27A765', 'views': '#7362BF', 'visitors': '#3288D7', 'user-visitors': '#3288D7', 'anonymous-visitors': '#7362BF', 'published': '#8AC3D0','published-contents': '#8AC3D0', 'published-products': '#8AC3D0', 'published-pages': '#8AC3D0', 'published-posts': '#8AC3D0', 'posts': '#8AC3D0' , downloads: '#3288D7', 'clicks': '#3288D7', 'Other1': '#3288D7', 'Other2': '#7362BF', 'Other3': '#8AC3D0' }; const chartTensionValues = [0.1, 0.3, 0.5, 0.7]; const getOrCreateTooltip = (chart) => { let tooltipEl = chart.canvas.parentNode.querySelector('div'); if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.classList.add('wps-chart-tooltip'); tooltipEl.style.opacity = 1; tooltipEl.style.pointerEvents = 'none'; tooltipEl.style.position = 'absolute'; tooltipEl.style.transition = 'all .1s ease'; const table = document.createElement('table'); table.style.margin = '0px'; tooltipEl.appendChild(table); chart.canvas.parentNode.appendChild(tooltipEl); } return tooltipEl; }; wps_js.setTooltipPosition = function (tooltipEl, chart, tooltip) { const { offsetLeft: chartLeft, offsetTop: chartTop, clientWidth: chartWidth, clientHeight: chartHeight } = chart.canvas; const {caretX, caretY} = tooltip; const tooltipWidth = tooltipEl.offsetWidth; const tooltipHeight = tooltipEl.offsetHeight; const margin = 16; let tooltipX = chartLeft + caretX + margin; let tooltipY = chartTop + caretY - tooltipHeight / 2; if (tooltipX + tooltipWidth + margin > chartLeft + chartWidth) { tooltipX = chartLeft + caretX - tooltipWidth - margin; } if (tooltipX < chartLeft + margin) { tooltipX = chartLeft + margin; } if (tooltipY < chartTop + margin) { tooltipY = chartTop + margin; } if (tooltipY + tooltipHeight + margin > chartTop + chartHeight) { tooltipY = chartTop + chartHeight - tooltipHeight - margin; } tooltipEl.style.opacity = 1; tooltipEl.style.left = tooltipX + 'px'; tooltipEl.style.top = tooltipY + 'px'; } const externalTooltipHandler = (context, data, dateLabels, prevDateLabels, monthTooltip, prevMonthTooltip, prevFullLabels) => { const {chart, tooltip} = context; const unitTime = chart.options.plugins.tooltip.unitTime; const tooltipEl = getOrCreateTooltip(chart); if (tooltip.opacity === 0) { tooltipEl.style.opacity = 0; return; } if (tooltip.body) { const titleLines = tooltip.title || []; const dataIndex = tooltip.dataPoints[0].dataIndex; const datasets = chart.data.datasets; let innerHtml = `
`; const phpDateFormat = wps_js.isset(wps_js.global, 'options', 'wp_date_format') ? wps_js.global['options']['wp_date_format'] : 'MM/DD/YYYY'; let momentDateFormat = phpToMomentFormat(phpDateFormat); momentDateFormat = momentDateFormat .replace(/\/YYYY|YYYY/g, '') .replace(/,\s*$/, '') .replace(/^\s*,/, '') .trim(); titleLines.forEach(title => { if (unitTime === 'day') { const label = (data.data) ? data.data.labels[dataIndex] : data.labels[dataIndex]; const {date, day} = label; // Ensure `date` and `day` are correctly extracted innerHtml += `
${moment(date).format(momentDateFormat)} (${day})
`; } else if (unitTime === 'month') { innerHtml += `
${monthTooltip[dataIndex]}
`; } else { innerHtml += `
${dateLabels[dataIndex]}
`; } }); datasets.forEach((dataset, index) => { const meta = chart.getDatasetMeta(index); const isPrevious = dataset.label.includes('(Previous)'); if (!meta.hidden && !isPrevious) { const value = dataset.data[dataIndex]; innerHtml += `
${dataset.label}
${value.toLocaleString()}
`; } const previousDataset = datasets.find(ds => ds.label === `${dataset.label} (Previous)`); if (previousDataset) { const previousMeta = chart.getDatasetMeta(datasets.indexOf(previousDataset)); const previousPeriodElement = document.querySelector('.wps-postbox-chart--previousPeriod'); const isPreviousHidden = previousPeriodElement && previousPeriodElement.classList.contains('wps-line-through'); if (!previousMeta.hidden && !isPreviousHidden) { const previousValue = previousDataset.data[dataIndex] || 0; let previousLabel = null; if (unitTime === 'day') { const prevLabelObj = prevFullLabels && prevFullLabels[dataIndex]; if (prevLabelObj) { previousLabel = `${moment(prevLabelObj.date).format(momentDateFormat)} (${prevLabelObj.day})`; } else { previousLabel = prevDateLabels[dataIndex] || 'N/A'; } } else if (unitTime === 'month') { previousLabel = prevMonthTooltip[dataIndex]; } else { previousLabel = prevDateLabels[dataIndex]; } if (previousLabel === undefined) { previousLabel = 'N/A'; // Fallback for undefined labels } innerHtml += `
${previousLabel}
${previousValue.toLocaleString()}
`; } } }); innerHtml += `
`; tooltipEl.innerHTML = innerHtml; wps_js.setTooltipPosition(tooltipEl, chart, tooltip); } }; // Custom plugin definition const drawVerticalLinePlugin = { id: 'drawVerticalLine', beforeDatasetDraw(chart) { const {ctx, scales: {x, y}, tooltip, chartArea: {top, bottom}} = chart; if (tooltip && tooltip._active && tooltip._active.length) { const xValue = tooltip._active[0].element.x; ctx.beginPath(); ctx.strokeStyle = '#A9AAAE'; ctx.lineWidth = 1; ctx.setLineDash([6, 6]); ctx.moveTo(xValue, top); ctx.lineTo(xValue, bottom); ctx.stroke(); ctx.setLineDash([]); } } }; const phpToMomentFormat = (phpFormat) => { const formatMap = { 'd': 'DD', 'j': 'D', 'S': 'Do', 'n': 'M', 'm': 'MM', 'F': 'MMM', 'M': 'MMM', 'y': 'YY', 'Y': 'YYYY' }; return phpFormat.replace(/([a-zA-Z])/g, (match) => formatMap[match] || match); } const formatDateRange = (startDate, endDate, unit, momentDateFormat, isInsideDashboardWidgets) => { const baseFormat = momentDateFormat .replace(/\/YYYY|YYYY/g, '') .replace(/\/YY|YY/g, '') .replace(/[,/\s-]/g, ' ') .trim(); const cleanFormat = baseFormat .replace(/MM|MMM/g, 'MMM') .replace(/DD|D/g, 'D') .trim(); if (unit === 'month') { const monthFormat = cleanFormat .replace(/\s*D/g, '') .replace(/YYYY|YY/g, '') .replace(/\/YY|YY/g, '') .trim(); return moment(startDate).format(monthFormat); } else { return `${moment(startDate).format(cleanFormat)} to ${moment(endDate).format(cleanFormat)}`; } } const setMonthDateRange = (startDate, endDate, momentDateFormat) => { const startDateFormat = momentDateFormat.replace(/,?\s?(YYYY|YY)[-/\s]?,?|[-/\s]?(YYYY|YY)[-/\s]?,?/g, ""); return `${moment(startDate).format(startDateFormat)} to ${moment(endDate).format(startDateFormat)}`; } const aggregateData = (labels, datasets, unit, momentDateFormat, isInsideDashboardWidgets) => { if (!labels || !labels.length || !datasets || !datasets.length) { console.error("Invalid input: labels or datasets are empty."); return { aggregatedLabels: [], aggregatedData: datasets ? datasets.map(() => []) : [], monthTooltipTitle: [], }; } const isIncompletePeriod = []; const now = moment(); if (unit === 'day') { labels.forEach(label => { const date = moment(label.date); isIncompletePeriod.push(date.isSameOrAfter(now, 'day')); }); return { aggregatedLabels: labels.map(label => label.formatted_date), aggregatedData: datasets.map(dataset => dataset.data), monthTooltipTitle: [], isIncompletePeriod }; } const aggregatedLabels = []; const aggregatedData = datasets.map(() => []); const monthTooltipTitle = []; const groupedData = {}; if (unit === 'week') { if (wps_js._('start_of_week')) { moment.updateLocale('en', { week: { dow: parseInt(wps_js._('start_of_week')) } }); } const startDate = moment(labels[0].date); const endDate = moment(labels[labels.length - 1].date); // Create an array of all weeks between start and end date const weeks = []; let currentWeekStart = startDate.clone(); while (currentWeekStart.isSameOrBefore(endDate)) { let nextWeekStart = currentWeekStart.clone().startOf('week').add(1, 'week'); let weekEnd = nextWeekStart.clone().subtract(1, 'day'); // For the last week, if it would go beyond endDate, adjust it if (weekEnd.isAfter(endDate)) { weekEnd = endDate.clone(); } weeks.push({ start: currentWeekStart.clone(), end: weekEnd, key: currentWeekStart.format('YYYY-[W]WW'), data: new Array(datasets.length).fill(0) }); // Move to next week's start currentWeekStart = nextWeekStart; } labels.forEach((label, i) => { if (label.date) { // Check if label.date is valid const date = moment(label.date); for (let week of weeks) { if (date.isBetween(week.start, week.end, 'day', '[]')) { datasets.forEach((dataset, datasetIndex) => { week.data[datasetIndex] += dataset.data[i] || 0; }); break; } } } }); // Build the output arrays weeks.forEach(week => { const label = formatDateRange(week.start, week.end, unit, momentDateFormat, isInsideDashboardWidgets); aggregatedLabels.push(label); monthTooltipTitle.push(setMonthDateRange(week.start, week.end, momentDateFormat)); week.data.forEach((total, datasetIndex) => { if (!aggregatedData[datasetIndex]) { aggregatedData[datasetIndex] = []; } aggregatedData[datasetIndex].push(total); }); }); weeks.forEach(week => { const isIncomplete = week.end.isSameOrAfter(moment(), 'day'); isIncompletePeriod.push(isIncomplete); }); } else if (unit === 'month') { const startDate = moment(labels[0].date); const endDate = moment(labels[labels.length - 1].date); let currentDate = startDate.clone(); while (currentDate.isSameOrBefore(endDate, 'month')) { const monthKey = currentDate.format('YYYY-MM'); if (!groupedData[monthKey]) { groupedData[monthKey] = { startDate: currentDate.clone().startOf('month'), endDate: currentDate.clone().endOf('month'), indices: [], }; } currentDate.add(1, 'month'); } labels.forEach((label, i) => { if (label.date) { const date = moment(label.date); const monthKey = date.format('YYYY-MM'); if (groupedData[monthKey]) { groupedData[monthKey].indices.push(i); } } }); Object.keys(groupedData).forEach(monthKey => { const {startDate, endDate, indices} = groupedData[monthKey]; const actualStartDate = moment.max(startDate, moment(labels[0].date)); const actualEndDate = moment.min(endDate, moment(labels[labels.length - 1].date)); if (!actualStartDate.isValid() || !actualEndDate.isValid()) { console.error(`Invalid date range for monthKey ${monthKey}`); return; } if (indices.length > 0) { const label = formatDateRange(actualStartDate, actualEndDate, unit, momentDateFormat, isInsideDashboardWidgets); aggregatedLabels.push(label); datasets.forEach((dataset, idx) => { const total = indices.reduce((sum, i) => sum + (dataset.data[i] || 0), 0); aggregatedData[idx].push(total); }); monthTooltipTitle.push(setMonthDateRange(actualStartDate, actualEndDate, momentDateFormat)); } }); Object.keys(groupedData).forEach(monthKey => { const isIncomplete = groupedData[monthKey].endDate.isSameOrAfter(moment(), 'day'); isIncompletePeriod.push(isIncomplete); }); } return {aggregatedLabels, aggregatedData, monthTooltipTitle, isIncompletePeriod}; } const sortTotal = (datasets) => { datasets.sort((a, b) => { if (a.slug === 'total') return -1; if (b.slug === 'total') return 1; if (a.slug === 'total-previous') return -1; if (b.slug === 'total-previous') return 1; return 0; }); } const updateLegend = (lineChart, datasets, tag_id, data) => { const chartElement = document.getElementById(tag_id); const legendContainer = chartElement.parentElement.parentElement.querySelector('.wps-postbox-chart--items'); if (legendContainer) { legendContainer.innerHTML = ''; const previousPeriod = chartElement.parentElement.parentElement.querySelector('.wps-postbox-chart--previousPeriod'); if (previousPeriod) { let foundPrevious = datasets.some(dataset => dataset.label.includes('(Previous)')); if (foundPrevious) { previousPeriod.style.display = 'flex'; previousPeriod.style.cursor = 'pointer'; if (previousPeriod._clickHandler) { previousPeriod.removeEventListener('click', previousPeriod._clickHandler); } previousPeriod._clickHandler = function (e) { e.stopPropagation(); const isPreviousHidden = previousPeriod.classList.contains('wps-line-through'); previousPeriod.classList.toggle('wps-line-through'); datasets.forEach((dataset, datasetIndex) => { if (dataset.label.includes('(Previous)')) { const meta = lineChart.getDatasetMeta(datasetIndex); meta.hidden = !isPreviousHidden; } }); const previousDataElements = legendContainer.querySelectorAll('.previous-data'); previousDataElements.forEach(elem => { if (isPreviousHidden) { elem.classList.remove('wps-line-through'); } else { elem.classList.add('wps-line-through'); } }); lineChart.update(); }; previousPeriod.addEventListener('click', previousPeriod._clickHandler); } } datasets.forEach((dataset, index) => { const isPrevious = dataset.label.includes('(Previous)'); if (!isPrevious) { const currentData = dataset.data.reduce((a, b) => Number(a) + Number(b), 0); let previousData = null; let previousDatasetIndex = null; if (data?.previousData?.datasets.length > 0) { const previousDataset = data.previousData.datasets.find((prev, prevIndex) => { if (prev.label === dataset.label) { previousDatasetIndex = prevIndex; return true; } return false; }); if (previousDataset && previousDataset.data) { previousData = previousDataset.data.reduce((a, b) => Number(a) + Number(b), 0); } } const legendItem = document.createElement('div'); legendItem.className = 'wps-postbox-chart--item'; const previousDataHTML = previousData !== null ? `
${previousData.toLocaleString()}
` : ''; legendItem.innerHTML = ` ${dataset.label}
${currentData.toLocaleString()}
${previousDataHTML}
`; const currentDataDiv = legendItem.querySelector('.current-data'); currentDataDiv.addEventListener('click', function () { const metaMain = lineChart.getDatasetMeta(index); metaMain.hidden = !metaMain.hidden; currentDataDiv.classList.toggle('wps-line-through'); lineChart.update(); }); const previousDataDiv = legendItem.querySelector('.previous-data'); if (previousDataDiv && previousDatasetIndex !== null) { previousDataDiv.addEventListener('click', function () { const metaPrevious = lineChart.data.datasets.find((dataset, dsIndex) => { return dataset.label === `${datasets[index].label} (Previous)` && lineChart.getDatasetMeta(dsIndex); }); if (metaPrevious) { const metaPreviousIndex = lineChart.data.datasets.indexOf(metaPrevious); const metaPreviousVisibility = lineChart.getDatasetMeta(metaPreviousIndex); metaPreviousVisibility.hidden = !metaPreviousVisibility.hidden; previousDataDiv.classList.toggle('wps-line-through'); const allPreviousData = legendContainer.querySelectorAll('.previous-data'); const allHaveLineThrough = Array.from(allPreviousData).every(el => el.classList.contains('wps-line-through')); if (previousPeriod) { if (allHaveLineThrough) { previousPeriod.classList.add('wps-line-through'); } else { previousPeriod.classList.remove('wps-line-through'); } } lineChart.update(); } }); } legendContainer.appendChild(legendItem); } }); } } const deepCopy = (obj) => JSON.parse(JSON.stringify(obj)); const chartInstances = {}; const getDisplayTextForUnitTime = (unitTime, tag_id) => { const select = document.querySelector(`#${tag_id}`).closest('.o-wrap').querySelector('.js-unitTimeSelect'); if (select) { const option = select.querySelector(`.wps-unit-time-chart__option[data-value="${unitTime}"]`); if (option) { return option.textContent.trim(); } } return unitTime; } wps_js.new_line_chart = function (data, tag_id, newOptions = null, type = 'line') { sortTotal(data.data.datasets); const realdata = deepCopy(data); const phpDateFormat = wps_js.isset(wps_js.global, 'options', 'wp_date_format') ? wps_js.global['options']['wp_date_format'] : 'MM/DD/YYYY'; let momentDateFormat = phpToMomentFormat(phpDateFormat); const isInsideDashboardWidgets = document.getElementById(tag_id).closest('#dashboard-widgets') !== null; // Determine the initial unitTime const length = data.data.labels.map(dateObj => dateObj.formatted_date).length; const threshold = type === 'performance' ? 30 : 60; let unitTime = length <= threshold ? 'day' : length <= 180 ? 'week' : 'month'; const datasets = []; // Check if there is only one data point const isSingleDataPoint = data.data.labels.length === 1; const select = document.querySelector(`#${tag_id}`).closest('.o-wrap').querySelector('.js-unitTimeSelect'); if (select) { const selectedItem = select.querySelector('.wps-unit-time-chart__selected-item'); if (selectedItem) { selectedItem.textContent = getDisplayTextForUnitTime(unitTime, tag_id); } const options = select.querySelectorAll('.wps-unit-time-chart__option'); options.forEach(opt => { if (opt.getAttribute('data-value') === unitTime) { opt.classList.add('selected'); } else { opt.classList.remove('selected'); } }); } const day = aggregateData(realdata.data.labels, realdata.data.datasets, 'day', momentDateFormat, isInsideDashboardWidgets); const week = aggregateData(realdata.data.labels, realdata.data.datasets, 'week', momentDateFormat, isInsideDashboardWidgets); const month = aggregateData(realdata.data.labels, realdata.data.datasets, 'month', momentDateFormat, isInsideDashboardWidgets); const prevDay = realdata?.previousData ? aggregateData(realdata.previousData.labels, realdata.previousData.datasets, 'day', momentDateFormat, isInsideDashboardWidgets) : null; const prevWeek = realdata.previousData ? aggregateData(realdata.previousData.labels, realdata.previousData.datasets, 'week', momentDateFormat, isInsideDashboardWidgets) : null; const prevMonth = realdata.previousData ? aggregateData(realdata.previousData.labels, realdata.previousData.datasets, 'month', momentDateFormat, isInsideDashboardWidgets) : null; // Initialize dateLabels based on the selected unitTime let dateLabels = unitTime === 'day' ? day.aggregatedLabels : unitTime === 'week' ? week.aggregatedLabels : month.aggregatedLabels; // Initialize monthTooltip and prevMonthTooltip let monthTooltip = unitTime === 'day' ? day.monthTooltipTitle : unitTime === 'week' ? week.monthTooltipTitle : month.monthTooltipTitle; let prevMonthTooltip = unitTime === 'day' ? (prevDay ? prevDay.monthTooltipTitle : []) : unitTime === 'week' ? (prevWeek ? prevWeek.monthTooltipTitle : []) : (prevMonth ? prevMonth.monthTooltipTitle : []); let prevDateLabels = []; let prevAggregatedData = []; if (prevWeek) { prevDateLabels = prevWeek.aggregatedLabels; prevAggregatedData = prevWeek.aggregatedData; while (prevDateLabels.length < dateLabels.length) { prevDateLabels.push("N/A"); prevAggregatedData.forEach(dataset => dataset.push(0)); } } else { prevDateLabels = Array(dateLabels.length).fill("N/A"); if (datasets && datasets.length > 0 && Array.isArray(datasets)) { prevAggregatedData = datasets.map(() => Array(dateLabels.length).fill(0)); } } function updateChart(unitTime) { const displayText = getDisplayTextForUnitTime(unitTime, tag_id); const chartElement = document.getElementById(tag_id); const chartContainer = chartElement.parentElement.parentElement.querySelector('.wps-postbox-chart--data'); const previousPeriodElement = chartContainer?.querySelector('.wps-postbox-chart--previousPeriod'); if (previousPeriodElement) { previousPeriodElement.classList.remove('wps-line-through'); } const select = document.querySelector(`#${tag_id}`).closest('.o-wrap').querySelector('.js-unitTimeSelect'); if (select) { const selectedItem = select.querySelector('.wps-unit-time-chart__selected-item'); if (selectedItem) { selectedItem.textContent = displayText; } } let aggregatedData, prevAggregatedData; switch (unitTime) { case 'day': aggregatedData = day; prevAggregatedData = prevDay; break; case 'week': aggregatedData = week; prevAggregatedData = prevWeek; break; case 'month': aggregatedData = month; prevAggregatedData = prevMonth; break; default: aggregatedData = day; prevAggregatedData = prevDay; } dateLabels = aggregatedData.aggregatedLabels; monthTooltip = aggregatedData.monthTooltipTitle; prevDateLabels = prevAggregatedData ? prevAggregatedData.aggregatedLabels : []; prevMonthTooltip = prevAggregatedData ? prevAggregatedData.monthTooltipTitle : []; // If prevDateLabels is empty, fill it with "N/A" for each month if (prevDateLabels.length === 0 && dateLabels.length > 0) { prevDateLabels = Array(dateLabels.length).fill("N/A"); } const datasets = data.data.datasets.map((dataset, idx) => { const datasetType = dataset.type || (type === 'performance' && idx === 2 ? 'bar' : 'line'); return { ...dataset, type: datasetType, // Set the type explicitly data: aggregatedData.aggregatedData[idx], borderColor: chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`], backgroundColor: chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`], fill: false, yAxisID: datasetType === 'bar' ? 'y1' : 'y', // Use y1 for bar, y for line borderWidth: datasetType === 'line' ? 2 : undefined, pointRadius: datasetType === 'line' ? dateLabels.length === 1 ? 5 : 0 : undefined, pointBorderColor: datasetType === 'line' ? 'transparent' : undefined, pointBackgroundColor: datasetType === 'line' ? chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`] : undefined, pointBorderWidth: datasetType === 'line' ? 2 : undefined, hoverPointRadius: datasetType === 'line' ? 6 : undefined, hoverPointBorderColor: datasetType === 'line' ? '#fff' : undefined, hoverPointBackgroundColor: chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`], hoverPointBorderWidth: datasetType === 'line' ? 4 : undefined, tension: datasetType === 'line' ? chartTensionValues[idx % chartTensionValues.length] : undefined, hitRadius: 10, meta: { incompletePeriods: aggregatedData.isIncompletePeriod || [] }, segment: datasetType === 'line' ? { borderDash: (ctx) => { const incompletePeriods = ctx.chart.data.datasets[ctx.datasetIndex]?.meta?.incompletePeriods || []; const currentIncomplete = incompletePeriods[ctx.p1DataIndex]; const previousIncomplete = incompletePeriods[ctx.p0DataIndex]; // Dash if either end of segment is in incomplete period if (currentIncomplete || previousIncomplete) { return [5, 5]; } return undefined; } } : undefined }; }); if (prevAggregatedData) { data.previousData.datasets.forEach((dataset, idx) => { datasets.push({ ...dataset, type: 'line', // Previous datasets are always lines label: `${dataset.label} (Previous)`, data: prevAggregatedData.aggregatedData[idx], borderColor: wps_js.hex_to_rgba(chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`], 0.7), hoverBorderColor: chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`], backgroundColor: chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`], fill: false, yAxisID: 'y', borderWidth: 1, borderDash: [5, 5], pointRadius: aggregatedData.aggregatedLabels.length === 1 ? 5 : 0, pointBorderColor: 'transparent', pointBackgroundColor: chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`], pointBorderWidth: 2, hoverPointRadius: 6, hoverPointBorderColor: '#fff', hoverPointBackgroundColor: chartColors[data.data.datasets[idx].slug] || chartColors[`Other${idx}`], hoverPointBorderWidth: 4, tension: chartTensionValues[idx % chartTensionValues.length], hitRadius: 10 }); }); } // Calculate max values for y and y1 axes const yAxisData = datasets .filter(dataset => dataset.yAxisID === 'y') .flatMap(dataset => dataset.data) .filter(val => typeof val === 'number' && !isNaN(val)); const y1AxisData = datasets .filter(dataset => dataset.yAxisID === 'y1') .flatMap(dataset => dataset.data) .filter(val => typeof val === 'number' && !isNaN(val)); const yMax = yAxisData.length > 0 ? Math.max(...yAxisData) : 0; const y1Max = y1AxisData.length > 0 ? Math.max(...y1AxisData) : 0; // Set dynamic stepSize based on max values const maxTicksY = isInsideDashboardWidgets ? 4 : 7; const stepSizeY = yMax > 0 ? Math.ceil(yMax / maxTicksY) : 1; const maxTicksY1 = 7; const stepSizeY1 = y1Max > 0 ? Math.ceil(y1Max / maxTicksY1) : 1; lineChart.options.scales.y.ticks.stepSize = Math.max(1, stepSizeY); if (lineChart.options.scales.y1) { lineChart.options.scales.y1.ticks.stepSize = Math.max(1, stepSizeY1); } lineChart.data.labels = dateLabels; lineChart.options.scales.x.offset = lineChart.data.labels.length === 1; lineChart.data.datasets = datasets; lineChart.options.scales.x.ticks.maxTicksLimit = isInsideDashboardWidgets ? unitTime === 'week' ? 2 : 4 : unitTime === 'week' ? 3 : unitTime === 'month' ? 7 : 9; lineChart.options.plugins.tooltip.unitTime = unitTime; lineChart.options.plugins.tooltip.external = (context) => externalTooltipHandler(context, realdata, dateLabels, prevDateLabels, monthTooltip, prevMonthTooltip, realdata.previousData ? realdata.previousData.labels : []); updateLegend(lineChart, datasets, tag_id, data); lineChart.update(); } let ctx_line = document.getElementById(tag_id).getContext('2d'); Object.keys(data.data.datasets).forEach((key, index) => { let color = chartColors[data.data.datasets[key].slug] || chartColors[`Other${index + 1}`]; let tension = chartTensionValues[index % chartTensionValues.length]; let datasetType = 'line'; // Default to line if (type === 'performance' && index === 2) { datasetType = 'bar'; // Set to bar for index 2 in performance charts } const dataset = { type: datasetType, label: data.data.datasets[key].label, data: data.data.datasets[key].data, backgroundColor: color, hoverBackgroundColor: color, hoverPointBackgroundColor: color, yAxisID: datasetType === 'bar' ? 'y1' : 'y', // Use y1 for bar, y for line }; if (datasetType === 'line') { dataset.borderColor = color; dataset.fill = false; dataset.borderWidth = 2; dataset.pointRadius = 0; dataset.pointBorderColor = 'transparent'; dataset.pointBackgroundColor = color; dataset.pointBorderWidth = 2; dataset.hoverPointRadius = 6; dataset.hoverPointBorderColor = '#fff'; dataset.hoverPointBorderWidth = 4; dataset.tension = tension; dataset.hitRadius = 10; } datasets.push(dataset); }); if (data?.previousData) { Object.keys(data.previousData.datasets).forEach((key, index) => { let color = chartColors[data.previousData.datasets[key].slug] || chartColors[`Other${index}`]; let tension = chartTensionValues[index % chartTensionValues.length]; datasets.push({ type: 'line', label: `${data.previousData.datasets[key].label} (Previous)`, data: data.previousData.datasets[key].data, borderColor: wps_js.hex_to_rgba(color, 0.7), hoverBorderColor: color, backgroundColor: color, fill: false, yAxisID: 'y', borderWidth: 1, borderDash: [5, 5], pointRadius: 0, pointBorderColor: 'transparent', pointBackgroundColor: color, pointBorderWidth: 2, hoverPointRadius: 6, hoverPointBorderColor: '#fff', hoverPointBackgroundColor: color, hoverPointBorderWidth: 4, tension: tension, hitRadius: 10 }); }); } const defaultOptions = { maintainAspectRatio: false, resizeDelay: 200, animation: {duration: 0}, responsive: true, interaction: {intersect: false, mode: 'index'}, plugins: { legend: false, tooltip: { enabled: false, external: (context) => externalTooltipHandler(context, realdata, dateLabels, prevDateLabels, monthTooltip, prevMonthTooltip, realdata.previousData ? realdata.previousData.labels : []), unitTime: unitTime, // Set initial unitTime }, }, scales: { x: { offset: isSingleDataPoint, grid: {display: false, drawBorder: false, tickLength: 0, drawTicks: false}, border: {color: 'transparent', width: 0}, ticks: { align: 'inner', autoSkip: true, maxTicksLimit: isInsideDashboardWidgets ? (unitTime === 'week' ? 2 : 4) : (unitTime === 'week' ? 3 : unitTime === 'month' ? 7 : 9), font: { color: '#898A8E', style: 'italic', weight: 'lighter', size: isInsideDashboardWidgets ? (unitTime === 'week' ? 9 : 11) : (unitTime === 'week' ? 11 : 13) }, padding: 8, } }, y: { min: 0, suggestedMax: 4, ticks: { autoSkip: true, maxTicksLimit: isInsideDashboardWidgets ? 4 : 7, fontColor: '#898A8E', fontSize: 13, fontStyle: 'italic', fontWeight: 'lighter', padding: 8, lineHeight: 15, callback: renderFormatNum, }, afterBuildTicks: wpsBuildTicks, border: {color: 'transparent', width: 0}, type: 'linear', position: 'right', grid: {display: true, tickMarkLength: 0, drawBorder: false, tickColor: '#EEEFF1', color: '#EEEFF1'}, gridLines: {drawTicks: false}, title: {display: false}, } }, }; if (type === 'performance' && data.data.datasets.length > 2) { defaultOptions.scales.y1 = { type: 'linear', position: 'left', border: {color: 'transparent', width: 0}, grid: {display: false, drawBorder: false, tickLength: 0}, ticks: { autoSkip: true, maxTicksLimit: 7, fontColor: '#898A8E', fontSize: 13, fontStyle: 'italic', fontFamily: '"Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', fontWeight: 'lighter', padding: 8, lineHeight: 15, callback: renderFormatNum, }, afterBuildTicks: wpsBuildTicks, title: { display: true, text: `${wps_js._('published')} Posts`, color: '#898A8E', fontSize: 13 } }; defaultOptions.scales.y = { border: {color: 'transparent', width: 0}, ticks: { autoSkip: true, maxTicksLimit: 9, fontColor: '#898A8E', fontSize: 13, fontStyle: 'italic', fontFamily: '"Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', fontWeight: 'lighter', padding: 8, lineHeight: 15, callback: renderFormatNum, }, position: 'right', grid: {display: true, borderDash: [5, 5], tickColor: '#EEEFF1', color: '#EEEFF1'}, title: { display: true, text: wps_js._('visits'), color: '#898A8E', fontSize: 13, } }; } const lineChart = new Chart(ctx_line, { type: type === 'performance' && data.data.datasets.length > 2 ? 'bar' : 'line', data: {labels: [], datasets: []}, plugins: [drawVerticalLinePlugin], options: Object.assign({}, defaultOptions, newOptions) }); updateChart(unitTime); chartInstances[tag_id] = {chart: lineChart, updateChart: updateChart}; return chartInstances[tag_id]; }; document.body.addEventListener('click', function (event) { const select = event.target.closest('.wps-unit-time-chart__selected-item'); const option = event.target.closest('.wps-unit-time-chart__option'); if (select) { if (!option) { document.querySelectorAll('.js-unitTimeSelect.open').forEach(openSelect => { if (openSelect !== select) { openSelect.parentElement.classList.remove('open'); } }); select.parentElement.classList.toggle('open'); } event.stopImmediatePropagation(); } else if (option) { const select = option.closest('.js-unitTimeSelect'); const selectedValue = option.getAttribute('data-value'); if (select) { const selectedItem = select.querySelector('.wps-unit-time-chart__selected-item'); if (selectedItem) { selectedItem.textContent = option.textContent.trim(); } const options = select.querySelectorAll('.wps-unit-time-chart__option'); options.forEach(opt => opt.classList.remove('selected')); option.classList.add('selected'); select.classList.remove('open'); const chartContainer = select.closest('.o-wrap').querySelector('.wps-postbox-chart--container'); const canvas = chartContainer.querySelector('canvas'); const canvas_id = canvas.getAttribute('id'); if (chartInstances[canvas_id]) { chartInstances[canvas_id].updateChart(selectedValue); } } event.stopImmediatePropagation(); } else { document.querySelectorAll('.js-unitTimeSelect.open').forEach(openSelect => { openSelect.classList.remove('open'); }); } }); window.renderWPSLineChart = function (chartId, data ,newOptions) { const chartItem = document.getElementById(chartId); if (chartItem) { const parentElement = jQuery(`#${chartId}`).parent(); const placeholder = wps_js.rectangle_placeholder(); parentElement.append(placeholder); if (!data?.data?.datasets || data.data.datasets.length === 0) { parentElement.html(wps_js.no_results()); jQuery('.wps-ph-item').remove(); } else { wps_js.new_line_chart(data, chartId, newOptions); jQuery('.wps-ph-item').remove(); jQuery('.wps-postbox-chart--data').removeClass('c-chart__wps-skeleton--legend'); parentElement.removeClass('c-chart__wps-skeleton'); } } }