// --- GLOBAL PALETTE LOGIC --- let globalPalette = {}; let globalLabels = []; function collectAllLabels() { // Donut ve line chartlarda geçen tüm benzersiz isimleri topla const donutLabels = Array.from(document.querySelectorAll('.donutWrapper')) .flatMap(wrapper => JSON.parse(wrapper.getAttribute('data-labels') || '[]')); const lineLabels = Array.from(document.querySelectorAll('.lineWrapper')) .flatMap(wrapper => { const series = JSON.parse(wrapper.getAttribute('data-series') || '[]'); return series.map(s => s.name); }); // Sıralı ve benzersiz globalLabels = Array.from(new Set([...donutLabels, ...lineLabels])); } // Rastgele renk üreten fonksiyon (label sayısına göre) function generatePalette(labels) { const colors = []; const goldenRatio = 0.618033988749895; let h = 0.13; // sabit başlat, her yüklemede aynı renkler için for (let i = 0; i < labels.length; i++) { h += goldenRatio; h %= 1; const color = `hsl(${Math.floor(h * 360)}, 65%, 55%)`; colors.push(color); } const palette = {}; labels.forEach((label, i) => { palette[label] = colors[i]; }); return palette; } // Chart instance map for resizing const chartInstances = new WeakMap(); // Helper: Remove all children of an element function clearElement(el) { while (el.firstChild) el.removeChild(el.firstChild); } // Donut Chart Renderer function renderDonutChart(wrapper) { clearElement(wrapper); const title = wrapper.getAttribute('data-title'); const labels = JSON.parse(wrapper.getAttribute('data-labels')); const values = JSON.parse(wrapper.getAttribute('data-values')); const total = values.reduce((a,b) => a+b, 0); const percentages = values.map(v => total ? (v/total*100) : 0); // Global palette kullan const palette = globalPalette; // Title const titleEl = document.createElement('h3'); titleEl.textContent = title; titleEl.style.textAlign = 'center'; titleEl.style.margin = '0 0 10px 0'; titleEl.style.color = '#152935'; wrapper.appendChild(titleEl); // Responsive size (centered vertically) const size = Math.min(wrapper.offsetWidth || 320, 320); // Center canvas vertically using a flex container const flexCenter = document.createElement('div'); flexCenter.style.display = 'flex'; flexCenter.style.flexDirection = 'column'; flexCenter.style.justifyContent = 'center'; flexCenter.style.alignItems = 'center'; flexCenter.style.height = '100%'; flexCenter.style.flex = '1 1 auto'; // Canvas const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; canvas.style.width = `${size}px`; canvas.style.height = `${size}px`; flexCenter.appendChild(canvas); wrapper.appendChild(flexCenter); // Chart.js Donut const chart = new Chart(canvas.getContext('2d'), { type: 'doughnut', data: { labels, datasets: [{ data: values, backgroundColor: labels.map(label => palette[label]), borderWidth: 2, borderColor: "#fff" }] }, options: { cutout: '65%', plugins: { legend: { display: false } }, responsive: false, maintainAspectRatio: false } }); chartInstances.set(wrapper, chart); } // Line Chart Renderer function renderLineChart(wrapper) { clearElement(wrapper); const title = wrapper.getAttribute('data-title'); const labels = JSON.parse(wrapper.getAttribute('data-labels')); const series = JSON.parse(wrapper.getAttribute('data-series')); // Global palette kullan const palette = globalPalette; // Set specific colors for titles based on chart type let titleColor = '#152935'; let lineColor = '#152935'; let lineStyle = []; if (title === 'Media Signal Volume') { lineColor = '#1B57D1'; // Dotted line for Media Signal Volume lineStyle = [5, 5]; } else if (title === 'Commercial Signal Count') { lineColor = '#FF832B'; // Solid line for Commercial Signal Count (default) lineStyle = []; } // Title const titleEl = document.createElement('h3'); titleEl.textContent = title; titleEl.style.textAlign = 'center'; titleEl.style.margin = '0 0 10px 0'; titleEl.style.color = titleColor; wrapper.appendChild(titleEl); // Scrollable container for chart and legend const scrollContainer = document.createElement('div'); // scrollContainer.style.overflowX = 'auto'; scrollContainer.style.maxWidth = '100%'; wrapper.appendChild(scrollContainer); // Chart and legend container (flex) const container = document.createElement('div'); container.style.display = 'flex'; container.style.alignItems = 'flex-start'; container.style.gap = '24px'; scrollContainer.appendChild(container); // Store original data for filtering const originalLabels = [...labels]; const originalSeries = series.map(s => ({...s, data: [...s.data]})); let filteredLabels = [...labels]; let filteredSeries = series.map(s => ({...s, data: [...s.data]})); // Line chart dimensions const parentDiv = wrapper.parentElement; const parentWidth = parentDiv ? parentDiv.offsetWidth : 1000; const chartWidth = '100%'; const chartHeight = 320; // Chart wrapper with relative positioning for zoom sliders const chartWrapper = document.createElement('div'); chartWrapper.style.position = 'relative'; chartWrapper.style.flex = '1'; chartWrapper.style.minWidth = '0'; // Zoom containers will be positioned after chart is created // We'll use Chart.js layout callback to get accurate positions let xZoomContainer, yZoomContainer; // Function to create zoom controls function createZoomControls() { // Add track styling if not already added if (!document.querySelector('style[data-chart-sliders]')) { const style = document.createElement('style'); style.setAttribute('data-chart-sliders', 'true'); style.textContent = ` input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #1B57D1; cursor: pointer; border: 2px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.2); margin-top: -6px; position: relative; } input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: #1B57D1; cursor: pointer; border: 2px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.2); position: relative; } input[type="range"]::-webkit-slider-runnable-track { height: 4px; background: transparent; border-radius: 2px; } input[type="range"]::-moz-range-track { height: 4px; background: transparent; border-radius: 2px; } `; document.head.appendChild(style); } // Get chart layout to position zoom controls accurately const chartArea = chart.chartArea; if (!chartArea) return; // Get scales to calculate padding for axis labels const xScale = chart.scales.x; const yScale = chart.scales.y; // X axis labels are rotated 90 degrees, so they need space below // Estimate space needed for rotated x-axis labels (typically 60-80px) const xAxisLabelHeight = 70; // Space for rotated x-axis labels // Y axis labels are on the left, estimate width needed (typically 50-60px) const yAxisLabelWidth = 55; // Space for y-axis labels on left // Calculate center position for X zoom to align with x-axis // X-axis is centered at chartArea, so we need to center the zoom slider accordingly const xAxisCenterY = chartArea.bottom + (xAxisLabelHeight / 2); // Center of x-axis labels area // X-axis zoom slider container - positioned BELOW x-axis labels (which are rotated 90deg) // X axis labels are rotated 90 degrees and positioned below chartArea.bottom // We need to place zoom axis below the labels, centered vertically xZoomContainer = document.createElement('div'); xZoomContainer.style.position = 'absolute'; xZoomContainer.style.top = `${chartArea.bottom + xAxisLabelHeight}px`; // Below x-axis labels (rotated 90deg) xZoomContainer.style.left = `${chartArea.left}px`; // Align with chart area start xZoomContainer.style.width = `${chartArea.right - chartArea.left}px`; xZoomContainer.style.height = '20px'; xZoomContainer.style.display = 'flex'; xZoomContainer.style.alignItems = 'center'; xZoomContainer.style.justifyContent = 'flex-start'; xZoomContainer.style.padding = '0 8px'; xZoomContainer.style.zIndex = '10'; xZoomContainer.style.pointerEvents = 'none'; // Don't block chart interactions const xZoomLabel = document.createElement('span'); xZoomLabel.textContent = 'X Zoom:'; xZoomLabel.style.fontSize = '11px'; xZoomLabel.style.color = '#152935'; xZoomLabel.style.marginRight = '6px'; xZoomLabel.style.whiteSpace = 'nowrap'; xZoomLabel.style.pointerEvents = 'none'; const xZoomSliderWrapper = document.createElement('div'); xZoomSliderWrapper.style.position = 'relative'; xZoomSliderWrapper.style.flex = '1'; xZoomSliderWrapper.style.height = '20px'; xZoomSliderWrapper.style.display = 'flex'; xZoomSliderWrapper.style.alignItems = 'center'; xZoomSliderWrapper.style.pointerEvents = 'auto'; // Allow slider interaction const xZoomSliderMin = document.createElement('input'); xZoomSliderMin.type = 'range'; xZoomSliderMin.min = '0'; xZoomSliderMin.max = '100'; xZoomSliderMin.value = '0'; xZoomSliderMin.style.position = 'absolute'; xZoomSliderMin.style.width = '100%'; xZoomSliderMin.style.height = '20px'; // Increased height for better hit area xZoomSliderMin.style.cursor = 'pointer'; xZoomSliderMin.style.zIndex = '2'; xZoomSliderMin.style.pointerEvents = 'auto'; xZoomSliderMin.style.margin = '0'; xZoomSliderMin.style.top = '50%'; xZoomSliderMin.style.transform = 'translateY(-50%)'; xZoomSliderMin.style.background = 'transparent'; // Make track area transparent const xZoomSliderMax = document.createElement('input'); xZoomSliderMax.type = 'range'; xZoomSliderMax.min = '0'; xZoomSliderMax.max = '100'; xZoomSliderMax.value = '100'; xZoomSliderMax.style.position = 'absolute'; xZoomSliderMax.style.width = '100%'; xZoomSliderMax.style.height = '20px'; // Increased height for better hit area xZoomSliderMax.style.cursor = 'pointer'; xZoomSliderMax.style.zIndex = '3'; xZoomSliderMax.style.pointerEvents = 'auto'; xZoomSliderMax.style.margin = '0'; xZoomSliderMax.style.top = '50%'; xZoomSliderMax.style.transform = 'translateY(-50%)'; xZoomSliderMax.style.background = 'transparent'; // Make track area transparent [xZoomSliderMin, xZoomSliderMax].forEach(slider => { slider.style.webkitAppearance = 'none'; slider.style.appearance = 'none'; slider.style.background = 'transparent'; slider.style.outline = 'none'; slider.style.cursor = 'pointer'; }); // Make both sliders always interactive - use mouse position to determine active slider let xSliderActive = null; xZoomSliderWrapper.addEventListener('mousemove', function(e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const minPercent = parseFloat(xZoomSliderMin.value); const maxPercent = parseFloat(xZoomSliderMax.value); const sliderWidth = rect.width; const minPos = (minPercent / 100) * sliderWidth; const maxPos = (maxPercent / 100) * sliderWidth; // Determine which slider is closer to mouse position const distToMin = Math.abs(x - minPos); const distToMax = Math.abs(x - maxPos); if (distToMin < distToMax && distToMin < 30) { xSliderActive = 'min'; xZoomSliderMin.style.zIndex = '5'; xZoomSliderMax.style.zIndex = '3'; } else if (distToMax < 30) { xSliderActive = 'max'; xZoomSliderMax.style.zIndex = '5'; xZoomSliderMin.style.zIndex = '2'; } }); xZoomSliderMin.addEventListener('mousedown', function(e) { e.stopPropagation(); xSliderActive = 'min'; xZoomSliderMin.style.zIndex = '5'; xZoomSliderMax.style.zIndex = '3'; }); xZoomSliderMax.addEventListener('mousedown', function(e) { e.stopPropagation(); xSliderActive = 'max'; xZoomSliderMax.style.zIndex = '5'; xZoomSliderMin.style.zIndex = '2'; }); // Reset z-index when mouse is released const resetXZIndex = function() { xZoomSliderMin.style.zIndex = '2'; xZoomSliderMax.style.zIndex = '3'; xSliderActive = null; }; document.addEventListener('mouseup', resetXZIndex); xZoomSliderWrapper.addEventListener('mouseleave', resetXZIndex); xZoomSliderWrapper.appendChild(xZoomSliderMin); xZoomSliderWrapper.appendChild(xZoomSliderMax); xZoomContainer.appendChild(xZoomLabel); xZoomContainer.appendChild(xZoomSliderWrapper); chartWrapper.appendChild(xZoomContainer); // Calculate center position for Y zoom to align with y-axis // Y-axis is centered at chartArea.left, so we need to center the zoom slider accordingly const yAxisCenterX = chartArea.left - (yAxisLabelWidth / 2); // Center of y-axis labels area // Y-axis zoom slider container - positioned to the LEFT of y-axis labels, centered yZoomContainer = document.createElement('div'); yZoomContainer.style.position = 'absolute'; yZoomContainer.style.left = `${chartArea.left - yAxisLabelWidth - 20}px`; // Left of y-axis labels yZoomContainer.style.top = `${chartArea.top}px`; // Align with top of chart area yZoomContainer.style.width = '20px'; yZoomContainer.style.height = `${chartArea.bottom - chartArea.top}px`; yZoomContainer.style.display = 'flex'; yZoomContainer.style.flexDirection = 'column'; yZoomContainer.style.alignItems = 'center'; yZoomContainer.style.justifyContent = 'center'; yZoomContainer.style.padding = '0'; yZoomContainer.style.zIndex = '10'; yZoomContainer.style.pointerEvents = 'none'; // Don't block chart interactions const yZoomLabel = document.createElement('span'); yZoomLabel.textContent = 'Y Zoom:'; yZoomLabel.style.fontSize = '11px'; yZoomLabel.style.color = '#152935'; yZoomLabel.style.marginBottom = '6px'; yZoomLabel.style.writingMode = 'vertical-rl'; yZoomLabel.style.textOrientation = 'mixed'; yZoomLabel.style.whiteSpace = 'nowrap'; yZoomLabel.style.pointerEvents = 'none'; const yZoomSliderWrapper = document.createElement('div'); yZoomSliderWrapper.style.position = 'relative'; yZoomSliderWrapper.style.width = `${chartArea.bottom - chartArea.top}px`; yZoomSliderWrapper.style.height = '20px'; yZoomSliderWrapper.style.transform = 'rotate(-90deg)'; yZoomSliderWrapper.style.transformOrigin = 'center'; yZoomSliderWrapper.style.display = 'flex'; yZoomSliderWrapper.style.alignItems = 'center'; yZoomSliderWrapper.style.pointerEvents = 'auto'; // Allow slider interaction const yZoomSliderMin = document.createElement('input'); yZoomSliderMin.type = 'range'; yZoomSliderMin.min = '0'; yZoomSliderMin.max = '100'; yZoomSliderMin.value = '0'; yZoomSliderMin.style.position = 'absolute'; yZoomSliderMin.style.width = `${chartArea.bottom - chartArea.top}px`; yZoomSliderMin.style.height = '20px'; // Increased height for better hit area yZoomSliderMin.style.cursor = 'pointer'; yZoomSliderMin.style.zIndex = '2'; yZoomSliderMin.style.pointerEvents = 'auto'; yZoomSliderMin.style.margin = '0'; yZoomSliderMin.style.top = '0'; yZoomSliderMin.style.left = '0'; yZoomSliderMin.style.background = 'transparent'; // Make track area transparent const yZoomSliderMax = document.createElement('input'); yZoomSliderMax.type = 'range'; yZoomSliderMax.min = '0'; yZoomSliderMax.max = '100'; yZoomSliderMax.value = '100'; yZoomSliderMax.style.position = 'absolute'; yZoomSliderMax.style.width = `${chartArea.bottom - chartArea.top}px`; yZoomSliderMax.style.height = '20px'; // Increased height for better hit area yZoomSliderMax.style.cursor = 'pointer'; yZoomSliderMax.style.zIndex = '3'; yZoomSliderMax.style.pointerEvents = 'auto'; yZoomSliderMax.style.margin = '0'; yZoomSliderMax.style.top = '0'; yZoomSliderMax.style.left = '0'; yZoomSliderMax.style.background = 'transparent'; // Make track area transparent [yZoomSliderMin, yZoomSliderMax].forEach(slider => { slider.style.webkitAppearance = 'none'; slider.style.appearance = 'none'; slider.style.background = 'transparent'; slider.style.outline = 'none'; slider.style.cursor = 'pointer'; }); // Make both sliders always interactive - use mouse position to determine active slider // Note: Y slider is rotated -90deg, so we need to account for that let ySliderActive = null; yZoomSliderWrapper.addEventListener('mousemove', function(e) { const rect = this.getBoundingClientRect(); // Since the slider is rotated -90deg, we need to calculate relative to the rotated coordinate system // The slider runs vertically, so we use clientY instead of clientX const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const mouseX = e.clientX - centerX; const mouseY = e.clientY - centerY; // Rotate back to get the position along the slider (which is now vertical) const sliderPos = mouseY; // After rotation, Y becomes the slider position const sliderHeight = rect.height; const minPercent = parseFloat(yZoomSliderMin.value); const maxPercent = parseFloat(yZoomSliderMax.value); const minPos = (minPercent / 100) * sliderHeight - sliderHeight / 2; const maxPos = (maxPercent / 100) * sliderHeight - sliderHeight / 2; // Determine which slider is closer const distToMin = Math.abs(sliderPos - minPos); const distToMax = Math.abs(sliderPos - maxPos); if (distToMin < distToMax && distToMin < 30) { ySliderActive = 'min'; yZoomSliderMin.style.zIndex = '5'; yZoomSliderMax.style.zIndex = '3'; } else if (distToMax < 30) { ySliderActive = 'max'; yZoomSliderMax.style.zIndex = '5'; yZoomSliderMin.style.zIndex = '2'; } }); yZoomSliderMin.addEventListener('mousedown', function(e) { e.stopPropagation(); ySliderActive = 'min'; yZoomSliderMin.style.zIndex = '5'; yZoomSliderMax.style.zIndex = '3'; }); yZoomSliderMax.addEventListener('mousedown', function(e) { e.stopPropagation(); ySliderActive = 'max'; yZoomSliderMax.style.zIndex = '5'; yZoomSliderMin.style.zIndex = '2'; }); // Reset z-index when mouse is released const resetYZIndex = function() { yZoomSliderMin.style.zIndex = '2'; yZoomSliderMax.style.zIndex = '3'; ySliderActive = null; }; document.addEventListener('mouseup', resetYZIndex); yZoomSliderWrapper.addEventListener('mouseleave', resetYZIndex); yZoomSliderWrapper.appendChild(yZoomSliderMin); yZoomSliderWrapper.appendChild(yZoomSliderMax); yZoomContainer.appendChild(yZoomLabel); yZoomContainer.appendChild(yZoomSliderWrapper); chartWrapper.appendChild(yZoomContainer); // Store slider references - use the variables we just created // These are the actual slider elements we created above const xSliderMinEl = xZoomContainer.querySelector('input[type="range"]:first-of-type'); const xSliderMaxEl = xZoomContainer.querySelector('input[type="range"]:last-of-type'); const ySliderMinEl = yZoomContainer.querySelector('input[type="range"]:first-of-type'); const ySliderMaxEl = yZoomContainer.querySelector('input[type="range"]:last-of-type'); // Attach event listeners - these will work because updateXZoom and updateYZoom are defined later // We'll use closure to capture the slider references if (xSliderMinEl) { xSliderMinEl.addEventListener('input', function() { if (!chart || !filteredLabels || filteredLabels.length === 0) return; let minPercent = parseFloat(this.value); let maxPercent = parseFloat(xSliderMaxEl.value); // Prevent crossing - ensure min <= max if (minPercent > maxPercent) { xSliderMaxEl.value = minPercent; maxPercent = minPercent; } // If both are at extremes (0 and 100), show full chart if (minPercent === 0 && maxPercent === 100) { chart.data.labels = [...filteredLabels]; chart.data.datasets.forEach((ds, i) => { ds.data = [...filteredSeries[i].data]; }); } else { const startPercent = minPercent / 100; const endPercent = maxPercent / 100; let startIndex = Math.floor(filteredLabels.length * startPercent); let endIndex = Math.ceil(filteredLabels.length * endPercent); // If min and max are the same (end to end), show at least one data point if (startIndex >= endIndex) { endIndex = Math.min(filteredLabels.length, startIndex + 1); } // Ensure indices are within bounds startIndex = Math.max(0, Math.min(startIndex, filteredLabels.length - 1)); endIndex = Math.max(startIndex + 1, Math.min(endIndex, filteredLabels.length)); chart.data.labels = filteredLabels.slice(startIndex, endIndex); chart.data.datasets.forEach((ds, i) => { ds.data = filteredSeries[i].data.slice(startIndex, endIndex); }); } chart.update('none'); }); } if (xSliderMaxEl) { xSliderMaxEl.addEventListener('input', function() { if (!chart || !filteredLabels || filteredLabels.length === 0) return; let minPercent = parseFloat(xSliderMinEl.value); let maxPercent = parseFloat(this.value); // Prevent crossing - ensure min <= max if (minPercent > maxPercent) { xSliderMinEl.value = maxPercent; minPercent = maxPercent; } // If both are at extremes (0 and 100), show full chart if (minPercent === 0 && maxPercent === 100) { chart.data.labels = [...filteredLabels]; chart.data.datasets.forEach((ds, i) => { ds.data = [...filteredSeries[i].data]; }); } else { const startPercent = minPercent / 100; const endPercent = maxPercent / 100; let startIndex = Math.floor(filteredLabels.length * startPercent); let endIndex = Math.ceil(filteredLabels.length * endPercent); // If min and max are the same (end to end), show at least one data point if (startIndex >= endIndex) { endIndex = Math.min(filteredLabels.length, startIndex + 1); } // Ensure indices are within bounds startIndex = Math.max(0, Math.min(startIndex, filteredLabels.length - 1)); endIndex = Math.max(startIndex + 1, Math.min(endIndex, filteredLabels.length)); chart.data.labels = filteredLabels.slice(startIndex, endIndex); chart.data.datasets.forEach((ds, i) => { ds.data = filteredSeries[i].data.slice(startIndex, endIndex); }); } chart.update('none'); }); } if (ySliderMinEl) { ySliderMinEl.addEventListener('input', function() { if (!chart || !filteredSeries || filteredSeries.length === 0) return; // Get values from currently displayed data (after X zoom if applied) const allValues = chart.data.datasets.flatMap(ds => ds.data.filter(v => v != null && v !== undefined)); if (allValues.length === 0) return; const min = Math.min(...allValues); const max = Math.max(...allValues); const range = max - min; if (range === 0) return; // Prevent division by zero let minPercent = parseFloat(this.value); let maxPercent = parseFloat(ySliderMaxEl.value); // Prevent crossing - ensure min <= max if (minPercent > maxPercent) { ySliderMaxEl.value = minPercent; maxPercent = minPercent; } // If both are at extremes (0 and 100), show full range if (minPercent === 0 && maxPercent === 100) { chart.options.scales.y.min = undefined; chart.options.scales.y.max = undefined; } else { const startPercent = minPercent / 100; const endPercent = maxPercent / 100; // Calculate new min and max based on percentage (same logic as X zoom) // minPercent: 0 = show from bottom (min), 100 = show from top (max) // maxPercent: 0 = show to bottom (min), 100 = show to top (max) let newMin = min + range * startPercent; let newMax = min + range * endPercent; // If min and max are the same (end to end), show at least a small range if (newMax <= newMin) { const center = (newMin + newMax) / 2; const smallRange = Math.max(range * 0.01, 1); // At least 1% of range or 1 unit newMin = center - smallRange / 2; newMax = center + smallRange / 2; } // Ensure valid range and bounds newMin = Math.max(min, Math.min(newMin, max)); newMax = Math.max(newMin + 0.01, Math.min(newMax, max)); chart.options.scales.y.min = newMin; chart.options.scales.y.max = newMax; } chart.update('none'); }); } if (ySliderMaxEl) { ySliderMaxEl.addEventListener('input', function() { if (!chart || !filteredSeries || filteredSeries.length === 0) return; // Get values from currently displayed data (after X zoom if applied) const allValues = chart.data.datasets.flatMap(ds => ds.data.filter(v => v != null && v !== undefined)); if (allValues.length === 0) return; const min = Math.min(...allValues); const max = Math.max(...allValues); const range = max - min; if (range === 0) return; // Prevent division by zero let minPercent = parseFloat(ySliderMinEl.value); let maxPercent = parseFloat(this.value); // Prevent crossing - ensure min <= max if (minPercent > maxPercent) { ySliderMinEl.value = maxPercent; minPercent = maxPercent; } // If both are at extremes (0 and 100), show full range if (minPercent === 0 && maxPercent === 100) { chart.options.scales.y.min = undefined; chart.options.scales.y.max = undefined; } else { const startPercent = minPercent / 100; const endPercent = maxPercent / 100; // Calculate new min and max based on percentage (same logic as X zoom) // minPercent: 0 = show from bottom (min), 100 = show from top (max) // maxPercent: 0 = show to bottom (min), 100 = show to top (max) let newMin = min + range * startPercent; let newMax = min + range * endPercent; // If min and max are the same (end to end), show at least a small range if (newMax <= newMin) { const center = (newMin + newMax) / 2; const smallRange = Math.max(range * 0.01, 1); // At least 1% of range or 1 unit newMin = center - smallRange / 2; newMax = center + smallRange / 2; } // Ensure valid range and bounds newMin = Math.max(min, Math.min(newMin, max)); newMax = Math.max(newMin + 0.01, Math.min(newMax, max)); chart.options.scales.y.min = newMin; chart.options.scales.y.max = newMax; } chart.update('none'); }); } } // Canvas const canvas = document.createElement('canvas'); //canvas.width = chartWidth; canvas.height = chartHeight; canvas.style.width = `${chartWidth}`; canvas.style.height = `${chartHeight}px`; chartWrapper.appendChild(canvas); container.appendChild(chartWrapper); // Legend buttons container const legendButtonsContainer = document.createElement('div'); legendButtonsContainer.style.display = 'flex'; legendButtonsContainer.style.gap = '8px'; legendButtonsContainer.style.marginBottom = '8px'; legendButtonsContainer.style.marginTop = '12px'; legendButtonsContainer.style.minWidth = '160px'; // Clear All button const clearAllBtn = document.createElement('button'); clearAllBtn.textContent = 'Clear All'; clearAllBtn.style.padding = '6px 12px'; clearAllBtn.style.border = '1px solid #ccc'; clearAllBtn.style.borderRadius = '4px'; clearAllBtn.style.fontSize = '14px'; clearAllBtn.style.color = '#152935'; clearAllBtn.style.backgroundColor = '#fff'; clearAllBtn.style.cursor = 'pointer'; clearAllBtn.style.flex = '1'; clearAllBtn.addEventListener('mouseenter', () => { clearAllBtn.style.backgroundColor = '#f5f5f5'; }); clearAllBtn.addEventListener('mouseleave', () => { clearAllBtn.style.backgroundColor = '#fff'; }); // Select All button const selectAllBtn = document.createElement('button'); selectAllBtn.textContent = 'Select All'; selectAllBtn.style.padding = '6px 12px'; selectAllBtn.style.border = '1px solid #ccc'; selectAllBtn.style.borderRadius = '4px'; selectAllBtn.style.fontSize = '14px'; selectAllBtn.style.color = '#152935'; selectAllBtn.style.backgroundColor = '#fff'; selectAllBtn.style.cursor = 'pointer'; selectAllBtn.style.flex = '1'; selectAllBtn.addEventListener('mouseenter', () => { selectAllBtn.style.backgroundColor = '#f5f5f5'; }); selectAllBtn.addEventListener('mouseleave', () => { selectAllBtn.style.backgroundColor = '#fff'; }); legendButtonsContainer.appendChild(clearAllBtn); legendButtonsContainer.appendChild(selectAllBtn); // Legend (checkboxes) const legend = document.createElement('div'); legend.style.display = 'flex'; legend.style.flexDirection = 'column'; legend.style.gap = '12px'; legend.style.marginTop = '0'; legend.style.minWidth = '160px'; container.appendChild(legendButtonsContainer); container.appendChild(legend); // Extract unique years and quarters from labels const yearQuarterMap = {}; const years = []; const quarters = ['Q1', 'Q2', 'Q3', 'Q4']; labels.forEach(label => { const match = label.match(/(\d{4})\s*(Q[1-4])?/); if (match) { const year = match[1]; if (!yearQuarterMap[year]) { yearQuarterMap[year] = new Set(); years.push(year); } if (match[2]) { yearQuarterMap[year].add(match[2]); } } }); years.sort(); // Custom dropdown function for Year and Quarter filters function createCustomDropdown(labelText, items, selectedValues) { const dropdownWrapper = document.createElement('div'); dropdownWrapper.style.position = 'relative'; dropdownWrapper.style.display = 'flex'; dropdownWrapper.style.alignItems = 'center'; dropdownWrapper.style.marginRight = '16px'; // Label const label = document.createElement('label'); label.textContent = labelText + ':'; label.style.color = '#152935'; label.style.fontSize = '14px'; label.style.marginRight = '6px'; label.style.flexShrink = '0'; // Toggle button const toggleBtn = document.createElement('button'); toggleBtn.style.padding = '6px 12px'; toggleBtn.style.border = '1px solid #ccc'; toggleBtn.style.borderRadius = '4px'; toggleBtn.style.fontSize = '14px'; toggleBtn.style.color = '#152935'; toggleBtn.style.backgroundColor = '#fff'; toggleBtn.style.cursor = 'pointer'; toggleBtn.style.minWidth = '140px'; toggleBtn.style.textAlign = 'left'; toggleBtn.style.flexShrink = '0'; const updateButtonText = () => { const selected = selectedValues.filter(v => v !== 'selectall'); const arrow = isOpen ? '▲' : '▼'; if (selected.length === 0) { toggleBtn.textContent = `None selected ${arrow}`; } else if (selected.length === items.length) { toggleBtn.textContent = `All selected ${arrow}`; } else { toggleBtn.textContent = `${selected.length} selected ${arrow}`; } }; let isOpen = false; updateButtonText(); // Dropdown menu (hidden by default) const dropdownMenu = document.createElement('div'); dropdownMenu.style.display = 'none'; dropdownMenu.style.position = 'absolute'; dropdownMenu.style.top = '100%'; dropdownMenu.style.left = '0'; dropdownMenu.style.marginTop = '4px'; dropdownMenu.style.padding = '8px'; dropdownMenu.style.border = '1px solid #ccc'; dropdownMenu.style.borderRadius = '4px'; dropdownMenu.style.backgroundColor = '#fff'; dropdownMenu.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; dropdownMenu.style.zIndex = '1000'; dropdownMenu.style.maxHeight = '200px'; dropdownMenu.style.overflowY = 'auto'; dropdownMenu.style.overflowX = 'hidden'; dropdownMenu.style.minWidth = '140px'; dropdownMenu.style.overscrollBehavior = 'contain'; // Enable wheel scrolling in dropdown menu dropdownMenu.addEventListener('wheel', (e) => { e.stopPropagation(); // Prevent event from bubbling up // Allow default scroll behavior }, { passive: true }); // Also prevent click events from closing dropdown when scrolling dropdownMenu.addEventListener('mousedown', (e) => { e.stopPropagation(); }); // Select All checkbox const selectAllRow = document.createElement('label'); selectAllRow.style.display = 'flex'; selectAllRow.style.alignItems = 'center'; selectAllRow.style.padding = '4px 0'; selectAllRow.style.cursor = 'pointer'; selectAllRow.style.userSelect = 'none'; const selectAllCheckbox = document.createElement('input'); selectAllCheckbox.type = 'checkbox'; selectAllCheckbox.checked = selectedValues.length === items.length; selectAllCheckbox.style.marginRight = '8px'; selectAllCheckbox.style.cursor = 'pointer'; const selectAllLabel = document.createElement('span'); selectAllLabel.textContent = 'Select All'; selectAllLabel.style.fontSize = '14px'; selectAllLabel.style.color = '#152935'; selectAllRow.appendChild(selectAllCheckbox); selectAllRow.appendChild(selectAllLabel); selectAllRow.addEventListener('click', (e) => { e.stopPropagation(); if (e.target !== selectAllCheckbox) { selectAllCheckbox.checked = !selectAllCheckbox.checked; } const allSelected = selectAllCheckbox.checked; if (allSelected) { selectedValues.length = 0; selectedValues.push(...items); items.forEach(item => { const option = optionMap.get(item); if (option) option.querySelector('input').checked = true; }); } else { selectedValues.length = 0; items.forEach(item => { const option = optionMap.get(item); if (option) option.querySelector('input').checked = false; }); } updateButtonText(); applyFilters(); }); dropdownMenu.appendChild(selectAllRow); // Individual checkboxes const optionMap = new Map(); items.forEach(item => { const row = document.createElement('label'); row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.padding = '4px 0'; row.style.cursor = 'pointer'; row.style.userSelect = 'none'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = item; checkbox.checked = selectedValues.includes(item); checkbox.style.marginRight = '8px'; checkbox.style.cursor = 'pointer'; checkbox.addEventListener('change', () => { if (checkbox.checked && !selectedValues.includes(item)) { selectedValues.push(item); } else if (!checkbox.checked) { const index = selectedValues.indexOf(item); if (index > -1) selectedValues.splice(index, 1); } selectAllCheckbox.checked = selectedValues.length === items.length; updateButtonText(); applyFilters(); }); const labelSpan = document.createElement('span'); labelSpan.textContent = item; labelSpan.style.fontSize = '14px'; labelSpan.style.color = '#152935'; row.appendChild(checkbox); row.appendChild(labelSpan); row.addEventListener('click', (e) => { if (e.target !== checkbox) { checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change')); } }); dropdownMenu.appendChild(row); optionMap.set(item, row); }); // Toggle dropdown toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); isOpen = dropdownMenu.style.display !== 'none'; isOpen = !isOpen; dropdownMenu.style.display = isOpen ? 'block' : 'none'; updateButtonText(); }); // Close when clicking outside document.addEventListener('click', (e) => { if (!dropdownWrapper.contains(e.target)) { isOpen = false; dropdownMenu.style.display = 'none'; updateButtonText(); } }); dropdownWrapper.appendChild(label); const btnWrapper = document.createElement('div'); btnWrapper.style.position = 'relative'; btnWrapper.appendChild(toggleBtn); btnWrapper.appendChild(dropdownMenu); dropdownWrapper.appendChild(btnWrapper); return dropdownWrapper; } // Chart.js instance const chart = new Chart(canvas.getContext('2d'), { type: 'line', data: { labels: filteredLabels, datasets: filteredSeries.map(s => ({ label: s.name, data: s.data, borderColor: s.name === "Total" ? lineColor : palette[s.name], backgroundColor: s.name === "Total" ? lineColor : palette[s.name], borderWidth: s.name === "Total" ? 4 : 2, pointRadius: s.name === "Total" ? 5 : 3, pointBackgroundColor: "#fff", pointBorderWidth: 2, pointBorderColor: s.name === "Total" ? lineColor : palette[s.name], pointHoverRadius: s.name === "Total" ? 8 : 6, fill: false, tension: 0.6, // Increased from 0.3 for smoother curves borderDash: s.name === "Total" && title === "Media Signal Volume" ? lineStyle : [], zIndex: s.name === "Total" ? 3 : 2, hidden: false, cubicInterpolationMode: 'monotone' // Add monotone interpolation for better curves })) }, options: { plugins: { legend: { display: false } }, scales: { x: { ticks: { color: "#152935", maxRotation: 90, minRotation: 90 }, grid: { display: false } }, y: { title: { display: true, text: 'Signal Activity', color: "#152935" }, ticks: { color: "#152935" }, grid: { display: false } } }, elements: { line: { tension: 0.6 // Global tension setting } }, responsive: false, maintainAspectRatio: false } }); chartInstances.set(wrapper, chart); // Create zoom controls after chart is initialized // Wait for chart to render first, then position everything setTimeout(() => { createZoomControls(); // Position filter container below x zoom axis const chartArea = chart.chartArea; if (chartArea && filterContainer && xZoomContainer) { // X zoom axis is positioned at chartArea.bottom + xAxisLabelHeight (70px) // So filter container should be below that const xZoomBottom = chartArea.bottom + 70 + 20; // chartArea.bottom + xAxisLabelHeight + xZoom height filterContainer.style.top = `${xZoomBottom + 8}px`; // Add some spacing filterContainer.style.left = `${chartArea.left}px`; filterContainer.style.width = `${chartArea.right - chartArea.left}px`; } }, 100); // Store zoom state let xZoomMin = 0; let xZoomMax = 100; let yZoomMin = 0; let yZoomMax = 100; let xZoomSliderMin, xZoomSliderMax, yZoomSliderMin, yZoomSliderMax; // X-axis zoom handler - Range slider function updateXZoom() { if (!xZoomSliderMin || !xZoomSliderMax || !chart) return; const totalLabels = filteredLabels.length; if (totalLabels === 0) return; const minPercent = parseFloat(xZoomSliderMin.value); const maxPercent = parseFloat(xZoomSliderMax.value); // Ensure min <= max if (minPercent > maxPercent) { if (xZoomSliderMin === document.activeElement) { xZoomSliderMax.value = minPercent; } else { xZoomSliderMin.value = maxPercent; } return updateXZoom(); } xZoomMin = minPercent; xZoomMax = maxPercent; const startPercent = minPercent / 100; const endPercent = maxPercent / 100; const startIndex = Math.floor(totalLabels * startPercent); const endIndex = Math.ceil(totalLabels * endPercent); chart.data.labels = filteredLabels.slice(startIndex, endIndex); chart.data.datasets.forEach((ds, i) => { ds.data = filteredSeries[i].data.slice(startIndex, endIndex); }); chart.update('none'); // Use 'none' mode to prevent animations that might cause issues } // Y-axis zoom handler - Range slider function updateYZoom() { if (!yZoomSliderMin || !yZoomSliderMax || !chart) return; const allValues = filteredSeries.flatMap(s => s.data.filter(v => v != null)); if (allValues.length === 0) return; const min = Math.min(...allValues); const max = Math.max(...allValues); const range = max - min; const minPercent = parseFloat(yZoomSliderMin.value); const maxPercent = parseFloat(yZoomSliderMax.value); // Ensure min <= max if (minPercent > maxPercent) { if (yZoomSliderMin === document.activeElement) { yZoomSliderMax.value = minPercent; } else { yZoomSliderMin.value = maxPercent; } return updateYZoom(); } yZoomMin = minPercent; yZoomMax = maxPercent; // minPercent: 0-100 where 0 shows full range, 100 shows minimal range from bottom // maxPercent: 0-100 where 0 shows full range, 100 shows minimal range from top // When both are 0, show full range (min to max) // When both are 100, show minimal range in the middle const zoomMinFactor = minPercent / 100; // How much to crop from bottom (0-1) const zoomMaxFactor = maxPercent / 100; // How much to crop from top (0-1) const newMin = min + range * zoomMinFactor; const newMax = max - range * zoomMaxFactor; chart.options.scales.y.min = newMin; chart.options.scales.y.max = newMax; chart.update('none'); // Use 'none' mode to prevent animations } // Year and Quarter dropdowns - positioned below X axis const filterContainer = document.createElement('div'); filterContainer.style.display = 'flex'; filterContainer.style.justifyContent = 'center'; filterContainer.style.gap = '8px'; filterContainer.style.alignItems = 'center'; filterContainer.style.position = 'absolute'; filterContainer.style.paddingTop = '8px'; const selectedYears = [...years]; const selectedQuarters = [...quarters]; const yearDropdown = createCustomDropdown('Year', years, selectedYears); const quarterDropdown = createCustomDropdown('Quarter', quarters, selectedQuarters); filterContainer.appendChild(yearDropdown); filterContainer.appendChild(quarterDropdown); chartWrapper.appendChild(filterContainer); // Filter function - Multiselect support function applyFilters() { filteredLabels = []; const filteredIndices = []; originalLabels.forEach((label, index) => { let match = true; if (selectedYears.length > 0) { const yearMatch = label.match(/(\d{4})/); if (!yearMatch || !selectedYears.includes(yearMatch[1])) { match = false; } } if (selectedQuarters.length > 0 && match) { const quarterMatch = label.match(/Q([1-4])/); if (!quarterMatch || !selectedQuarters.includes(quarterMatch[0])) { match = false; } } if (match) { filteredLabels.push(label); filteredIndices.push(index); } }); filteredSeries = originalSeries.map(s => ({ ...s, data: filteredIndices.map(i => s.data[i]) })); chart.data.labels = filteredLabels; chart.data.datasets.forEach((ds, i) => { ds.data = filteredSeries[i].data; }); // Reset zoom sliders if they exist if (xZoomSliderMin && xZoomSliderMax) { xZoomSliderMin.value = '0'; xZoomSliderMax.value = '100'; } if (yZoomSliderMin && yZoomSliderMax) { yZoomSliderMin.value = '0'; yZoomSliderMax.value = '100'; } xZoomMin = 0; xZoomMax = 100; yZoomMin = 0; yZoomMax = 100; chart.options.scales.y.min = undefined; chart.options.scales.y.max = undefined; chart.update('none'); // Update chart first // Then update zoom if sliders exist if (xZoomSliderMin && xZoomSliderMax) { updateXZoom(); } if (yZoomSliderMin && yZoomSliderMax) { updateYZoom(); } } // Create checkboxes for each dataset chart.data.datasets.forEach((ds, i) => { const row = document.createElement('label'); row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.cursor = 'pointer'; row.style.gap = '8px'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = !ds.hidden; checkbox.style.width = '16px'; checkbox.style.height = '16px'; checkbox.style.display = 'none'; // Hide the actual checkbox input checkbox.dataset.index = i; // Add dataset index for reference // Custom checkbox box const box = document.createElement('span'); box.style.display = 'inline-flex'; // Use flex for centering box.style.alignItems = 'center'; box.style.justifyContent = 'center'; box.style.width = '16px'; box.style.height = '16px'; box.style.border = `2px solid ${ds.borderColor}`; box.style.borderRadius = '4px'; box.style.backgroundColor = 'white'; box.style.position = 'relative'; // Checkmark inside the box const check = document.createElement('span'); check.textContent = '✔'; check.style.fontSize = '13px'; check.style.color = ds.borderColor; check.style.display = checkbox.checked ? 'block' : 'none'; check.style.lineHeight = '1'; check.style.position = 'static'; // Let flexbox center it box.appendChild(check); // Store check element reference on checkbox for easy access checkbox.checkElement = check; // Line indicator const line = document.createElement('span'); line.style.display = 'inline-block'; line.style.width = '16px'; line.style.height = '2px'; line.style.borderTop = ds.borderDash && ds.borderDash.length ? `2px dashed ${ds.borderColor}` : `2px solid ${ds.borderColor}`; // Checkbox event: show/hide dataset checkbox.addEventListener('change', (e) => { chart.setDatasetVisibility(i, checkbox.checked); chart.update(); check.style.display = checkbox.checked ? 'block' : 'none'; }); // Direct click handler for the entire label element row.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (e.target !== checkbox) { checkbox.checked = !checkbox.checked; chart.setDatasetVisibility(i, checkbox.checked); chart.update(); check.style.display = checkbox.checked ? 'block' : 'none'; } return false; }); const txt = document.createElement('span'); txt.textContent = ds.label; txt.style.color = ds.borderColor; txt.style.fontWeight = ds.label === "Total" ? "bold" : "normal"; row.appendChild(checkbox); // Hidden checkbox row.appendChild(box); // Custom checkbox styling row.appendChild(line); // Line style indicator row.appendChild(txt); // Label text legend.appendChild(row); }); // Add event listeners to Clear All and Select All buttons clearAllBtn.addEventListener('click', () => { chart.data.datasets.forEach((ds, i) => { chart.setDatasetVisibility(i, false); const checkbox = legend.querySelector(`input[data-index="${i}"]`); if (checkbox) { checkbox.checked = false; if (checkbox.checkElement) { checkbox.checkElement.style.display = 'none'; } } }); chart.update(); }); selectAllBtn.addEventListener('click', () => { chart.data.datasets.forEach((ds, i) => { chart.setDatasetVisibility(i, true); const checkbox = legend.querySelector(`input[data-index="${i}"]`); if (checkbox) { checkbox.checked = true; if (checkbox.checkElement) { checkbox.checkElement.style.display = 'block'; } } }); chart.update(); }); } // Render donut legend row function renderDonutLegendRow(chartRow) { const donut = chartRow.querySelector('.donutWrapper'); if (!donut) return; const legendRow = chartRow.querySelector('.donutLegendRow'); if (!legendRow) return; clearElement(legendRow); const labels = JSON.parse(donut.getAttribute('data-labels')); const values = JSON.parse(donut.getAttribute('data-values')); const total = values.reduce((a,b) => a+b, 0); const percentages = values.map(v => total ? (v/total*100) : 0); // Global palette kullan const palette = globalPalette; legendRow.style.display = 'flex'; legendRow.style.flexDirection = 'row'; // Changed back to row since we're not adding explanation here legendRow.style.flexWrap = 'wrap'; legendRow.style.gap = '18px'; legendRow.style.margin = '18px 0 0 0'; legendRow.style.fontSize = '15px'; legendRow.style.maxWidth = 'calc(100% - 40px)'; legendRow.style.color = '#152935'; labels.forEach((label, i) => { const colorBox = document.createElement('span'); colorBox.style.background = palette[label]; colorBox.style.display = 'inline-block'; colorBox.style.width = '16px'; colorBox.style.height = '16px'; colorBox.style.borderRadius = '3px'; const txt = document.createElement('span'); txt.textContent = `${label} ${percentages[i].toFixed(1)}% (${values[i]})`; const item = document.createElement('div'); item.style.display = 'flex'; item.style.alignItems = 'center'; item.style.gap = '7px'; item.appendChild(colorBox); item.appendChild(txt); legendRow.appendChild(item); }); // Removed explanation text from here, as it's now in the separate container at the bottom } // Render all charts function renderAllCharts() { collectAllLabels(); globalPalette = generatePalette(globalLabels); document.querySelectorAll('.donutWrapper').forEach(renderDonutChart); document.querySelectorAll('.lineWrapper').forEach(renderLineChart); document.querySelectorAll('.chartRow').forEach(renderDonutLegendRow); } window.addEventListener('DOMContentLoaded', () => { renderAllCharts(); // Resize observer for each chart wrapper const resizeObserver = new ResizeObserver(() => { renderAllCharts(); }); document.querySelectorAll('.donutWrapper, .lineWrapper').forEach(wrapper => { resizeObserver.observe(wrapper); }); });

Geothermal Energy: Meta's 2025 AI Power Play

Meta's Geothermal Bet: Powering AI Data Centers with 24/7 Clean Energy in 2025…


Google's 2025 LDES Pivot: Powering Its AI Future Now

Google's 2025 Pivot: How Long-Duration Storage Is Powering Its AI Future…


Privacy Preference Center