explorer / index.html
spencerlima's picture
Add 2 files
8007a6c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Explorer - Grammar of Graphics</title>
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
<script src="https://cdn.jsdelivr.net/npm/papaparse@5.3.0/papaparse.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
:root {
--primary: #1a73e8;
--primary-light: #e8f0fe;
--secondary: #f1f3f4;
--text: #202124;
--text-light: #5f6368;
--border: #dadce0;
--white: #ffffff;
--success: #34a853;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
color: var(--text);
background-color: #f8f9fa;
line-height: 1.6;
padding: 0;
margin: 0;
min-height: 100vh;
}
.container {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
gap: 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo i {
color: var(--primary);
font-size: 24px;
}
.logo h1 {
font-size: 20px;
font-weight: 600;
}
.upload-area {
background-color: var(--white);
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.upload-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px;
border: 2px dashed var(--border);
border-radius: 6px;
background-color: var(--primary-light);
cursor: pointer;
transition: all 0.3s ease;
}
.upload-container:hover {
background-color: rgba(26, 115, 232, 0.1);
border-color: var(--primary);
}
.upload-icon {
font-size: 36px;
color: var(--primary);
margin-bottom: 10px;
}
.upload-text {
color: var(--text-light);
margin-bottom: 15px;
text-align: center;
}
.btn {
background-color: var(--primary);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 14px;
transition: background-color 0.2s;
}
.btn:hover {
background-color: #1557b0;
}
.btn-outline {
background-color: transparent;
color: var(--primary);
border: 1px solid var(--primary);
}
.btn-outline:hover {
background-color: var(--primary-light);
}
.btn-group {
display: flex;
gap: 10px;
}
input[type="file"] {
display: none;
}
.main-content {
display: flex;
gap: 20px;
}
.sidebar {
width: 300px;
background-color: var(--white);
border-radius: 8px;
padding: 15px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 20px;
}
.panel {
background-color: var(--white);
border-radius: 8px;
padding: 15px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.panel-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.panel-title i {
color: var(--text-light);
}
.control-group {
margin-bottom: 15px;
}
.control-label {
display: block;
font-size: 14px;
color: var(--text-light);
margin-bottom: 5px;
font-weight: 500;
}
select, input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 4px;
font-family: inherit;
font-size: 14px;
background-color: var(--white);
}
select {
cursor: pointer;
}
.color-option {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 5px;
cursor: pointer;
}
.color-preview {
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid var(--border);
}
.visualization {
flex: 1;
background-color: var(--white);
border-radius: 8px;
padding: 15px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
#chart {
width: 100%;
min-height: 500px;
}
.hidden {
display: none;
}
.data-preview {
margin-top: 20px;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 4px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th, td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background-color: var(--secondary);
font-weight: 500;
color: var(--text-light);
position: sticky;
top: 0;
}
tr:hover {
background-color: #f5f5f5;
}
.success-message {
background-color: #e6f4ea;
color: var(--success);
padding: 10px 15px;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.success-message i {
font-size: 16px;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
margin-bottom: 15px;
}
.tab {
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--text-light);
position: relative;
}
.tab.active {
color: var(--primary);
}
.tab.active:after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background-color: var(--primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.field-list {
list-style: none;
margin: 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 4px;
}
.field-item {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
cursor: grab;
font-size: 13px;
}
.field-item:hover {
background-color: var(--secondary);
}
.field-item.dragging {
opacity: 0.5;
}
.drop-area {
border: 2px dashed var(--border);
padding: 15px;
border-radius: 4px;
min-height: 50px;
margin-bottom: 15px;
background-color: var(--primary-light);
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.drop-area.highlight {
border-color: var(--primary);
background-color: rgba(26, 115, 232, 0.1);
}
.pill {
background-color: var(--primary);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.pill i {
cursor: pointer;
font-size: 12px;
}
footer {
text-align: center;
padding: 15px 0;
color: var(--text-light);
font-size: 13px;
margin-top: 30px;
border-top: 1px solid var(--border);
}
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.sidebar {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">
<i class="fas fa-chart-bar"></i>
<h1>Grammar of Graphics Explorer</h1>
</div>
<div class="btn-group">
<button id="reset-btn" class="btn btn-outline">
<i class="fas fa-redo"></i> Reset
</button>
</div>
</header>
<div id="upload-section" class="upload-area">
<div class="upload-container" id="upload-container">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<p class="upload-text">Drag & drop your CSV file here or click to browse</p>
<button class="btn">
<i class="fas fa-file-import"></i> Select File
</button>
<input type="file" id="fileInput" accept=".csv">
</div>
<div id="upload-success" class="success-message hidden">
<i class="fas fa-check-circle"></i>
<span id="success-message-text">File uploaded successfully!</span>
</div>
<div id="data-preview" class="data-preview hidden">
<table>
<thead id="preview-head">
<tr></tr>
</thead>
<tbody id="preview-body">
</tbody>
</table>
</div>
</div>
<div id="explorer-section" class="hidden">
<div class="main-content">
<div class="sidebar">
<div class="panel">
<div class="panel-title">
<i class="fas fa-sliders-h"></i>
<span>Visual Encoding</span>
</div>
<div class="tabs">
<div class="tab active" data-tab="encodings">Encodings</div>
<div class="tab" data-tab="transformations">Transformations</div>
</div>
<div class="tab-content active" id="encodings-tab">
<div class="control-group">
<label class="control-label">X-Axis</label>
<div class="drop-area" id="x-drop-area"></div>
<select id="x-axis" disabled>
<option value="">Select field</option>
</select>
</div>
<div class="control-group">
<label class="control-label">Y-Axis</label>
<div class="drop-area" id="y-drop-area"></div>
<select id="y-axis" disabled>
<option value="">Select field</option>
</select>
</div>
<div class="control-group">
<label class="control-label">Color</label>
<div class="drop-area" id="color-drop-area"></div>
<select id="color" disabled>
<option value="">Select field</option>
</select>
</div>
<div class="control-group">
<label class="control-label">Size</label>
<div class="drop-area" id="size-drop-area"></div>
<select id="size" disabled>
<option value="">Select field</option>
</select>
</div>
</div>
<div class="tab-content" id="transformations-tab">
<div class="control-group">
<label class="control-label">Mark Type</label>
<select id="mark-type">
<option value="point">Point</option>
<option value="bar">Bar</option>
<option value="line">Line</option>
<option value="area">Area</option>
<option value="circle">Circle</option>
<option value="square">Square</option>
<option value="text">Text</option>
</select>
</div>
<div class="control-group">
<label class="control-label">Aggregation</label>
<select id="aggregation">
<option value="none">None</option>
<option value="mean">Mean</option>
<option value="sum">Sum</option>
<option value="median">Median</option>
<option value="min">Min</option>
<option value="max">Max</option>
<option value="count">Count</option>
</select>
</div>
<div class="control-group">
<label class="control-label">Sort</label>
<select id="sort">
<option value="none">None</option>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
</div>
</div>
<div class="panel">
<div class="panel-title">
<i class="fas fa-list"></i>
<span>Data Fields</span>
</div>
<ul class="field-list" id="field-list"></ul>
</div>
</div>
<div class="visualization">
<div id="chart"></div>
</div>
</div>
</div>
<footer>
Grammar of Graphics Explorer | Built with Vega-Lite
</footer>
</div>
<script>
// Global variables
let data = [];
let rawData = '';
let currentChart = null;
let fieldTypes = {};
// DOM elements
const uploadContainer = document.getElementById('upload-container');
const fileInput = document.getElementById('fileInput');
const uploadSuccess = document.getElementById('upload-success');
const successMessageText = document.getElementById('success-message-text');
const dataPreview = document.getElementById('data-preview');
const previewHead = document.getElementById('preview-head');
const previewBody = document.getElementById('preview-body');
const explorerSection = document.getElementById('explorer-section');
const fieldList = document.getElementById('field-list');
const chartContainer = document.getElementById('chart');
const resetBtn = document.getElementById('reset-btn');
// Encoding elements
const xAxisSelect = document.getElementById('x-axis');
const yAxisSelect = document.getElementById('y-axis');
const colorSelect = document.getElementById('color');
const sizeSelect = document.getElementById('size');
const markTypeSelect = document.getElementById('mark-type');
const aggregationSelect = document.getElementById('aggregation');
const sortSelect = document.getElementById('sort');
// Drop areas
const xDropArea = document.getElementById('x-drop-area');
const yDropArea = document.getElementById('y-drop-area');
const colorDropArea = document.getElementById('color-drop-area');
const sizeDropArea = document.getElementById('size-drop-area');
const dropAreas = [xDropArea, yDropArea, colorDropArea, sizeDropArea];
// Tab functionality
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const tabId = `${tab.dataset.tab}-tab`;
document.getElementById(tabId).classList.add('active');
});
});
// Handle file upload
uploadContainer.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
// Handle drag and drop
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadContainer.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadContainer.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadContainer.addEventListener(eventName, unhighlight, false);
});
function highlight() {
uploadContainer.style.borderColor = 'var(--primary)';
uploadContainer.style.backgroundColor = 'rgba(26, 115, 232, 0.1)';
}
function unhighlight() {
uploadContainer.style.borderColor = 'var(--border)';
uploadContainer.style.backgroundColor = 'var(--primary-light)';
}
uploadContainer.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length) {
fileInput.files = files;
handleFileSelect({ target: fileInput });
}
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
successMessageText.textContent = `File uploaded: ${file.name}`;
Papa.parse(file, {
header: true,
dynamicTyping: true,
complete: function(results) {
data = results.data;
rawData = results;
initializeExplorer();
}
});
}
function initializeExplorer() {
// Show success message
uploadSuccess.classList.remove('hidden');
dataPreview.classList.remove('hidden');
explorerSection.classList.remove('hidden');
// Populate preview table
const headers = Object.keys(data[0] || {});
renderPreview(headers);
// Populate field list
populateFields(headers);
// Enable controls
enableControls();
// Set up drag and drop
setupDragAndDrop();
}
function renderPreview(headers) {
// Clear previous content
previewHead.innerHTML = '';
previewBody.innerHTML = '';
// Add headers
const headRow = previewHead.querySelector('tr');
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header;
headRow.appendChild(th);
});
// Add sample data (first 10 rows)
const sampleData = data.slice(0, 10);
sampleData.forEach(row => {
const tr = document.createElement('tr');
headers.forEach(header => {
const td = document.createElement('td');
td.textContent = row[header] !== undefined ? row[header] : '';
tr.appendChild(td);
});
previewBody.appendChild(tr);
});
}
function populateFields(headers) {
fieldList.innerHTML = '';
headers.forEach(header => {
// Determine field type
const sampleValue = data[0][header];
let type = typeof sampleValue === 'number' ? 'quantitative' :
sampleValue instanceof Date ? 'temporal' : 'nominal';
fieldTypes[header] = type;
const li = document.createElement('li');
li.className = 'field-item';
li.textContent = header;
li.dataset.field = header;
li.dataset.type = type;
li.draggable = true;
// Add icon based on type
const icon = document.createElement('i');
icon.className = type === 'quantitative' ? 'fas fa-hashtag' :
type === 'temporal' ? 'fas fa-calendar-day' : 'fas fa-tag';
li.prepend(icon);
li.insertAdjacentText('beforeend', ` (${type})`);
fieldList.appendChild(li);
});
// Populate select dropdowns
populateSelectOptions();
}
function populateSelectOptions() {
// Clear previous options
[xAxisSelect, yAxisSelect, colorSelect, sizeSelect].forEach(select => {
select.innerHTML = '<option value="">Select field</option>';
select.disabled = false;
});
// Add options
Object.keys(fieldTypes).forEach(field => {
const type = fieldTypes[field];
[xAxisSelect, yAxisSelect].forEach(select => {
const option = document.createElement('option');
option.value = field;
option.textContent = `${field} (${type})`;
option.dataset.type = type;
select.appendChild(option);
});
// Color and size can use any field
[colorSelect, sizeSelect].forEach(select => {
const option = document.createElement('option
</html>