Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced Graph Creator</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/luxon@3.0.2/build/global/luxon.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.2.0/dist/chartjs-adapter-luxon.min.js"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| .chart-container { | |
| position: relative; | |
| height: 60vh; | |
| width: 100%; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| .data-point { | |
| display: flex; | |
| margin-bottom: 8px; | |
| align-items: center; | |
| } | |
| .data-point input { | |
| margin-right: 8px; | |
| padding: 6px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| } | |
| .remove-point { | |
| color: #ef4444; | |
| cursor: pointer; | |
| margin-left: 8px; | |
| font-size: 18px; | |
| } | |
| .custom-scrollbar { | |
| scrollbar-width: thin; | |
| scrollbar-color: #6b7280 #f3f4f6; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-track { | |
| background: #f3f4f6; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { | |
| background-color: #6b7280; | |
| border-radius: 20px; | |
| } | |
| .slide-in { | |
| animation: slideIn 0.3s ease-out forwards; | |
| } | |
| .slide-out { | |
| animation: slideOut 0.3s ease-out forwards; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| transform: translateX(100%); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| } | |
| @keyframes slideOut { | |
| from { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| to { | |
| transform: translateX(100%); | |
| opacity: 0; | |
| } | |
| } | |
| .tooltip { | |
| position: absolute; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| pointer-events: none; | |
| z-index: 1000; | |
| white-space: nowrap; | |
| } | |
| .dataset-selector { | |
| margin-top: 8px; | |
| padding: 8px; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 6px; | |
| background-color: #f9fafb; | |
| } | |
| .dataset-option { | |
| display: flex; | |
| align-items: center; | |
| padding: 4px 8px; | |
| margin: 4px 0; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| transition: background-color 0.2s; | |
| } | |
| .dataset-option:hover { | |
| background-color: #f3f4f6; | |
| } | |
| .dataset-option.selected { | |
| background-color: #e0f2fe; | |
| border: 1px solid #0ea5e9; | |
| } | |
| .multi-input-container { | |
| border: 1px solid #e5e7eb; | |
| border-radius: 6px; | |
| padding: 8px; | |
| margin-top: 8px; | |
| background-color: #f9fafb; | |
| } | |
| .multi-data-point { | |
| display: flex; | |
| margin-bottom: 6px; | |
| align-items: center; | |
| } | |
| .multi-data-point input { | |
| margin-right: 6px; | |
| padding: 4px 6px; | |
| border: 1px solid #d1d5db; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| } | |
| .remove-multi-point { | |
| color: #ef4444; | |
| cursor: pointer; | |
| margin-left: 6px; | |
| font-size: 16px; | |
| } | |
| /* Custom select styling */ | |
| .select-wrapper { | |
| position: relative; | |
| } | |
| .select-wrapper::after { | |
| content: '▼'; | |
| font-size: 10px; | |
| color: #6b7280; | |
| position: absolute; | |
| right: 10px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| pointer-events: none; | |
| } | |
| .color-preview { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| margin-right: 6px; | |
| vertical-align: middle; | |
| } | |
| /* Animation for chart update */ | |
| @keyframes chartUpdate { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.02); } | |
| 100% { transform: scale(1); } | |
| } | |
| .chart-update { | |
| animation: chartUpdate 0.3s ease-out; | |
| } | |
| /* CSV separator styles */ | |
| .separator-option { | |
| border: 1px solid #e5e7eb; | |
| padding: 8px; | |
| margin: 4px 0; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .separator-option:hover { | |
| background-color: #f9fafb; | |
| } | |
| .separator-option.selected { | |
| border-color: #3b82f6; | |
| background-color: #eff6ff; | |
| } | |
| /* Info tooltip */ | |
| .info-icon { | |
| display: inline-block; | |
| width: 14px; | |
| height: 14px; | |
| border-radius: 50%; | |
| background-color: #6b7280; | |
| color: white; | |
| text-align: center; | |
| line-height: 14px; | |
| font-size: 10px; | |
| margin-left: 4px; | |
| cursor: help; | |
| } | |
| .help-text { | |
| @apply bg-gray-700 text-white px-2 py-1 rounded text-xs absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 whitespace-nowrap opacity-0 transition-opacity duration-300; | |
| pointer-events: none; | |
| z-index: 1000; | |
| width: 200px; | |
| text-align: center; | |
| } | |
| .help-text::after { | |
| content: ''; | |
| position: absolute; | |
| top: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| border: 6px solid transparent; | |
| border-top-color: #374151; | |
| } | |
| .info-icon:hover + .help-text { | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen font-sans"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="text-center mb-8"> | |
| <h1 class="text-4xl font-bold text-indigo-800 mb-2">📊 Graph Creator</h1> | |
| <p class="text-lg text-gray-600">Create beautiful charts from your data with customizable options</p> | |
| </header> | |
| <div class="flex flex-col lg:flex-row gap-6"> | |
| <!-- Left Panel: Controls --> | |
| <div class="lg:w-1/3"> | |
| <div class="bg-white rounded-xl shadow-lg p-6 mb-6 transition-all duration-300 hover:shadow-xl"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> | |
| <i class="fas fa-cogs mr-2 text-indigo-600"></i> Configuration | |
| </h2> | |
| <!-- Data Input Method --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Data Input Method</label> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button id="manual-input-btn" class="py-2 px-4 bg-indigo-100 text-indigo-800 rounded-lg font-medium transition-colors duration-200 flex items-center justify-center hover:bg-indigo-200 active:bg-indigo-300"> | |
| <i class="fas fa-keyboard mr-2"></i> Manual | |
| </button> | |
| <button id="file-input-btn" class="py-2 px-4 bg-gray-100 text-gray-700 rounded-lg font-medium transition-colors duration-200 flex items-center justify-center hover:bg-gray-200 active:bg-gray-300"> | |
| <i class="fas fa-file-upload mr-2"></i> Upload | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Manual Input Section --> | |
| <div id="manual-input" class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Chart Title</label> | |
| <input type="text" id="chart-title" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter chart title"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">X-axis Label</label> | |
| <input type="text" id="x-label" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter X-axis label"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Y-axis Label</label> | |
| <input type="text" id="y-label" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter Y-axis label"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Data Points</label> | |
| <div class="flex items-center mb-2"> | |
| <span class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">Multiple Columns Mode</span> | |
| </div> | |
| <div id="multi-data-container" class="multi-input-container"> | |
| <div id="multi-data-points-container" class="space-y-2 max-h-40 overflow-y-auto custom-scrollbar pr-2"> | |
| <div class="multi-data-point"> | |
| <input type="text" class="category-input w-1/4" placeholder="Category"> | |
| <input type="number" class="dataset1-input w-1/4" placeholder="Dataset 1"> | |
| <input type="number" class="dataset2-input w-1/4" placeholder="Dataset 2"> | |
| <i class="fas fa-trash remove-multi-point"></i> | |
| </div> | |
| </div> | |
| <button id="add-multi-point" class="mt-2 flex items-center text-sm text-indigo-600 hover:text-indigo-800"> | |
| <i class="fas fa-plus mr-1"></i> Add Data Point | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Dataset Configuration --> | |
| <div id="dataset-config" class="mt-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Dataset Labels</label> | |
| <div id="dataset-labels-container" class="space-y-2"> | |
| <div class="flex"> | |
| <input type="text" id="dataset1-label" value="Dataset 1" class="w-3/4 px-3 py-1 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <div class="w-1/4 bg-blue-100 text-blue-800 px-2 py-1 rounded-r-lg text-center">Color</div> | |
| </div> | |
| <div class="flex"> | |
| <input type="text" id="dataset2-label" value="Dataset 2" class="w-3/4 px-3 py-1 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <div class="w-1/4 bg-green-100 text-green-800 px-2 py-1 rounded-r-lg text-center">Color</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- File Upload Section (Hidden by default) --> | |
| <div id="file-input" class="hidden space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Upload CSV File</label> | |
| <input type="file" id="csv-file" accept=".csv" class="w-full px-3 py-2 border border-gray-300 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"> | |
| <div class="flex items-center mt-1"> | |
| <span class="text-xs text-gray-500">Upload a CSV file (first row should contain headers)</span> | |
| <span class="info-icon relative ml-1">i | |
| <span class="help-text"> | |
| Supported file types: CSV (.csv). The first row should contain column headers. | |
| </span> | |
| </span> | |
| </div> | |
| </div> | |
| <div class="space-y-3"> | |
| <label class="block text-sm font-medium text-gray-700">CSV Separator</label> | |
| <div class="flex flex-wrap gap-2"> | |
| <div class="separator-option selected" data-separator=","> | |
| <div class="font-medium">Comma (,)</div> | |
| <div class="text-xs text-gray-600">Most common format</div> | |
| </div> | |
| <div class="separator-option" data-separator=";"> | |
| <div class="font-medium">Semicolon (;)</div> | |
| <div class="text-xs text-gray-600">Common in European systems</div> | |
| </div> | |
| <div class="separator-option" data-separator="\t"> | |
| <div class="font-medium">Tab</div> | |
| <div class="text-xs text-gray-600">Tab character</div> | |
| </div> | |
| <div class="separator-option" data-separator="|"> | |
| <div class="font-medium">Pipe (|)</div> | |
| <div class="text-xs text-gray-600">Vertical bar</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="csv-preview" class="hidden"> | |
| <h3 class="text-sm font-medium text-gray-700 mb-2">Data Preview</h3> | |
| <div id="csv-content" class="bg-gray-50 p-3 rounded-lg text-xs max-h-32 overflow-y-auto custom-scrollbar"></div> | |
| <div class="mt-3"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Select Data to Plot</label> | |
| <div class="select-wrapper"> | |
| <select id="x-axis-select" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <option value="">Select X-axis column</option> | |
| </select> | |
| </div> | |
| <div class="mt-2 dataset-selector"> | |
| <h4 class="text-sm font-medium text-gray-700 mb-1">Select Y-axis columns:</h4> | |
| <div id="y-axis-options" class="space-y-1 max-h-32 overflow-y-auto custom-scrollbar"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chart Settings --> | |
| <div class="bg-white rounded-xl shadow-lg p-6 transition-all duration-300 hover:shadow-xl"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> | |
| <i class="fas fa-sliders-h mr-2 text-indigo-600"></i> Chart Settings | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Chart Type</label> | |
| <select id="chart-type" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <option value="bar">Bar Chart</option> | |
| <option value="line">Line Chart</option> | |
| <option value="radar">Radar Chart</option> | |
| <option value="polarArea">Polar Area</option> | |
| <option value="bubble">Bubble Chart</option> | |
| <option value="scatter">Scatter Chart</option> | |
| </select> | |
| </div> | |
| <div id="line-options" class="space-y-3 hidden"> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="show-lines" class="mr-2 h-4 w-4 text-indigo-600 rounded focus:ring-indigo-500" checked> | |
| <label for="show-lines" class="text-sm font-medium text-gray-700">Show Lines</label> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="show-points" class="mr-2 h-4 w-4 text-indigo-600 rounded focus:ring-indigo-500" checked> | |
| <label for="show-points" class="text-sm font-medium text-gray-700">Show Points</label> | |
| </div> | |
| </div> | |
| <div id="scatter-options" class="space-y-3 hidden"> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="show-trendline" class="mr-2 h-4 w-4 text-indigo-600 rounded focus:ring-indigo-500"> | |
| <label for="show-trendline" class="text-sm font-medium text-gray-700">Show Trendline</label> | |
| </div> | |
| </div> | |
| <div id="color-scheme"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Color Scheme</label> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button class="color-option p-2 rounded-lg flex items-center justify-center" data-colors='["#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#06B6D4", "#EC4899", "#F97316"]'> | |
| <div class="w-6 h-6 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-full"></div> | |
| </button> | |
| <button class="color-option p-2 rounded-lg flex items-center justify-center" data-colors='["#EC4899", "#DB2777", "#BE185D", "#9D174D", "#7E113B", "#C2410C", "#9A3412", "#7C2D12"]'> | |
| <div class="w-6 h-6 bg-pink-500 rounded-full"></div> | |
| </button> | |
| <button class="color-option p-2 rounded-lg flex items-center justify-center" data-colors='["#10B981", "#059669", "#047857", "#065F46", "#064E3B", "#16A34A", "#22C55E", "#4ADE80"]'> | |
| <div class="w-6 h-6 bg-green-500 rounded-full"></div> | |
| </button> | |
| <button class="color-option p-2 rounded-lg flex items-center justify-center" data-colors='["#F59E0B", "#D97706", "#B45309", "#92400E", "#78350F", "#F97316", "#EA580C", "#C2410C"]'> | |
| <div class="w-6 h-6 bg-amber-500 rounded-full"></div> | |
| </button> | |
| <button class="color-option p-2 rounded-lg flex items-center justify-center" data-colors='["#8B5CF6", "#7C3AED", "#6D28D9", "#5B21B6", "#4C1D95", "#A855F7", "#C026D3", "#BE185D"]'> | |
| <div class="w-6 h-6 bg-purple-500 rounded-full"></div> | |
| </button> | |
| <button class="color-option p-2 rounded-lg flex items-center justify-center" data-colors='["#06B6D4", "#0891B2", "#0E7490", "#155E75", "#164E63", "#38BDF8", "#0EA5E9", "#0284C7"]'> | |
| <div class="w-6 h-6 bg-cyan-500 rounded-full"></div> | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Animation Duration (ms)</label> | |
| <input type="range" id="animation-duration" min="0" max="3000" value="1000" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>0ms</span> | |
| <span id="duration-value">1000ms</span> | |
| <span>3000ms</span> | |
| </div> | |
| </div> | |
| <button id="generate-btn" class="w-full py-3 px-4 bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500 focus:ring-offset-indigo-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg"> | |
| <i class="fas fa-chart-line mr-2"></i> Generate Chart | |
| </button> | |
| <button id="download-btn" class="w-full py-2 px-4 bg-gray-600 hover:bg-gray-700 text-white transition ease-in duration-200 text-center text-sm font-semibold shadow-md focus:outline-none rounded-lg flex items-center justify-center"> | |
| <i class="fas fa-download mr-2"></i> Download Chart | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Panel: Chart --> | |
| <div class="lg:w-2/3"> | |
| <div class="bg-white rounded-xl shadow-lg p-6 h-full transition-all duration-300 hover:shadow-xl"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-800 flex items-center"> | |
| <i class="fas fa-chart-bar mr-2 text-indigo-600"></i> <span id="chart-title-display">Your Chart</span> | |
| </h2> | |
| <div class="flex space-x-2"> | |
| <button id="export-data-btn" class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-200" title="Export Data"> | |
| <i class="fas fa-file-export"></i> | |
| </button> | |
| <button id="fullscreen-btn" class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-200"> | |
| <i class="fas fa-expand"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="chart-container relative"> | |
| <canvas id="graph" class="w-full h-full"></canvas> | |
| <div id="empty-state" class="absolute inset-0 flex flex-col items-center justify-center text-gray-400"> | |
| <i class="fas fa-chart-line text-6xl mb-4 opacity-50"></i> | |
| <p class="text-lg font-medium">No data to display</p> | |
| <p class="text-sm">Configure your data and settings, then click "Generate Chart"</p> | |
| </div> | |
| </div> | |
| <div id="chart-info" class="mt-4 text-sm text-gray-500 hidden"> | |
| <div class="flex justify-between"> | |
| <span>Chart Statistics:</span> | |
| <span id="data-count">0 points</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast Notification --> | |
| <div id="toast" class="fixed bottom-5 right-5 bg-gray-800 text-white px-4 py-3 rounded-lg shadow-lg flex items-center hidden transition-all duration-300 z-50"> | |
| <i class="fas fa-check-circle mr-2"></i> | |
| <span id="toast-message">Chart downloaded successfully!</span> | |
| </div> | |
| <!-- Fullscreen Modal --> | |
| <div id="fullscreen-modal" class="fixed inset-0 bg-black bg-opacity-90 z-50 hidden flex items-center justify-center p-4"> | |
| <div class="relative w-full max-w-5xl"> | |
| <button id="close-fullscreen" class="absolute -top-12 right-0 text-white text-2xl hover:text-gray-300 transition-colors duration-200"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| <h3 id="fullscreen-title" class="text-white text-xl mb-4 text-center"></h3> | |
| <div class="bg-white rounded-lg p-2"> | |
| <canvas id="fullscreen-chart" class="w-full h-auto"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Elements | |
| const manualInputBtn = document.getElementById('manual-input-btn'); | |
| const fileInputBtn = document.getElementById('file-input-btn'); | |
| const manualInput = document.getElementById('manual-input'); | |
| const fileInput = document.getElementById('file-input'); | |
| const csvFile = document.getElementById('csv-file'); | |
| const csvPreview = document.getElementById('csv-preview'); | |
| const csvContent = document.getElementById('csv-content'); | |
| const addMultiPointBtn = document.getElementById('add-multi-point'); | |
| const multiDataPointsContainer = document.getElementById('multi-data-points-container'); | |
| const xLabel = document.getElementById('x-label'); | |
| const yLabel = document.getElementById('y-label'); | |
| const chartType = document.getElementById('chart-type'); | |
| const lineOptions = document.getElementById('line-options'); | |
| const scatterOptions = document.getElementById('scatter-options'); | |
| const showLines = document.getElementById('show-lines'); | |
| const showPoints = document.getElementById('show-points'); | |
| const showTrendline = document.getElementById('show-trendline'); | |
| const colorScheme = document.getElementById('color-scheme'); | |
| const colorOptions = document.querySelectorAll('.color-option'); | |
| const animationDuration = document.getElementById('animation-duration'); | |
| const durationValue = document.getElementById('duration-value'); | |
| const generateBtn = document.getElementById('generate-btn'); | |
| const downloadBtn = document.getElementById('download-btn'); | |
| const chartTitle = document.getElementById('chart-title'); | |
| const chartTitleDisplay = document.getElementById('chart-title-display'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toast-message'); | |
| const fullscreenBtn = document.getElementById('fullscreen-btn'); | |
| const fullscreenModal = document.getElementById('fullscreen-modal'); | |
| const closeFullscreen = document.getElementById('close-fullscreen'); | |
| const fullscreenTitle = document.getElementById('fullscreen-title'); | |
| const graph = document.getElementById('graph'); | |
| const fullscreenChart = document.getElementById('fullscreen-chart'); | |
| const xAxisSelect = document.getElementById('x-axis-select'); | |
| const yAxisOptions = document.getElementById('y-axis-options'); | |
| const chartInfo = document.getElementById('chart-info'); | |
| const dataCount = document.getElementById('data-count'); | |
| const exportDataBtn = document.getElementById('export-data-btn'); | |
| const separatorOptions = document.querySelectorAll('.separator-option'); | |
| // Dataset elements | |
| const dataset1Label = document.getElementById('dataset1-label'); | |
| const dataset2Label = document.getElementById('dataset2-label'); | |
| // Chart instance | |
| let chart = null; | |
| let fullscreenChartInstance = null; | |
| let currentColors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4', '#EC4899', '#F97316']; | |
| let csvHeaders = []; | |
| let csvData = []; | |
| let selectedYColumns = []; | |
| let currentSeparator = ','; | |
| // Initialize chart with empty data | |
| function initChart() { | |
| return new Chart(graph, { | |
| type: 'bar', | |
| data: { | |
| labels: [], | |
| datasets: [] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: true, | |
| position: 'bottom' | |
| }, | |
| tooltip: { | |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', | |
| titleColor: '#fff', | |
| bodyColor: '#fff', | |
| borderColor: '#333', | |
| borderWidth: 1, | |
| cornerRadius: 6, | |
| displayColors: true | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| grid: { | |
| color: 'rgba(0, 0, 0, 0.05)' | |
| }, | |
| ticks: { | |
| color: '#6b7280' | |
| } | |
| }, | |
| x: { | |
| grid: { | |
| color: 'rgba(0, 0, 0, 0.05)' | |
| }, | |
| ticks: { | |
| color: '#6b7280' | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Initialize chart | |
| chart = initChart(); | |
| // Event Listeners | |
| manualInputBtn.addEventListener('click', function() { | |
| manualInputBtn.classList.add('bg-indigo-100', 'text-indigo-800'); | |
| manualInputBtn.classList.remove('bg-gray-100', 'text-gray-700'); | |
| fileInputBtn.classList.remove('bg-indigo-100', 'text-indigo-800'); | |
| fileInputBtn.classList.add('bg-gray-100', 'text-gray-700'); | |
| manualInput.classList.remove('hidden'); | |
| fileInput.classList.add('hidden'); | |
| }); | |
| fileInputBtn.addEventListener('click', function() { | |
| fileInputBtn.classList.add('bg-indigo-100', 'text-indigo-800'); | |
| fileInputBtn.classList.remove('bg-gray-100', 'text-gray-700'); | |
| manualInputBtn.classList.remove('bg-indigo-100', 'text-indigo-800'); | |
| manualInputBtn.classList.add('bg-gray-100', 'text-gray-700'); | |
| fileInput.classList.remove('hidden'); | |
| manualInput.classList.add('hidden'); | |
| }); | |
| // Add new multi data point | |
| addMultiPointBtn.addEventListener('click', function() { | |
| const point = document.createElement('div'); | |
| point.className = 'multi-data-point'; | |
| point.innerHTML = ` | |
| <input type="text" class="category-input w-1/4" placeholder="Category"> | |
| <input type="number" class="dataset1-input w-1/4" placeholder="Dataset 1"> | |
| <input type="number" class="dataset2-input w-1/4" placeholder="Dataset 2"> | |
| <i class="fas fa-trash remove-multi-point"></i> | |
| `; | |
| multiDataPointsContainer.appendChild(point); | |
| // Add event listener to remove button | |
| point.querySelector('.remove-multi-point').addEventListener('click', function() { | |
| multiDataPointsContainer.removeChild(point); | |
| }); | |
| }); | |
| // Remove multi data point | |
| multiDataPointsContainer.addEventListener('click', function(e) { | |
| if (e.target.classList.contains('remove-multi-point')) { | |
| const point = e.target.closest('.multi-data-point'); | |
| if (multiDataPointsContainer.children.length > 1) { | |
| multiDataPointsContainer.removeChild(point); | |
| } | |
| } | |
| }); | |
| // Handle CSV separator selection | |
| separatorOptions.forEach(option => { | |
| option.addEventListener('click', function() { | |
| separatorOptions.forEach(opt => { | |
| opt.classList.remove('selected'); | |
| }); | |
| this.classList.add('selected'); | |
| // Update the current separator | |
| currentSeparator = this.dataset.separator; | |
| // If a file is already loaded, re-parse it with the new separator | |
| if (csvFile.files[0]) { | |
| const file = csvFile.files[0]; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const content = e.target.result; | |
| // Parse CSV data with the selected separator | |
| parseCSVData(content); | |
| }; | |
| reader.readAsText(file); | |
| } | |
| }); | |
| }); | |
| // Handle file upload | |
| csvFile.addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const content = e.target.result; | |
| // Parse CSV data with the selected separator | |
| parseCSVData(content); | |
| }; | |
| reader.readAsText(file); | |
| }); | |
| // Function to parse CSV data | |
| function parseCSVData(content) { | |
| // Replace the tab literal with actual tab character | |
| const separator = currentSeparator === '\\t' ? '\t' : currentSeparator; | |
| // Split the content into lines | |
| const lines = content.split('\n'); | |
| // Parse the CSV data | |
| csvData = []; | |
| for (let i = 0; i < lines.length; i++) { | |
| if (lines[i].trim() === '') continue; | |
| // Handle quoted fields that may contain the separator | |
| let rowData = []; | |
| let inQuotes = false; | |
| let field = ''; | |
| for (let j = 0; j < lines[i].length; j++) { | |
| let char = lines[i][j]; | |
| // Handle quoted fields | |
| if (char === '"') { | |
| if (inQuotes && j < lines[i].length - 1 && lines[i][j + 1] === '"') { | |
| // Two consecutive quotes represent a literal quote in the field | |
| field += '"'; | |
| j++; // Skip the next quote | |
| } else { | |
| inQuotes = !inQuotes; | |
| } | |
| } else if (char === separator && !inQuotes) { | |
| // End of field | |
| rowData.push(field); | |
| field = ''; | |
| } else { | |
| // Regular character | |
| field += char; | |
| } | |
| } | |
| // Don't forget the last field | |
| if (field.length > 0 || lines[i][lines[i].length - 1] === separator) { | |
| // The last character was a separator, so add an empty field | |
| rowData.push(field); | |
| } | |
| csvData.push(rowData.map(cell => cell.trim())); | |
| } | |
| // Extract headers from first row | |
| if (csvData.length > 0) { | |
| csvHeaders = csvData[0]; | |
| // Display preview | |
| let previewHTML = '<table class="w-full text-xs">'; | |
| // Show headers | |
| previewHTML += '<tr class="bg-gray-100">'; | |
| csvHeaders.forEach(header => { | |
| previewHTML += `<th class="px-2 py-1 text-left border-b">${header || 'Empty'}</th>`; | |
| }); | |
| previewHTML += '</tr>'; | |
| // Show data rows (max 5) | |
| for (let i = 1; i < Math.min(csvData.length, 6); i++) { | |
| previewHTML += '<tr>'; | |
| csvData[i].forEach(cell => { | |
| previewHTML += `<td class="px-2 py-1 border-b">${cell || 'Empty'}</td>`; | |
| }); | |
| previewHTML += '</tr>'; | |
| } | |
| if (csvData.length > 6) { | |
| previewHTML += '<tr><td colspan="' + csvHeaders.length + '" class="px-2 py-1 text-center text-gray-500">... and ' + (csvData.length - 6) + ' more rows</td></tr>'; | |
| } | |
| previewHTML += '</table>'; | |
| csvContent.innerHTML = previewHTML; | |
| csvPreview.classList.remove('hidden'); | |
| // Populate X-axis select | |
| xAxisSelect.innerHTML = '<option value="">Select X-axis column</option>'; | |
| csvHeaders.forEach((header, index) => { | |
| const option = document.createElement('option'); | |
| option.value = index; | |
| option.textContent = header || `Column ${index + 1}`; | |
| xAxisSelect.appendChild(option); | |
| }); | |
| // Populate Y-axis options | |
| yAxisOptions.innerHTML = ''; | |
| csvHeaders.forEach((header, index) => { | |
| const optionDiv = document.createElement('div'); | |
| optionDiv.className = 'flex items-center dataset-option'; | |
| optionDiv.dataset.column = index; | |
| optionDiv.innerHTML = ` | |
| <input type="checkbox" id="y-col-${index}" class="mr-2 h-4 w-4 text-indigo-600 rounded focus:ring-indigo-500"> | |
| <label for="y-col-${index}" class="text-sm font-medium text-gray-700">${header || `Column ${index + 1}`}</label> | |
| `; | |
| optionDiv.querySelector('input').addEventListener('change', function() { | |
| if (this.checked) { | |
| if (!selectedYColumns.includes(parseInt(index))) { | |
| selectedYColumns.push(parseInt(index)); | |
| } | |
| } else { | |
| selectedYColumns = selectedYColumns.filter(col => col !== parseInt(index)); | |
| } | |
| }); | |
| yAxisOptions.appendChild(optionDiv); | |
| }); | |
| } | |
| } | |
| // Update animation duration display | |
| animationDuration.addEventListener('input', function() { | |
| durationValue.textContent = `${this.value}ms`; | |
| }); | |
| // Apply color scheme | |
| colorOptions.forEach(option => { | |
| option.addEventListener('click', function() { | |
| colorOptions.forEach(opt => { | |
| opt.classList.remove('ring-2', 'ring-indigo-500'); | |
| }); | |
| this.classList.add('ring-2', 'ring-indigo-500'); | |
| currentColors = JSON.parse(this.dataset.colors); | |
| }); | |
| }); | |
| // Set default color scheme | |
| colorOptions[0].classList.add('ring-2', 'ring-indigo-500'); | |
| // Chart type change | |
| chartType.addEventListener('change', function() { | |
| if (this.value === 'line') { | |
| lineOptions.classList.remove('hidden'); | |
| scatterOptions.classList.add('hidden'); | |
| } else if (this.value === 'scatter') { | |
| lineOptions.classList.add('hidden'); | |
| scatterOptions.classList.remove('hidden'); | |
| } else { | |
| lineOptions.classList.add('hidden'); | |
| scatterOptions.classList.add('hidden'); | |
| } | |
| }); | |
| // Generate chart | |
| generateBtn.addEventListener('click', function() { | |
| // Get data based on input method | |
| if (manualInput.classList.contains('hidden')) { | |
| // File input method | |
| const file = csvFile.files[0]; | |
| if (!file) { | |
| showToast('Please select a CSV file'); | |
| return; | |
| } | |
| const xColumn = xAxisSelect.value; | |
| if (!xColumn) { | |
| showToast('Please select an X-axis column'); | |
| return; | |
| } | |
| if (selectedYColumns.length === 0) { | |
| showToast('Please select at least one Y-axis column'); | |
| return; | |
| } | |
| // Extract data for chart | |
| const labels = []; | |
| const datasets = []; | |
| // Use headers as labels if the selected X column contains non-numeric data | |
| // Otherwise use the values from that column | |
| for (let i = 1; i < csvData.length; i++) { | |
| if (csvData[i].length > xColumn) { | |
| labels.push(csvData[i][xColumn]); | |
| } else { | |
| labels.push(`Row ${i}`); | |
| } | |
| } | |
| // Create dataset for each selected Y column | |
| selectedYColumns.forEach((colIndex, datasetIndex) => { | |
| const dataset = { | |
| label: csvHeaders[colIndex] || `Dataset ${datasetIndex + 1}`, | |
| data: [], | |
| backgroundColor: currentColors[datasetIndex % currentColors.length], | |
| borderColor: currentColors[datasetIndex % currentColors.length], | |
| borderWidth: 1 | |
| }; | |
| for (let i = 1; i < csvData.length; i++) { | |
| if (csvData[i].length > colIndex) { | |
| const value = parseFloat(csvData[i][colIndex]); | |
| if (!isNaN(value)) { | |
| dataset.data.push(value); | |
| } else { | |
| dataset.data.push(null); | |
| } | |
| } else { | |
| dataset.data.push(null); | |
| } | |
| } | |
| datasets.push(dataset); | |
| }); | |
| createChart(labels, datasets); | |
| } else { | |
| // Manual input method | |
| const points = multiDataPointsContainer.querySelectorAll('.multi-data-point'); | |
| let labels = []; | |
| let datasets = [ | |
| { | |
| label: dataset1Label.value, | |
| data: [], | |
| backgroundColor: currentColors[0], | |
| borderColor: currentColors[0], | |
| borderWidth: 1 | |
| }, | |
| { | |
| label: dataset2Label.value, | |
| data: [], | |
| backgroundColor: currentColors[1], | |
| borderColor: currentColors[1], | |
| borderWidth: 1 | |
| } | |
| ]; | |
| let isValid = true; | |
| let hasData = false; | |
| points.forEach(point => { | |
| const category = point.querySelector('.category-input').value; | |
| const value1 = point.querySelector('.dataset1-input').value; | |
| const value2 = point.querySelector('.dataset2-input').value; | |
| if (category) { | |
| labels.push(category); | |
| hasData = true; | |
| } | |
| if (value1 !== '') { | |
| const num1 = parseFloat(value1); | |
| if (!isNaN(num1)) { | |
| datasets[0].data.push(num1); | |
| } else { | |
| datasets[0].data.push(null); | |
| } | |
| } else if (category) { | |
| datasets[0].data.push(null); | |
| } | |
| if (value2 !== '') { | |
| const num2 = parseFloat(value2); | |
| if (!isNaN(num2)) { | |
| datasets[1].data.push(num2); | |
| } else { | |
| datasets[1].data.push(null); | |
| } | |
| } else if (category) { | |
| datasets[1].data.push(null); | |
| } | |
| if ((value1 !== '' || value2 !== '') && !category) { | |
| isValid = false; | |
| } | |
| }); | |
| // Remove datasets that have no data | |
| datasets = datasets.filter(dataset => dataset.data.some(val => val !== null)); | |
| if (!isValid) { | |
| showToast('Please fill in category names for data points'); | |
| return; | |
| } | |
| if (!hasData) { | |
| showToast('Please add at least one data point with category'); | |
| return; | |
| } | |
| if (datasets.length === 0) { | |
| showToast('Please add at least one data value'); | |
| return; | |
| } | |
| createChart(labels, datasets); | |
| } | |
| }); | |
| // Create chart with data | |
| function createChart(labels, datasets) { | |
| // Update chart title | |
| const title = chartTitle.value || 'Your Chart'; | |
| chartTitleDisplay.textContent = title; | |
| // Count data points | |
| const pointCount = datasets.reduce((total, dataset) => { | |
| return total + dataset.data.filter(val => val !== null).length; | |
| }, 0); | |
| dataCount.textContent = `${pointCount} points`; | |
| chartInfo.classList.remove('hidden'); | |
| // Destroy existing chart | |
| if (chart) { | |
| chart.destroy(); | |
| } | |
| // Process labels if they look like dates or numbers | |
| let xAxisType = 'category'; | |
| let allLabelsAreNumbers = true; | |
| let allLabelsAreDates = true; | |
| labels.forEach(label => { | |
| const strLabel = String(label); | |
| // Check if it's a number | |
| if (isNaN(parseFloat(strLabel))) { | |
| allLabelsAreNumbers = false; | |
| } | |
| // Check if it's a date (very basic check) | |
| if (!/\d{4}-\d{2}-\d{2}/.test(strLabel) && | |
| !/\d{2}\/\d{2}\/\d{4}/.test(strLabel) && | |
| isNaN(Date.parse(strLabel))) { | |
| allLabelsAreDates = false; | |
| } | |
| }); | |
| if (allLabelsAreNumbers && chartType.value !== 'pie' && chartType.value !== 'doughnut') { | |
| xAxisType = 'linear'; | |
| labels = labels.map(label => parseFloat(label)); | |
| } else if (allLabelsAreDates) { | |
| xAxisType = 'time'; | |
| // Chart.js with Luxon adapter will handle the date parsing | |
| } | |
| // Chart configuration | |
| const chartConfig = { | |
| type: chartType.value, | |
| data: { | |
| labels: labels, | |
| datasets: datasets | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { | |
| duration: parseInt(animationDuration.value) | |
| }, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: title, | |
| font: { | |
| size: 16 | |
| } | |
| }, | |
| legend: { | |
| display: true, | |
| position: 'bottom' | |
| }, | |
| tooltip: { | |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', | |
| titleColor: '#fff', | |
| bodyColor: '#fff', | |
| borderColor: '#333', | |
| borderWidth: 1, | |
| cornerRadius: 6, | |
| displayColors: true | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| grid: { | |
| color: 'rgba(0, 0, 0, 0.05)' | |
| }, | |
| ticks: { | |
| color: '#6b7280' | |
| }, | |
| title: { | |
| display: true, | |
| text: yLabel.value || 'Value', | |
| color: '#6b7280' | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| // Configure X-axis based on type | |
| if (xAxisType === 'linear') { | |
| chartConfig.options.scales.x = { | |
| type: 'linear', | |
| grid: { | |
| color: 'rgba(0, 0, 0, 0.05)' | |
| }, | |
| ticks: { | |
| color: '#6b7280' | |
| }, | |
| title: { | |
| display: true, | |
| text: xLabel.value || 'Value', | |
| color: '#6b7280' | |
| } | |
| }; | |
| } else if (xAxisType === 'time') { | |
| chartConfig.options.scales.x = { | |
| type: 'time', | |
| time: { | |
| tooltipFormat: 'll' | |
| }, | |
| grid: { | |
| color: 'rgba(0, 0, 0, 0.05)' | |
| }, | |
| ticks: { | |
| color: '#6b7280' | |
| }, | |
| title: { | |
| display: true, | |
| text: xLabel.value || 'Date', | |
| color: '#6b7280' | |
| } | |
| }; | |
| } else { | |
| chartConfig.options.scales.x = { | |
| grid: { | |
| color: 'rgba(0, 0, 0, 0.05)' | |
| }, | |
| ticks: { | |
| color: '#6b7280' | |
| }, | |
| title: { | |
| display: true, | |
| text: xLabel.value || 'Category', | |
| color: '#6b7280' | |
| } | |
| }; | |
| } | |
| // Special configurations for different chart types | |
| datasets.forEach((dataset, index) => { | |
| if (chartType.value === 'bar') { | |
| dataset.backgroundColor = currentColors[index % currentColors.length]; | |
| dataset.borderColor = currentColors[index % currentColors.length]; | |
| } else if (chartType.value === 'radar') { | |
| dataset.backgroundColor = `${currentColors[index % currentColors.length]}44`; // 44 for 25% opacity | |
| dataset.borderColor = currentColors[index % currentColors.length]; | |
| } else if (chartType.value === 'line') { | |
| dataset.backgroundColor = `${currentColors[index % currentColors.length]}1A`; // 1A for 10% opacity | |
| dataset.borderColor = currentColors[index % currentColors.length]; | |
| dataset.borderWidth = 3; | |
| dataset.pointBackgroundColor = currentColors[index % currentColors.length]; | |
| dataset.pointBorderColor = '#fff'; | |
| dataset.pointBorderWidth = 2; | |
| dataset.pointRadius = showPoints.checked ? 4 : 0; | |
| if (!showLines.checked) { | |
| dataset.borderWidth = 0; | |
| } | |
| if (!showPoints.checked) { | |
| dataset.pointRadius = 0; | |
| } | |
| } else if (chartType.value === 'scatter') { | |
| dataset.pointBackgroundColor = currentColors[index % currentColors.length]; | |
| dataset.pointBorderColor = '#fff'; | |
| dataset.pointBorderWidth = 1; | |
| dataset.pointRadius = 6; | |
| chartConfig.options.scales.x.title.display = true; | |
| // Add trendline if requested | |
| if (showTrendline.checked) { | |
| // Calculate linear regression | |
| const data = dataset.data; | |
| let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0; | |
| let validPoints = 0; | |
| for (let i = 0; i < data.length; i++) { | |
| if (data[i] !== null) { | |
| sumX += i; | |
| sumY += data[i]; | |
| sumXY += i * data[i]; | |
| sumXX += i * i; | |
| validPoints++; | |
| } | |
| } | |
| if (validPoints > 1) { | |
| const slope = (validPoints * sumXY - sumX * sumY) / (validPoints * sumXX - sumX * sumX); | |
| const intercept = (sumY - slope * sumX) / validPoints; | |
| // Add trendline dataset | |
| const trendData = []; | |
| const startX = 0; | |
| const endX = data.length - 1; | |
| trendData.push({x: startX, y: slope * startX + intercept}); | |
| trendData.push({x: endX, y: slope * endX + intercept}); | |
| chartConfig.data.datasets.push({ | |
| label: `${dataset.label} Trendline`, | |
| data: trendData, | |
| showLine: true, | |
| pointRadius: 0, | |
| borderColor: dataset.borderColor, | |
| borderDash: [5, 5], | |
| borderWidth: 2 | |
| }); | |
| } | |
| } | |
| // For scatter, we need to format data as {x, y} objects | |
| const xLabels = chartConfig.data.labels; | |
| const scatterData = []; | |
| for (let i = 0; i < data.length; i++) { | |
| if (data[i] !== null) { | |
| let xVal; | |
| // If labels are numbers, use them as X values | |
| if (!isNaN(parseFloat(xLabels[i]))) { | |
| xVal = parseFloat(xLabels[i]); | |
| } else { | |
| xVal = i; // Otherwise use index | |
| } | |
| scatterData.push({x: xVal, y: data[i]}); | |
| } | |
| } | |
| // Clear the original data and replace with scatter data | |
| dataset.data = scatterData; | |
| // Update config for scatter | |
| if (xAxisType === 'linear' && !isNaN(parseFloat(xLabels[0]))) { | |
| chartConfig.options.scales.x.type = 'linear'; | |
| chartConfig.options.scales.x.title.text = xLabel.value || 'X Value'; | |
| } else { | |
| chartConfig.options.scales.x.type = 'linear'; | |
| chartConfig.options.scales.x.title.text = xLabel.value || 'Index'; | |
| } | |
| chartConfig.options.scales.y.title.text = yLabel.value || 'Y Value'; | |
| } else if (chartType.value === 'bubble') { | |
| // Convert data to bubble format | |
| const bubbleData = []; | |
| for (let i = 0; i < data.length; i++) { | |
| if (data[i] !== null) { | |
| bubbleData.push({ | |
| x: i, | |
| y: data[i], | |
| r: Math.abs(data[i]) * 2 + 5 | |
| }); | |
| } | |
| } | |
| dataset.data = bubbleData; | |
| chartConfig.options.scales.x.min = 0; | |
| chartConfig.options.scales.x.max = Math.max(data.length - 1, 1); | |
| } | |
| }); | |
| // Create new chart | |
| chart = new Chart(graph, chartConfig); | |
| // Hide empty state | |
| emptyState.classList.add('hidden'); | |
| // Add animation class to chart | |
| graph.classList.add('chart-update'); | |
| setTimeout(() => { | |
| graph.classList.remove('chart-update'); | |
| }, 300); | |
| // Show success message | |
| showToast('Chart generated successfully!'); | |
| } | |
| // Download chart | |
| downloadBtn.addEventListener('click', function() { | |
| if (!chart) { | |
| showToast('No chart to download'); | |
| return; | |
| } | |
| const now = new Date(); | |
| const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}`; | |
| const filename = `chart_${timestamp}.png`; | |
| const link = document.createElement('a'); | |
| link.download = filename; | |
| link.href = chart.toBase64Image(); | |
| link.click(); | |
| showToast(`Chart downloaded as ${filename}`); | |
| }); | |
| // Export data | |
| exportDataBtn.addEventListener('click', function() { | |
| if (!chart) { | |
| showToast('No data to export'); | |
| return; | |
| } | |
| // Create CSV content from chart data | |
| let csvContent = "data:text/csv;charset=utf-8,"; | |
| // Add headers | |
| const datasets = chart.data.datasets; | |
| const labels = chart.data.labels; | |
| // Build header row | |
| let headerRow = "X"; | |
| datasets.forEach(dataset => { | |
| headerRow += `,${dataset.label}`; | |
| }); | |
| csvContent += headerRow + "\n"; | |
| // Build data rows | |
| for (let i = 0; i < labels.length; i++) { | |
| let row = labels[i]; | |
| datasets.forEach(dataset => { | |
| // Handle different data formats (simple values, objects with x/y, etc.) | |
| let value = ''; | |
| if (Array.isArray(dataset.data)) { | |
| const dataPoint = dataset.data[i]; | |
| if (dataPoint !== null && typeof dataPoint === 'object' && dataPoint.y !== undefined) { | |
| value = dataPoint.y; | |
| } else if (dataPoint !== null) { | |
| value = dataPoint; | |
| } | |
| } | |
| row += `,${value}`; | |
| }); | |
| csvContent += row + "\n"; | |
| } | |
| // Create download link | |
| const encodedUri = encodeURI(csvContent); | |
| const link = document.createElement("a"); | |
| link.setAttribute("href", encodedUri); | |
| link.setAttribute("download", "chart_data.csv"); | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| showToast('Data exported successfully!'); | |
| }); | |
| // Fullscreen functionality | |
| fullscreenBtn.addEventListener('click', function() { | |
| fullscreenModal.classList.remove('hidden'); | |
| fullscreenModal.classList.add('slide-in'); | |
| // Create fullscreen chart | |
| if (fullscreenChartInstance) { | |
| fullscreenChartInstance.destroy(); | |
| } | |
| fullscreenTitle.textContent = chartTitleDisplay.textContent; | |
| fullscreenChartInstance = new Chart(fullscreenChart, JSON.parse(JSON.stringify(chart.config))); | |
| }); | |
| closeFullscreen.addEventListener('click', function() { | |
| fullscreenModal.classList.remove('slide-in'); | |
| fullscreenModal.classList.add('slide-out'); | |
| setTimeout(() => { | |
| fullscreenModal.classList.add('hidden'); | |
| fullscreenModal.classList.remove('slide-out'); | |
| if (fullscreenChartInstance) { | |
| fullscreenChartInstance.destroy(); | |
| fullscreenChartInstance = null; | |
| } | |
| }, 300); | |
| }); | |
| // Toast notification | |
| function showToast(message) { | |
| toastMessage.textContent = message; | |
| toast.classList.remove('hidden'); | |
| toast.classList.add('slide-in'); | |
| setTimeout(() => { | |
| toast.classList.remove('slide-in'); | |
| toast.classList.add('slide-out'); | |
| setTimeout(() => { | |
| toast.classList.add('hidden'); | |
| toast.classList.remove('slide-out'); | |
| }, 300); | |
| }, 3000); | |
| } | |
| // Initialize with empty data point if none exists | |
| if (multiDataPointsContainer.children.length === 0) { | |
| addMultiPointBtn.click(); | |
| } | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=alterzick/graph-creator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |