graph-creator / index.html
alterzick's picture
Add 2 files
6292155 verified
<!DOCTYPE html>
<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>