Public
Edited
Apr 28, 2024
Insert cell
Insert cell
viewof fileInput = inputFile()
Insert cell
viewof columnsForm = Generators.observe(notify => {
const updateColumnsForm = async () => {
const form = await createColumnSelectionForm(viewof fileInput);
notify(form);
};
viewof fileInput.addEventListener('change', updateColumnsForm);

updateColumnsForm();

return () => {
viewof fileInput.removeEventListener('change', updateColumnsForm);
};
});
Insert cell
viewof data = observeFilteredData(viewof fileInput, viewof columnsForm);
Insert cell
viewof controls = {
const columnTypes = detectColumnTypes(viewof data);
const controlElements = createFilterControls(columnTypes,viewof data);
return renderFilterControls(controlElements);
}
Insert cell
mutable filteredData = [];
Insert cell
tableOutput = Inputs.table(filteredData);
Insert cell
function detectColumnTypes(data, sampleSize = 10) {
const columnTypes = {};
const columns = Object.keys(data[0]);
const dateKeywords = ["date", "time", "datetime"];

columns.forEach(column => {
if (dateKeywords.some(keyword => column.toLowerCase().includes(keyword))) {
columnTypes[column] = 'date';
} else {
let typeCounts = {
'number': 0,
'string': 0,
'undefined': 0
};

for (let i = 0; i < Math.min(sampleSize, data.length); i++) {
let value = data[i][column];
let typedValue = d3.autoType({ [column]: value })[column];

if (typeof typedValue === 'number') {
typeCounts['number']++;
} else if (typeof typedValue === 'string') {
typeCounts['string']++;
} else {
typeCounts['undefined']++;
}
}

const nonEmptyCount = typeCounts['number'] + typeCounts['string'];
if (typeCounts['number'] === nonEmptyCount) {
columnTypes[column] = 'number';
} else if (typeCounts['string'] === nonEmptyCount) {
columnTypes[column] = 'string';
} else {
columnTypes[column] = 'unknown';
}
}
});

return columnTypes;
}
Insert cell
function parseDate(dateStr) {
const formats = ["yyyy/MM/dd", "dd/MM/yyyy","yyyy-MM-dd", "dd-MM-yyyy", "MM/dd/yyyy"];
for (let format of formats) {
const date = dateFns.parse(dateStr, format, new Date());
if (dateFns.isValid(date)) {
return date;
}
}
console.error("Failed to parse date:", dateStr);
}
Insert cell
function createFilterControls(columnTypes, data) {
const controls = {};


function updateAndRedraw() {
mutable filteredData = applyFilters(viewof data, controls);
console.log('Data filtered, new count: ', filteredData.length);
dispatchEvent(new CustomEvent('input', { bubbles: true }));
}

for (const [column, type] of Object.entries(columnTypes)) {
let control;
switch (type) {
case 'date': {
const dates = data.map(item => parseDate(item[column])).filter(d => d !== undefined && !isNaN(d.getTime()));
console.log('Date count: ', dates);
const minDate = new Date(Math.min(...dates.map(date => date.getTime())));
const maxDate = new Date(Math.max(...dates.map(date => date.getTime())));
const startDateInput = Inputs.date({label: `${column}: Start Date`, value: minDate, min: minDate, max: maxDate});
const endDateInput = Inputs.date({label: `${column}: End Date`, value: maxDate, min: minDate, max: maxDate});
if (isNaN(minDate.getTime()) || isNaN(maxDate.getTime())) {
console.error('Invalid date computed for controls.');
}

control = {
type: 'date',
element: () => {
const container = document.createElement('div');
container.appendChild(startDateInput);
container.appendChild(endDateInput);
return container;
},
getValue: () => [startDateInput.value, endDateInput.value]
};
break;
}
case 'number': {
const values = data.map(item => parseFloat(item[column])).filter(v => !isNaN(v));
const min = Math.min(...values);
const max = Math.max(...values);
const minInput = Inputs.text({label: `${column}: Minimum Value`, value: min.toString()});
const maxInput = Inputs.text({label: `${column}: Maximum Value`, value: max.toString()});

control = {
type: 'number',
element: () => {
const container = document.createElement('div');
container.appendChild(minInput);
container.appendChild(maxInput);
return container;
},
getValue: () => [parseFloat(minInput.value), parseFloat(maxInput.value)]
};
break;
}
case 'string': {
const uniqueOptions = Array.from(new Set(data.map(item => item[column])));
const selectionForm = createSelectionForm(uniqueOptions, {searchable: true, multiple: true});
control = {
type: 'string',
element: () => selectionForm,
getValue: () => [...selectionForm.querySelectorAll('input:checked')].map(el => el.value)
};
break;
}
}
controls[column] = control;
}
return {controls};
}
Insert cell
function renderFilterControls(controlSetup) {
const { controls } = controlSetup;
const container = document.createElement('div');
const applyButton = document.createElement('button');
applyButton.textContent = "Apply Filters";
applyButton.onclick = () => {
mutable filteredData = applyFilters(viewof data, controls);
console.log('Filtered Data:', mutable filteredData);
};
container.appendChild(applyButton);
for (const [column, control] of Object.entries(controls)) {
const header = document.createElement('h3');
header.textContent = column;
header.style.cursor = 'pointer';
const controlElement = control.element();
controlElement.style.display = 'none';
header.onclick = () => {
controlElement.style.display = controlElement.style.display === 'none' ? 'block' : 'none';
};
container.appendChild(header);
container.appendChild(controlElement);
}
return container;
}
Insert cell
function applyFilters( data, controls) {
return data.filter(item => {
return Object.entries(controls).every(([column, control]) => {
const controlValue = control.getValue();
if (control.type === 'date') {
const itemDate = new Date(item[column]);
return itemDate >= controlValue[0] && itemDate <= controlValue[1];
} else if (control.type === 'number') {
const itemNumber = parseFloat(item[column]);
return itemNumber >= controlValue[0] && itemNumber <= controlValue[1];
} else if (control.type === 'string') {
return controlValue.includes(item[column]);
}
return true;
});
});
}
Insert cell
import {inputFile, observeFilteredData, createSelectionForm, createColumnSelectionForm} from "a37b7594cef0e4b5"
Insert cell
dateFns = await require('https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.js');
Insert cell
d3 = require('d3')
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more