Skip to content

Commit fa6bc22

Browse files
committed
live data support
1 parent 816dc5a commit fa6bc22

14 files changed

Lines changed: 1867 additions & 1331 deletions

py-src/data_formulator/demo_stream_routes.py

Lines changed: 749 additions & 419 deletions
Large diffs are not rendered by default.

py-src/data_formulator/example_datasets_config.py

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -414,94 +414,6 @@
414414
}
415415
]
416416
},
417-
# ==========================================================================
418-
# LIVE DATA EXAMPLES - Auto-refresh streaming datasets
419-
# ==========================================================================
420-
{
421-
'source': 'demo',
422-
'name': 'Live Sales Feed (Live)',
423-
'description': 'Simulated e-commerce transactions with product details, regions, and channels. Updates every 1 second.',
424-
'live': True,
425-
'refreshIntervalSeconds': 1,
426-
'tables': [
427-
{
428-
"format": 'csv',
429-
"url": '/api/demo-stream/live-sales',
430-
"sample": '''transaction_id,timestamp,product,category,quantity,unit_price,discount_pct,total,region,channel
431-
TX123456,2024-01-15T10:30:00Z,Wireless Headphones,Electronics,2,71.99,10,143.98,North America,Web
432-
TX123457,2024-01-15T10:29:45Z,Running Shoes,Sports,1,129.99,0,129.99,Europe,Mobile App
433-
TX123458,2024-01-15T10:29:30Z,Coffee Maker,Home,1,80.99,10,80.99,Asia Pacific,Web'''
434-
}
435-
]
436-
},
437-
{
438-
'source': 'open-notify.org',
439-
'name': 'ISS Location (Live)',
440-
'description': 'Real-time International Space Station position trajectory. Updates every 5 seconds.',
441-
'live': True,
442-
'refreshIntervalSeconds': 5,
443-
'tables': [
444-
{
445-
"format": 'csv',
446-
"url": '/api/demo-stream/iss',
447-
"sample": '''timestamp,latitude,longitude,fetched_at
448-
2024-01-15T10:30:00Z,45.234,-122.456,2024-01-15T10:30:00Z
449-
2024-01-15T10:29:55Z,45.123,-122.345,2024-01-15T10:29:55Z
450-
2024-01-15T10:29:50Z,45.012,-122.234,2024-01-15T10:29:50Z'''
451-
}
452-
]
453-
},
454-
{
455-
'source': 'USGS',
456-
'name': 'Earthquakes (Live)',
457-
'description': 'Real-time earthquake data from USGS. New quakes appear over time. Updates every 60 seconds.',
458-
'live': True,
459-
'refreshIntervalSeconds': 60,
460-
'tables': [
461-
{
462-
"format": 'csv',
463-
"url": '/api/demo-stream/earthquakes?timeframe=hour',
464-
"sample": '''id,time,latitude,longitude,depth_km,magnitude,place,type,status,felt,cdi,mmi,tsunami,sig,net,code,url,fetched_at
465-
us7000abc1,2024-01-15T10:25:00Z,36.234,-117.456,5.2,2.5,"10km N of Ridgecrest, CA",earthquake,reviewed,,,0,0,125,us,abc1,https://earthquake.usgs.gov/earthquakes/eventpage/us7000abc1,2024-01-15T10:30:00Z
466-
us7000abc2,2024-01-15T10:15:00Z,61.123,-150.345,45.8,3.1,"50km S of Anchorage, AK",earthquake,automatic,5,,0,0,180,us,abc2,https://earthquake.usgs.gov/earthquakes/eventpage/us7000abc2,2024-01-15T10:30:00Z'''
467-
}
468-
]
469-
},
470-
{
471-
'source': 'Open-Meteo',
472-
'name': 'Weather (Live)',
473-
'description': 'Current weather conditions for major US cities. Updates every 5 minutes.',
474-
'live': True,
475-
'refreshIntervalSeconds': 300,
476-
'tables': [
477-
{
478-
"format": 'csv',
479-
"url": '/api/demo-stream/weather',
480-
"sample": '''city,latitude,longitude,temperature_c,humidity_percent,wind_speed_kmh,precipitation_mm,fetched_at
481-
Seattle,47.6062,-122.3321,8.5,72,12.5,0.2,2024-01-15T10:30:00Z
482-
New York,40.7128,-74.006,2.3,65,18.2,0.0,2024-01-15T10:30:00Z
483-
Los Angeles,34.0522,-118.2437,15.8,55,8.3,0.0,2024-01-15T10:30:00Z'''
484-
}
485-
]
486-
},
487-
{
488-
'source': 'yfinance',
489-
'name': 'Stock Prices (Live)',
490-
'description': 'Stock prices with 6 months of daily history + recent 15-minute intraday data. Updates every 5 minutes during market hours.',
491-
'live': True,
492-
'refreshIntervalSeconds': 300,
493-
'tables': [
494-
{
495-
"format": 'csv',
496-
"url": '/api/demo-stream/yfinance?symbols=AAPL,MSFT,GOOGL,NVDA',
497-
"sample": '''symbol,timestamp,date,open,high,low,close,volume,data_type,fetched_at
498-
AAPL,2024-01-15 14:30:00,2024-01-15,185.50,186.20,185.30,185.95,1234567,intraday,2024-01-15T19:35:00Z
499-
MSFT,2024-01-15 14:30:00,2024-01-15,388.75,389.50,388.25,389.10,987654,intraday,2024-01-15T19:35:00Z
500-
GOOGL,2024-01-15 14:30:00,2024-01-15,141.20,141.80,141.00,141.55,654321,intraday,2024-01-15T19:35:00Z
501-
NVDA,2024-01-15 14:30:00,2024-01-15,545.20,548.80,544.50,547.35,2345678,intraday,2024-01-15T19:35:00Z'''
502-
}
503-
]
504-
}
505417
]
506418

507419

src/app/dfSlice.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,55 @@ let getUnrefedDerivedTableIds = (state: DataFormulatorState) => {
182182
let tableWithDescendants = state.tables.filter(table => state.tables.some(t => t.derive?.trigger.tableId == table.id)).map(t => t.id);
183183

184184
return state.tables.filter(table => table.derive && !tableWithDescendants.includes(table.id) && !chartRefedTables.includes(table.id)).map(t => t.id);
185+
}
186+
187+
// Helper function to auto-populate latitude/longitude encodings for map charts
188+
let autoPopulateMapEncodings = (chart: Chart, table: DictTable | undefined, conceptShelfItems: FieldItem[]) => {
189+
if (!table) return;
190+
191+
// Patterns to match latitude/longitude column names
192+
const latPatterns = ['latitude', 'lat'];
193+
const lonPatterns = ['longitude', 'lon', 'lng', 'long'];
194+
195+
// Find latitude column (exact match first, then partial match)
196+
let latColumn = table.names.find(name =>
197+
latPatterns.some(p => name.toLowerCase() === p)
198+
);
199+
if (!latColumn) {
200+
latColumn = table.names.find(name =>
201+
latPatterns.some(p => name.toLowerCase().includes(p))
202+
);
203+
}
204+
205+
// Find longitude column (exact match first, then partial match)
206+
let lonColumn = table.names.find(name =>
207+
lonPatterns.some(p => name.toLowerCase() === p)
208+
);
209+
if (!lonColumn) {
210+
lonColumn = table.names.find(name =>
211+
lonPatterns.some(p => name.toLowerCase().includes(p))
212+
);
213+
}
214+
215+
// Auto-populate latitude encoding if found and not already set
216+
if (latColumn && chart.encodingMap.latitude?.fieldID == undefined) {
217+
const latField = conceptShelfItems.find(f =>
218+
f.name === latColumn && table.names.includes(f.name)
219+
);
220+
if (latField) {
221+
chart.encodingMap.latitude = { fieldID: latField.id };
222+
}
223+
}
224+
225+
// Auto-populate longitude encoding if found and not already set
226+
if (lonColumn && chart.encodingMap.longitude?.fieldID == undefined) {
227+
const lonField = conceptShelfItems.find(f =>
228+
f.name === lonColumn && table.names.includes(f.name)
229+
);
230+
if (lonField) {
231+
chart.encodingMap.longitude = { fieldID: lonField.id };
232+
}
233+
}
185234
}
186235

187236
let deleteChartsRoutine = (state: DataFormulatorState, chartIds: string[]) => {
@@ -603,6 +652,13 @@ export const dataFormulatorSlice = createSlice({
603652
let chartType = action.payload.chartType;
604653
let tableId = action.payload.tableId || state.tables[0].id;
605654
let freshChart = generateFreshChart(tableId, chartType, "user") as Chart;
655+
656+
// Auto-populate latitude/longitude for map charts
657+
if (chartType.toLowerCase().includes('map')) {
658+
let table = state.tables.find(t => t.id === tableId);
659+
autoPopulateMapEncodings(freshChart, table, state.conceptShelfItems);
660+
}
661+
606662
state.charts = [ freshChart , ...state.charts];
607663
state.focusedTableId = tableId;
608664
state.focusedChartId = freshChart.id;
@@ -652,6 +708,14 @@ export const dataFormulatorSlice = createSlice({
652708
let chart = dfSelectors.getAllCharts(state).find(c => c.id == chartId);
653709
if (chart) {
654710
chart = adaptChart(chart, getChartTemplate(chartType) as ChartTemplate);
711+
712+
// Auto-populate latitude/longitude for map charts
713+
if (chartType.toLowerCase().includes('map')) {
714+
let allCharts = dfSelectors.getAllCharts(state);
715+
let table = getDataTable(chart, state.tables, allCharts, state.conceptShelfItems);
716+
autoPopulateMapEncodings(chart, table, state.conceptShelfItems);
717+
}
718+
655719
dfSelectors.replaceChart(state, chart);
656720
}
657721
},

src/app/utils.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,17 @@ export const assembleVegaChart = (
688688
if (temporalKeys.length > 0) {
689689
values = values.map((r: any) => {
690690
for (let temporalKey of temporalKeys) {
691-
r[temporalKey] = String(r[temporalKey]);
691+
const val = r[temporalKey];
692+
// Convert numeric timestamps to ISO date strings for Vega-Lite compatibility
693+
if (typeof val === 'number') {
694+
// Detect if timestamp is in seconds (10 digits) or milliseconds (13 digits)
695+
const timestamp = val < 1e12 ? val * 1000 : val;
696+
r[temporalKey] = new Date(timestamp).toISOString();
697+
} else if (val instanceof Date) {
698+
r[temporalKey] = val.toISOString();
699+
} else {
700+
r[temporalKey] = String(val);
701+
}
692702
}
693703
return r;
694704
})
4.51 KB
Loading

src/components/ChartTemplates.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import chartIconCustomArea from '../assets/chart-icon-custom-area-min.png';
2828
import chartIconPie from '../assets/chart-icon-pie-min.png';
2929
import chartIconUSMap from '../assets/chart-icon-us-map-min.png';
3030
import chartIconPyramid from '../assets/chart-icon-pyramid-min.png';
31+
import chartIconWorldMap from '../assets/chart-icon-world-map-min.png';
3132

3233
// Chart Icon Component using static imports
3334
const ChartIcon: React.FC<{ src: string; alt?: string }> = ({ src, alt = "" }) => {
@@ -385,7 +386,7 @@ const barCharts: ChartTemplate[] = [
385386

386387
const mapCharts: ChartTemplate[] = [
387388
{
388-
"chart": "US Map with Points",
389+
"chart": "US Map",
389390
"icon": <ChartIcon src={chartIconUSMap} />,
390391
"template": {
391392
"width": 500,
@@ -429,6 +430,52 @@ const mapCharts: ChartTemplate[] = [
429430
"color": ["layer", 1, "encoding", "color"],
430431
"size": ["layer", 1, "encoding", "size"]
431432
}
433+
},
434+
{
435+
"chart": "World Map",
436+
"icon": <ChartIcon src={chartIconWorldMap} />,
437+
"template": {
438+
"width": 600,
439+
"height": 350,
440+
"layer": [
441+
{
442+
"data": {
443+
"url": "https://vega.github.io/vega-lite/data/world-110m.json",
444+
"format": {
445+
"type": "topojson",
446+
"feature": "countries"
447+
}
448+
},
449+
"projection": {
450+
"type": "equalEarth"
451+
},
452+
"mark": {
453+
"type": "geoshape",
454+
"fill": "lightgray",
455+
"stroke": "white"
456+
}
457+
},
458+
{
459+
"projection": {
460+
"type": "equalEarth"
461+
},
462+
"mark": "circle",
463+
"encoding": {
464+
"longitude": { },
465+
"latitude": { },
466+
"size": {},
467+
"color": {}
468+
}
469+
}
470+
]
471+
},
472+
"channels": ["longitude", "latitude", "color", "size"],
473+
"paths": {
474+
"longitude": ["layer", 1, "encoding", "longitude"],
475+
"latitude": ["layer", 1, "encoding", "latitude"],
476+
"color": ["layer", 1, "encoding", "color"],
477+
"size": ["layer", 1, "encoding", "size"]
478+
}
432479
}
433480
]
434481

0 commit comments

Comments
 (0)