Published unlisted
Edited
Insert cell
Insert cell
Added in parent
md`One extra thing we can do is find the number of unique bikes were ridden for the selected day. The work for this can primarily be done via the ArcGIS Server where the CitrixCycle data reside, but it requires building a query to the REST service specifically to return the unique ride ID values logged on the selected date. Unfortunately there is no field linking each point to a specific ride, but we can at least get some sense of fleet utilization.`
Insert cell
Added in parent
count = {
  let from = moment(date).valueOf();
  let to = moment(date).add(1, 'days').valueOf();
  let url = `${ccRidesUrl}/query?where=1%3D1&=&outFields=objectid%2Cid&returnGeometry=false&groupByFieldsForStatistics=id&outStatistics=%5B%7B%0D%0A"statisticType"%3A"count"%2C%0D%0A"onStatisticField"%3A"id"%2C%0D%0A"outStatisticFieldName"%3A"count"%0D%0A%7D%5D&f=pjson&time=${from}%2C${to}`
  let result = (await fetch(url)).json()
  return result
}
Insert cell
Added in parent
md`### CitrixCycle Open Docks`
Insert cell
Added in parent
md`While there are some some datasets either through Google Maps or ArcGIS Online showing where CitrixCycle docks are or will be located, they don't appear to be linked directly to the maps on the CitrixCycle website or in the app. After some digging, I found the an API endpoint for the CitrixCycle docks data.`
Insert cell
Added in parent
ccDocksUrl = 'https://citrixcycle.com/stations/stations/'
Insert cell
Added in parent
md`I had some issues fetching the JSON feed directly. There appears to be some sort of CORS issue. Regardless, I found a [CORS bypasser](https://glitch.com/~cors-bypasser) on Glitch to help me out. I remixed it so that it is now my own personal proxy!`
Insert cell
Added in parent
ccDocksJSON = (await fetch(`https://cors-bypasser.glitch.me/bypass/${ccDocksUrl}`)).json()
Insert cell
Added in parent
md`The data returned from the CitrixCycle API are returned as JSON, but not the GeoJSON that Leaflet prefers. However, with the help of turf.js it is relatively simple to parse the JSON and transform it into GeoJSON.`
Insert cell
Added in parent
ccDocksGeoJSON = {
  let featuresArray = [];
  ccDocksJSON.forEach((x) => {
    if (x.location && x.type == "OPEN") {
      let geometry = x.location.reverse()
      let properties = {
        "id": x.id,
        "locking_station_type": x.locking_station_type,
        "description": x.description,
        "address": x.address,
        "stocking_full": x.stocking_full
      }
      let feature = turf.point(geometry, properties)
      featuresArray.push(feature)
    }
  })
                 
  return turf.featureCollection(featuresArray)
}
Insert cell
Added in parent
md`With the CitrixCycle docks data transformed into GeoJSON, we can create a layer represent the locations. While there are lots of ways to draw points, in this case we'll show circle markers sized based on the carrying capacity of the docking station.`
Insert cell
Added in parent
ccDocksLayer = L.geoJSON(ccDocksGeoJSON, {
  pointToLayer: (feature, latlng) => {
    return L.circleMarker(latlng, {
      radius: feature.properties.stocking_full / 2,
      fillColor: '#EF9A9A',
      fillOpacity: 0.5,
      color: '#E57373',
      opacity: 1,
      weight: 1.5
    })
  }
})
Insert cell
Added in parent
md`### Existing Bike Facilities`
Insert cell
Added in parent
bikeFacilitesLayer =  L.esri.featureLayer({
  url: 'https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Existing_Bike_Facilities/FeatureServer/7',
  style: (feature) => {
    return {
      color: '#ffffff',
      dashArray: "2, 5",
      weight: 1.5
    }
  }  
})
Insert cell
Added in parent
md`### Greenway Trails`
Insert cell
Added in parent
ralGreenwaysLayer = L.esri.featureLayer({
  url: 'https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Greenway_Trails_All/FeatureServer/0',
  where: "STATUS='Existing'",
  style: feature => {
    return {
      color: '#FFB300',
      weight: 1,
      dashArray: "2, 5"
    }
  }
})
Insert cell
Insert cell
Added in parent
md`### Load Leaflet and plugins
While loading libraries using Observable is generally straightforward, extending Leaflet with plugins was less clear to me. See [this thread](https://talk.observablehq.com/t/extending-leaflet-with-plugins/2006?u=maptastik) for some explanation on how and why to do it the way below
`
Insert cell
L = {
const r = require.alias({leaflet: 'leaflet@1.4.0'});
const L = await r('leaflet');
if (!L._css) {
document.head.appendChild(L._css = html`
<link href='${await require.resolve('leaflet@1.4.0/dist/leaflet.css')}' rel=stylesheet>
`);
}
L.esri = await r('esri-leaflet');
L.esri.Heat = await r('esri-leaflet-heatmap');
await r('leaflet.heat').catch(() => L.heatLayer);
return L;
}
Insert cell
Removed in parent
L.esri
Insert cell
Removed in parent
L.heatLayer
Insert cell
Removed in parent
L.esri.Heat
Insert cell
Removed in parent
moment = require('moment')
Insert cell
Added in parent
html`<style>
  .metric {
    text-align:center
  }
  .metric-value {
    font-size:3rem;
  }
</style>
<div class="metric"text-align:center>
  <div><span class="metric-value">${count.features.length}</span><br>Bikes Used for Rides</div>
</div> `
Insert cell
Changed in parent
-
md`## Layers`
+
md`## Data`
Insert cell
Changed in parent
-
md`### CitrixCycle Heat Layer`
+
md`### Load Data`
Insert cell
Added in parent
md`The CitrixCycle rides data are accessible via a REST endpoint on City of Raleigh's ArcGIS Server.`
Insert cell
Changed in parent
-
ccRidesUrl = 'https://maps.raleighnc.gov/arcgis/rest/services/Hosted/CitrixCycleHistory/MapServer/0'
+
url = 'https://maps.raleighnc.gov/arcgis/rest/services/Hosted/CitrixCycleHistory/MapServer/0'
Insert cell
Changed in parent
-
md`We want to visualize the ridership for a given date selected via the <code>Select Date</code> input. To help this, we create an empty [Leaflet FeatureGroup](https://leafletjs.com/reference-1.5.0.html#featuregroup) into which we can remove the previously selected day's ridership data and add the newly selected day's ridership data.`
+
md`An empty feature group to hold our buffer result`
Insert cell
Changed in parent
-
ccRidesHeatFeatureGroup = L.featureGroup()
+
heatFG = L.featureGroup()
Insert cell
Added in parent
md`In April 2019 Raleigh launched a municipal bike share program called [CitrixCycle](https://citrixcycle.com/). Sponsored by Citrix, this bike share program allows riders to retrieve and deposit special CitrixCycle bikes at docking stations throughout the city.
![](https://maptastik.neocities.org/CitrixCycle_dock.jpg)
Access is granted using a phone app or a fob. During each ride, GPS locations of the rider are collected at regular intervals. A feed of this data is made publicly available by City of Raleigh GIS. This notebook is meant to offer a means for exploration for daily trends in ridership for the CitrixCycle program.`
Insert cell
Insert cell
Removed in parent
md`**Select Date**`
Insert cell
Changed in parent
-
viewof date = { const div = html` <b>Select Date</b><br> <input id="date-selection" type="date" min="2019-04-16" max="2019-05-24">` const d = div.querySelector("[type=date]"); div.value = d.value = "2019-04-16" d.onload = () => div.value = d.value d.oninput = () => div.value = d.value return div }
+
viewof date = html`<input id="date-selection" type="date" min="2019-04-16" max=${moment().format("YYYY-MM-DD")} value="2019-04-16">`
Insert cell
Added in parent
viewof heatColor = color({
  value:"#18FFFF",
  title: "Heat Color"
})
Insert cell
Changed in parent
viewof map = {
-
let container = DOM.element('div', { style: `width:${width}px;height:${width/3}px`});
+
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.5}px`});
yield container; const map = container.value = L.map(container).setView([35.777801,-78.642712],15); let base = L.esri.basemapLayer('DarkGray').addTo(map)
-
ccRidesHeatFeatureGroup.addTo(map)
-
let overlayLayers = { "CitrixCycle Heat Layer": ccRidesHeatFeatureGroup, "CitrixCycle Open Docks": ccDocksLayer, "Existing Bike Facilities": bikeFacilitesLayer, "Raleigh Greenway Trails": ralGreenwaysLayer } L.control.layers({}, overlayLayers).addTo(map)
+
heatFG.addTo(map)
}
Insert cell
Insert cell
Removed in parent
md`**Point Radius**: ${pointRadiusSlider}`
Insert cell
Changed in parent
-
viewof pointRadiusSlider = { const div = html` <b>Point Radius:</b><br> <input type=range min=1 max=25 step=1> <input type=number min=1 max=25 step=1 style="width:auto"> `; const range = div.querySelector("[type=range]"); const number = div.querySelector("[type=number]"); div.value = range.value = number.value = 5; range.oninput = () => number.value = div.value = range.valueAsNumber; number.oninput = () => range.value = div.value = number.valueAsNumber; return div }
+
viewof pointRadiusSlider = html`<input type=range min="1" max="25" step="1" value="5">`
Insert cell
Removed in parent
md`**Blur:** ${blurRadiusSlider}`
Insert cell
Changed in parent
-
viewof blurRadiusSlider = { const div = html` <b>Blur: </b><br> <input type=range min=0 max=25 step=1> <input type=number min=0 max=25 style="width:auto"> `; const range = div.querySelector("[type=range]"); const number = div.querySelector("[type=number]"); div.value = range.value = number.value = 15; range.oninput = () => number.value = div.value = range.valueAsNumber; number.oninput = () => range.value = div.value = number.valueAsNumber; return div }
+
viewof blurRadiusSlider = html`<input type=range min="1" max="25" step="1" value="15">`
Insert cell
Removed in parent
md`**Opacity:** ${opacitySlider}`
Insert cell
Removed in parent
viewof opacitySlider = html`<input type=range min="0" max="1" step="0.01" value="0.8">`
Insert cell
Added in parent
md`Here we setup the logic for updating the CitrixCycle ridership data when a parameter changes. Note, there are several parameters that could trigger this block to run:
- <code>url</code>: A new date is selected
- <code>radius</code>: The radius for the heat layer is changed
- <code>blur</code>: The blur of the heat layer is changed
- <code>gradient['0.5']</code>: The middle value for the heat gradient is changed.

When one of these parameters changes, the previous instance of <code>ccRidesHeatFeature</code> is removed from the map by running <code>ccRidesHeatFeatureGroup.clearLayers()</code>. Then we use the special esri-leaflet layer type, <code>L.esri.Heat.featureLayer()</code> to create an updated instance of the heat representation of the newly parametrized layer. Finally, that new layer is added to the map via <code>ccRidesHeatFeatureGroup.addLayer(ccRidesHeatFeature)</code>`
Insert cell
Changed in parent
-
ccRidesHeatLayer = { ccRidesHeatFeatureGroup.clearLayers(); let ccRidesHeatFeature = L.esri.Heat.featureLayer({ url: ccRidesUrl, fields: ['objectid', 'received'],
+
heatLayer = { heatFG.clearLayers(); let heatFeature = L.esri.Heat.featureLayer({ url: url, max: opacitySlider,
radius: pointRadiusSlider, blur: blurRadiusSlider, gradient: { '0': 'Black',
-
'0.5': heatColor,
+
'0.5': '#18FFFF',
'1': 'White' }, timeField: 'received', from: moment(date), to: moment(date).add(1, 'days')
-
});
+
}).addTo(map);
-
return ccRidesHeatFeatureGroup.addLayer(ccRidesHeatFeature);
+
return heatFG.addLayer(heatFeature);
}
Insert cell
Added in parent
md`### moment.js`
Insert cell
Added in parent
moment = require('moment')
Insert cell
Added in parent
md`### turf`
Insert cell
Added in parent
turf = require('https://npmcdn.com/@turf/turf/turf.min.js')
Insert cell
Added in parent
md`### Observable color input`
Insert cell
Added in parent
import {color} from "@jashkenas/inputs"
Insert cell