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" | |
| } | |
| } | |
| } |