Perspective.js NYC Citibike Example
An example of joining multiple real-time datasets from the NYC Citibike Open Data feed.
by texodus
HTML
<script src="https://unpkg.com/@jpmorganchase/perspective/build/perspective.js"></script>
<script src="https://unpkg.com/@jpmorganchase/perspective-viewer/build/perspective.view.js"></script>
<script src="https://unpkg.com/@jpmorganchase/perspective-viewer-hypergrid/build/hypergrid.plugin.js"></script>
<script src="https://unpkg.com/@jpmorganchase/perspective-viewer-highcharts/build/highcharts.plugin.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@jpmorganchase/perspective-viewer/build/material.css">
<div id="grid">
<perspective-viewer
row-pivots='["name"]'
columns='["num_bikes_available"]'
sort='[["num_bikes_available","desc"]]'>
</perspective-viewer>
<perspective-viewer
view='xy_scatter'
row-pivots='["name"]'
columns='["lon","lat","num_bikes_available"]'
sort='[["num_bikes_available","asc"]]'>
</perspective-viewer>
</div>
CSS
perspective-viewer {
display: block;
height: 50vw;
width: 50vw;
flex: 1;
border: 1px solid #ccc;
max-width: 600px;
max-height: 600px;
}
#grid {
display: flex;
flex-direction: row;
max-width: 1200px;
margin: auto;
}
#grid perspective-viewer {
margin-left: 3px;
}
@media (max-width: 800px) {
#grid {
flex-direction:column;
width: 50%;
}
#grid perspective-viewer {
min-width: 400px;
min-height: 400px;
margin-left: 0px;
margin-bottom: 3px;
}
}
JavaScript
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);
});
}
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;
}
}
const worker = perspective.worker();
async function get_schema(feed) {
const table = worker.table(feed);
const schema = await table.schema();
table.delete();
return schema;
}
async function merge_schemas(feeds) {
const schemas = await Promise.all(feeds.map(get_schema));
return Object.assign({}, ...schemas);
}
async function main() {
const feednames = ["station_status", "station_information"];
const feeds = await Promise.all(feednames.map(get_feed));
const schema = await merge_schemas(feeds);
const table = worker.table(schema, {index: "station_id"});
for (let feed of feeds) {
table.update(feed);
}
get_feed("station_status", table.update);
const viewers = document.getElementsByTagName("perspective-viewer");
for (viewer of viewers) {
viewer.load(table);
}
}
main();