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();