Demo of Perspective.
A real-time map of NYC Citi Bike stations colored by the number of bikes available at each updating once per second.
license: apache-2.0 |
Demo of Perspective.
A real-time map of NYC Citi Bike stations colored by the number of bikes available at each updating once per second.
// Quick wrapper function for making a GET call. | |
function get(url) { | |
return new Promise((resolve) => { | |
const xhr = new XMLHttpRequest(); | |
xhr.open("GET", url, true); | |
xhr.responseType = "json"; | |
xhr.onload = () => resolve(xhr.response); | |
xhr.send(null); | |
}); | |
} | |
// Fetch feed data from NYC Citibike, if a callback is provided do it again every 1s asynchronously. | |
async function get_feed(feedname, callback) { | |
const url = `https://gbfs.citibikenyc.com/gbfs/en/${feedname}.json`; | |
const { | |
data: {stations}, | |
ttl, | |
} = await get(url); | |
if (typeof callback === "function") { | |
callback(stations); | |
setTimeout(() => get_feed(feedname, callback), ttl * 1000); | |
} else { | |
return stations; | |
} | |
} | |
// Create a new Perspective WebWorker instance. | |
const worker = perspective.worker(); | |
// Use Perspective WebWorker's table to infer the feed's schema. | |
async function get_schema(feed) { | |
const table = await worker.table(feed); | |
const schema = await table.schema(); | |
table.delete(); | |
return schema; | |
} | |
// Create a superset of the schemas defined by the feeds. | |
async function merge_schemas(feeds) { | |
const schemas = await Promise.all(feeds.map(get_schema)); | |
return Object.assign({}, ...schemas); | |
} | |
async function get_layout() { | |
const req = await fetch("layout.json"); | |
const json = await req.json(); | |
return json; | |
} | |
async function main() { | |
const feednames = ["station_status", "station_information"]; | |
const feeds = await Promise.all(feednames.map(get_feed)); | |
const schema = await merge_schemas(feeds); | |
// Creating a table by joining feeds with an index | |
const table = await worker.table(schema, {index: "station_id"}); | |
// Load the `table` in the `<perspective-viewer>` DOM reference with the initial `feeds`. | |
for (let feed of feeds) { | |
table.update(feed); | |
} | |
// Start a recurring asyn call to `get_feed` and update the `table` with the response. | |
get_feed("station_status", table.update); | |
window.workspace.tables.set("citibike", Promise.resolve(table)); | |
const layout = await get_layout(); | |
window.workspace.restore(layout); | |
} | |
main(); |
#grid { | |
display: flex; | |
max-width: 600px; | |
max-height: 1200px; | |
margin: auto; | |
flex-direction: column; | |
} | |
#grid perspective-viewer { | |
height: 600px; | |
width: 600px; | |
flex: 1; | |
display: block; | |
} |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"/> | |
<link rel="stylesheet" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@finos/perspective-workspace/dist/css/material.css" /> | |
<link rel="stylesheet" href="index.css" /> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective-workspace@latest"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-datagrid@latest"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-d3fc@latest"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-openlayers/dist/umd/perspective-viewer-openlayers.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective@latest"></script> | |
<script src="citibike.js"></script> | |
<style> | |
body { | |
display: flex; | |
flex-direction: column; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
} | |
</style> | |
</head> | |
<body> | |
<perspective-workspace id="workspace"></perspective-workspace> | |
</body> | |
</html> |
{ | |
"sizes": [ | |
1 | |
], | |
"detail": { | |
"main": { | |
"type": "split-area", | |
"orientation": "horizontal", | |
"children": [ | |
{ | |
"type": "tab-area", | |
"widgets": [ | |
"PERSPECTIVE_GENERATED_ID_0" | |
], | |
"currentIndex": 0 | |
}, | |
{ | |
"type": "tab-area", | |
"widgets": [ | |
"PERSPECTIVE_GENERATED_ID_1" | |
], | |
"currentIndex": 0 | |
} | |
], | |
"sizes": [ | |
0.6, | |
0.4 | |
] | |
} | |
}, | |
"mode": "globalFilters", | |
"viewers": { | |
"PERSPECTIVE_GENERATED_ID_0": { | |
"plugin": "X/Y Scatter", | |
"columns": [ | |
"lon", | |
"lat", | |
"num_bikes_available" | |
], | |
"sort": [ | |
[ | |
"num_bikes_available", | |
"asc" | |
] | |
], | |
"plugin_config": { | |
"realValues": [ | |
"lon", | |
"lat", | |
"num_bikes_available" | |
] | |
}, | |
"master": false, | |
"table": "citibike", | |
"linked": false, | |
"name": "Map" | |
}, | |
"PERSPECTIVE_GENERATED_ID_1": { | |
"plugin": "datagrid", | |
"columns": [ | |
"capacity", | |
"num_bikes_available", | |
"name" | |
], | |
"sort": [["last_reported", "desc"]], | |
"master": false, | |
"table": "citibike", | |
"linked": false, | |
"name": "Recently updated" | |
} | |
} | |
} |