Public
Edited
Nov 4, 2022
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
L = require('leaflet@1.2.0')
Insert cell
<link href='${resolve('leaflet@1.2.0/dist/leaflet.css')}' rel='stylesheet' />
Insert cell
Insert cell
FileAttachment("geojson.png").image()
Insert cell
Insert cell
Insert cell
{
const selection = vl.selectPoint();
return vl.markGeoshape({stroke: '#aaa', strokeWidth: 0.25})
.data(vl.topojson(usa).feature('counties'))
.transform(
vl.lookup('id').from(vl.data(unemp).key('id').fields('rate'))
)
.params(selection)
.encode(
vl.color().if(selection, vl.color().fieldQ('rate').scale({domain: [0, 0.3], clamp: true}).legend({format: '%'})).value('grey'),
vl.tooltip().fieldQ('rate').format('.0%')
)
.project(vl.projection('albersUsa'))
.width(600).height(500)
.config({view: {stroke: null}})
.render()
}
Insert cell
import {usa, unemp} from "@uwdata/cartographic-visualization"
Insert cell
Insert cell
Insert cell
//Our function is in map

map = {
// You'll often see Leaflet examples initializing a map like L.map('map'),
// which tells the library to look for a div with the id 'map' on the page.
// In Observable, we instead create a div from scratch in this cell called 'container', so it's
// completely self-contained.
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
// This component utilizes "yield", which pauses the execution of this code block
// Yield returns the value of container back to the notebook, which allows the div to be placed on the page.
//This is important, because Leaflet uses the div's .offsetWidth and .offsetHeight (to get current size of the div) and size the map. If I were to only return the container at the end of this method, Leaflet might get the wrong idea about the map's size.
yield container;
// Now we create a map object and add a layer to it.
let map = L.map(container).setView([49.2527, -123.1207], 10.5); // initializes the map, sets zoom & coordinates

//This adds the tile layer to the map. The map dynamically loads tiles based on current view the experience smooth and seamless. Make sure to add attribution to your map.
let baseLayer = L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
})
.addTo(map);

}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
grandCentral = {
let response = await fetch('https://nominatim.openstreetmap.org/search.php?q=grand+central+station%2C+new+york&polygon_geojson=1&format=json');
let json = await response.json();
return json[0].geojson
}
Insert cell
grandCentralMap = {
// You'll often see Leaflet examples initializing a map like L.map('map'),
// which tells the library to look for a div with the id 'map' on the page.
// In Observable, we instead create a div from scratch in this cell, so it's
// completely self-contained.

//Here, I changed the width and height of the map since I got tired of accidentally scrolling in and out while navigating through the page
let container = DOM.element('div', { style: `width:${600}px;height:${width/2}px` });
// Note that I'm yielding the container pretty early here: this allows the
// div to be placed on the page. This is important, because Leaflet uses
// the div's .offsetWidth and .offsetHeight to size the map. If I were
// to only return the container at the end of this method, Leaflet might
// get the wrong idea about the map's size.
yield container;
// Now we create a map object
//NOTE: WE DO NOT SET THE VIEW BECAUSE WE WAIT TO GET THE BOUNDS OF THE LAYER WITH OUR POLYGON
let map = L.map(container);

//Basemap layer
let baseLayer = L.tileLayer(cartoLight, {
attribution:cartoAttr,
subdomains: 'abcd',
maxZoom: 20,
minZoom: 10
}).addTo(map);

//--------------------*** ADDING A LAYER ***--------------------------
//instantiates a new geoJson layer using built in geoJson handling
//We can set the attributes of the polygons (like CSS), including line weight and colors
let grandCentralLayer = L.geoJson(grandCentral,
{
weight: 5,
color: '#432'
})
.addTo(map);

//finds bounds of polygon and automatically gets map view to fit (useful for interaction and not having to 'cook' the map zoom and coordinates as in map instantiation
grandCentralLayer.bindPopup('Grand Central Terminal'); //tooltip
map.fitBounds(grandCentralLayer.getBounds());
}
Insert cell
Insert cell
Insert cell
VanAreas = FileAttachment("VancouverAreaSize.json").json()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
VanAreasMap = {

//here, we constrain the map to a certain size
let width = 600
let height = 350
let container = DOM.element('div', { style: `width:${width}px;height:${width/2}px` });

yield container;
// Did not use setView because we are using "fit bounds" to get the polygons to determine this
//Also, I fixed the zoom level for this map since we don't get more info by zooming in for this vis
let map = L.map(container, {maxZoom: 11, minZoom: 11});
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr
}).addTo(map);

//----------- ADDING A NEW LAYER -------
//TO-DO: let's call our new layer VanAreasLayer

//finds bounds of polygon and automatically gets map view to fit (useful for interaction and not having to 'cook' the map zoom and coordinates as in map instantiation
// map.fitBounds(VanAreasLayer.getBounds());

}
Insert cell
Insert cell
vanOneWays
Insert cell
vanStreets
Insert cell
//I defined a helper function that fetches me the types of street uses listed in the JSON

//we are using some js shorthand magic here, by first mapping only the streetuses to an array
//then we use [...new Set(ourArray)] to get uniques
streetUses = [...new Set(vanOneWays.features.map(function(d){return d.properties.streetuse}))]
Insert cell
//Simple function for getting street usage color: if it's Arterial, use red, or else blue
function getStreetUsageColor(d) {
if (d == 'Arterial')
return 'red';
else if (d == 'Residential')
return 'blue';
else
return 'black'
}
Insert cell
//Street stylizing: similar to the density map, I made a function that returns red for artery streets, blue for residential streets, and black for other categories



streetStyle = function(d){
return {
weight: 2,
color: getStreetUsageColor(d.properties.streetuse),
opacity: .9
}}
Insert cell
Insert cell
Insert cell
Insert cell
vanOneWays
Insert cell
Insert cell
vanFoodGardens
Insert cell
VanMarkerMap = {

let width = 600;
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;
// Did not use setView because we are using "fit bounds" to get the polygons to determine this
let map = L.map(container);

//add base layer
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr,
subdomains: 'abcd',
maxZoom: 20,
minZoom: 10
}).addTo(map);

//tooltip to define our content that we want to return
let tooltip = function (Layer) { return Layer.feature.properties.name + '<br> Address: ' + Layer.feature.properties.merged_address + '<br> Neighborhood: ' + Layer.feature.properties.geo_local_area; }

//As a reminder: here's an alternative way to write the same function above with arrow notation
// let tooltip = (Layer) => { Layer.feature.properties.name + '<br> Address: ' + Layer.feature.properties.merged_address + '<br> Neighborhood: ' + Layer.feature.properties.geo_local_area; }

//****** TRY UNCOMMENTING THE CODE IN THIS SECTION to define a custom green icon *******************
// var myIcon = new L.Icon({
// iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
// shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
// iconSize: [25, 41],
// iconAnchor: [12, 41],
// //popupAnchor: [1, -34],
// //shadowSize: [41, 41]
// });

// let customMarker = L.marker([49.28, -123.12], {icon: myIcon}).addTo(map)
// .bindPopup('A pretty CSS3 popup added to the map.<br> Easily customizable.')
// .openPopup();

//*****************************************************************************************
//Adds the layer to the map & bind tooltip
let VanAreasLayer = L.geoJson(vanFoodGardens)
.bindPopup(tooltip)
.addTo(map);

// //finds bounds of polygon and automatically gets map view to fit (useful for interaction and not having to 'cook' the map zoom and coordinates as in map instantiation
map.fitBounds(VanAreasLayer.getBounds());
}
Insert cell
Insert cell
VanCircleFoodMap = {

let width = 600;
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;
// Did not use setView because we are using "fit bounds" to get the polygons to determine this
let map = L.map(container);

//add base layer
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr,
}).addTo(map);

//geoMarker obj
let geojsonMarkerOptions = {
radius: 5,
fillColor: "#ff7800", //orange
color: "#ff7800",
weight: 1,
opacity: 1,
fillOpacity: 0.6
};


//passing a pointToLayer function in a GeoJSON options object when creating the GeoJSON layer
//This function is passed a LatLng and should return an instance of ILayer, in this case likely a Marker or CircleMarker.
let VanAreasLayer = L.geoJSON(vanFoodGardens, {
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, geojsonMarkerOptions);
}})

//bind popup and add layer to map
.bindPopup(function (Layer) {
return Layer.feature.properties.name + '<br> Address: ' +
Layer.feature.properties.merged_address + '<br> Neighborhood: ' +
Layer.feature.properties.geo_local_area;
})
.addTo(map);


//finds bounds of polygon and automatically gets map view to fit
map.fitBounds(VanAreasLayer.getBounds());
}

Insert cell
Insert cell
Insert cell
Insert cell
vanCrime
Insert cell
crimePoints
Insert cell
FilteredGeoCrimes
Insert cell
Insert cell
Insert cell
Insert cell
vanCrimeMap = {

let width = 600;
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;

//Use setview when making heatmaps, because heatLayer doesn't have a getBounds() method
let map = L.map(container).setView([49.2527, -123.1207], 10.5);

//add base layer
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr,
}).addTo(map);

let heatStyle = {
radius: 10,
blur: 15,
maxZoom: 10,
max: 4.0
}

//our heatmap layer
let crimeLayer = heatLayer(crimePoints, heatStyle).addTo(map);

//Of course, we can always add more layers on top, such the neighborhood boundaries
//TO-DO: LET'S TRY THAT NOW.
// let VanAreasLayer = L.geoJson(VanAreas, {style: neighbourhoodStyle})
// .bindPopup(d => d.feature.properties.NAME + '<br> Pop: ' + d.feature.properties.population)
// .addTo(map);


}

Insert cell
Insert cell
VanTwoLayers = {

let width = 600;
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;
// Did not use setView because we are using "fit bounds" to get the polygons to determine this
let map = L.map(container);

//add base layer
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr,
subdomains: 'abcd',
maxZoom: 20,
minZoom: 0
}).addTo(map);

//geoMarker obj
let geojsonMarkerOptions = {
radius: 5,
fillColor: "#ff7800",
color: "#ff7800",
weight: 1,
opacity: 1,
fillOpacity: 0.6
};


//THIS IS THE NEIGHBOURHOODS LAYER.
let VanAreasLayer = L.geoJson(VanAreas, {style: neighbourhoodStyle})
.bindPopup(d => d.feature.properties.NAME + '<br> Pop: ' + d.feature.properties.population)
.addTo(map);
//THIS IS THE POINTS LAYER, TRY MOVING IT SO THAT IT IS BOUND BEFORE VanPointsLayer
let VanPointsLayer = L.geoJSON(vanFoodGardens, {
pointToLayer: (feature, latlng) => {
return L.circleMarker(latlng, geojsonMarkerOptions);
}})
.bindPopup(function (Layer) {
return Layer.feature.properties.name + '<br> Address: ' +
Layer.feature.properties.merged_address + '<br> Neighborhood: ' +
Layer.feature.properties.geo_local_area;
})
.addTo(map);



//finds bounds of polygon and automatically gets map view to fit
map.fitBounds(VanAreasLayer.getBounds());
}

Insert cell
Insert cell
Insert cell
This is the CSS defining the legend
<link href='${resolve(
'leaflet@1.2.0/dist/leaflet.css'
)}' rel='stylesheet' /><style>

.legend {
line-height: 18px;
color: #555;
border: 1px solid #000000;
border-radius: 4px;
padding: 8px;
background: rgba(255,255,255,0.8);
}
.legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 8px;
opacity: 0.7;
}</style>
Insert cell
popMap2 = {
let width = 600
let height = 600
let container = DOM.element('div', { style: `width:${width}px;height:${width/2}px` });
yield container

//base layer
let map = L.map(container);
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr,
minZoom: 11,
maxZoom: 11,
}).addTo(map);

//add van areas
let VanAreasLayer = L.geoJson(vanAreas, {style: style})
.bindTooltip(function (Layer) {
return Layer.feature.properties.NAME;
}).addTo(map);


//DEFINING OUR LEGEND
var legend = L.control({position: 'bottomright'});

legend.onAdd = function (map) {
var div = L.DomUtil.create('div', 'info legend'),
grades = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7],
labels = [];

// loop through our density intervals and generate a label with a colored square for each interval
for (var i = 0; i < grades.length; i++) {
div.innerHTML +=
'<i style ="background-color:' + getDensColor(grades[i]) + '"></i> ' +
grades[i] + (grades[i + 1] ? '&ndash;' + grades[i + 1] + '<br>' : '+');
}
return div;
};

legend.addTo(map);
map.fitBounds(VanAreasLayer.getBounds());
}
Insert cell
Insert cell
Insert cell
Insert cell
{return "Neighbourhood selected: " + (nHood == null ? 'none' : nHood)}
Insert cell
<div style="
background: #fff;
display: grid;
height: ${screen.height / screen.width * 100}vw;
grid-template-areas:
'a'
'b';
grid-gap: 10px;
">
<div name="a" style="grid-area:a;border:solid 1px #ccc;text-align: center">${viewof crimeChart}</div>
<div name="b" style="grid-area:b;border:solid 1px #ccc;text-align: center">${viewof crimeMap}</div>
</div>`
Insert cell
brush = vl.selectInterval().encodings('x').resolve('global');
Insert cell
click = vl.selectPoint().encodings('color');
Insert cell
//OUR HEATMAP SHOWING CRIMES IN VANCOUVER
viewof crimeMap = {

let width = 900;
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;

//Use setview when making heatmaps, because heatLayer doesn't have a getBounds() method
let map = L.map(container).setView([49.2527, -123.1207], 10.5);

//add base layer
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr,
subdomains: 'abcd',
maxZoom: 12,
minZoom: 12
}).addTo(map);

//heatmap Style
let heatStyle = {
radius: 10,
blur: 15,
// maxZoom: 12,
// minZoom: 12,
max: 4.0,
scaleRadius: true
}


//layer for our neighborhoods
//*********THIS IS VERY IMPORTANT: onEachFeature ADDS A LISTENER TO THE LAYER with a callback function called featureEvents**********************
let VanAreasLayer = L.geoJson(vanAreas, {style: neighbourhoodStyle, onEachFeature: featureEvents})
.bindPopup(d => d.feature.properties.NAME)
.addTo(map);
//our heatmap layer
let crimeLayer = heatLayer(crimePoints, heatStyle).addTo(map);

//another mouse listener that resets the state of nHood when we click on a blank part of the map
map.on('click', () =>
mutable nHood = null
);

map.fitBounds(VanAreasLayer.getBounds());
}
Insert cell
Insert cell
//OUR CONCATENATED BAR CHART / LINE CHART THAT DOES BRUSHING AND LINKING
//Interactions are defined very similarly to the brushing & linking we did in Tutorial 6, except now we use the filter function to combine the selection returned internally by our bar/line brush, and the global selection of neighbourhoods from our map (mapSelection)

viewof crimeChart = {
//our filter function is telling us: we will use the 'and' operator while filtering, to only show the selection returned by 1) `brush`, and 2) our predicate of mapSelection (which returns neighbourhood names selected from the map) that are in the field NEIGHBOURHOOD
const crimeBar = vl.markBar()
.data(vanCrime2)
.transform(vl.filter({'and':
[brush, {'field': 'NEIGHBOURHOOD', 'oneOf': mapSelection}]}))
.select(click)
.encode(
vl.x().count(),
vl.y().fieldN('Offense'),
vl.color().value('lightgray').if(click, vl.color().fieldN('Offense Category'))
)
.width(200)
.height(200)

//Interactions are defined very similarly to the brushing & linking we did in Tutorial 6, except now we use the filter function
//our filter function is telling us: we will use the 'and' operator while filtering, to only show the selection returned by 1) `brclickush`, and 2) our predicate of mapSelection (which returns neighbourhood names selected from the map) that are in the field NEIGHBOURHOOD
const crimeLine = vl.markLine()
.data(vanCrime2)
.transform(vl.filter({'and':
[click, {'field': 'NEIGHBOURHOOD', 'oneOf': mapSelection}]}))
.encode(
vl.x().fieldT('Date').timeUnit('utcdatemonth'),
vl.y().count(),
vl.color().fieldN('Offense Category')
)
.params(brush)
.width(400)
.height(200)

//concat the bars together
return vl.hconcat(crimeBar, crimeLine).render()

}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{return "Neighbourhood selected: " + (nHood == null ? 'none' : nHood)}
Insert cell
mapSelection
Insert cell
vl.markCircle({size: 30})
.data(crimeDensityTotal)
.transform(
vl.filter({'and':
[{'field': 'NEIGHBOURHOOD', 'oneOf': mapSelection}]})
)
.encode(
vl.x().fieldN('Offense').count().axis('Number of crimes').scale({type: 'log'}),
vl.y().fieldQ('sqm').axis({title: 'Square meters/person'}).scale({'domain': [0, 500]}),
vl.column().fieldN('Offense Type'),
vl.color().fieldN('Offense Category')
)
.width(250)
.title(selectedNeighbourhoods)
.render()

Insert cell
Insert cell
viewof popMap = {
let width = 400
let height = 400
let container = DOM.element('div', { style: `width:${width}px;height:${width/2}px` });
yield container
let map = L.map(container);
//add base layer
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr,
maxZoom: 11,
minZoom: 11
}).addTo(map);

//popStyle uses a fillColor based on the value from feature.properties.population in the GeoJSON file
let VanAreasLayer = L.geoJson(vanAreas, {style: popStyle})
.bindTooltip(function (Layer) {
return Layer.feature.properties.NAME;
}).addTo(map)
map.fitBounds(VanAreasLayer.getBounds());
}
Insert cell
viewof densityMap = {
let width = 400
let height = 400
let container = DOM.element('div', { style: `width:${width}px;height:${width/2}px` });
yield container
let map = L.map(container);
//add base layer
let baseLayer = L.tileLayer(cartoLight, {
attribution: cartoAttr,
maxZoom: 11,
minZoom: 11
}).addTo(map);
//style uses a fillColor based on the value from feature.properties.density in the GeoJSON file
let VanAreasLayer = L.geoJson(vanAreas, {style: style})
.bindTooltip(function (Layer) {
return Layer.feature.properties.NAME;
}).addTo(map)
map.fitBounds(VanAreasLayer.getBounds());
}
Insert cell
Insert cell
//Try creating a map here

Insert cell
Insert cell
//Try creating a map here

Insert cell
Insert cell
//Try adding the legend to popMap and densityMap

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
//We are writing the event listener function for each feature in our geojson layer
//when each of these events are fired, a callback function is triggered
//more interaction (such as hover) is covered here: https://leafletjs.com/examples/choropleth/

function featureEvents(feature, layer) {
layer.on({
click: whenClicked, //callback functions
});
}
Insert cell
//We update our data in the function using mutable. This overrides observables defaults so we can push data from within //function scope globally
whenClicked = (event) => {
mutable nHood = event.target.feature.properties.NAME
}
Insert cell
//Mutable overrides Observable's defaults state, so we can push data from within function scope globally
mutable nHood = null
Insert cell
mapSelection = {
if(nHood == null ){
return Array.from(new Set(vanCrime2.map(d => d.NEIGHBOURHOOD))) //Set is a new datastructure in ES6 -> only unique values
} else {
return [nHood]
}
}
Insert cell
selectedNeighbourhoods = selectedNames(mapSelection);
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
VegaLite = require('vega-lite@2.0.0-beta.2/build/vega-lite.min.js')
//Vegalite = require("@observablehq/vega-lite@0.2")
Insert cell
vegalite = require("@observablehq/vega-lite@0.1")
Insert cell
Insert cell
heatLayer = L, require('leaflet.heat').catch(() => L.heatLayer)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function style(feature) {
return {
fillColor: getDensColor(feature.properties.density),
weight: 2,
opacity: .5,
color: 'white',
dashArray: '3',
fillOpacity: 0.7
};
}
Insert cell
function popStyle(feature) {
return {
fillColor: getPopColor(feature.properties.population),
weight: 2,
opacity: 1,
color: 'white',
dashArray: '3',
fillOpacity: 0.7
};
}
Insert cell
function neighbourhoodStyle(){
return {
fillColor: '#999999',
weight: 2,
opacity: .5,
color: 'white',
dashArray: '3',
fillOpacity: 0.2,
}};
Insert cell
Insert cell
Insert cell
vanFoodGardens = FileAttachment("community-gardens-and-food-trees.geojson").json()
Insert cell
vanAreas = FileAttachment("VanAreas.json").json()
Insert cell
vanAreaValues2 = d3.json("https://www.sfu.ca/~lyn/data/Urban/VancouverAreaSizeTopo.json")
Insert cell
vanStreets = FileAttachment("public-streets.geojson").json()
Insert cell
vanOneWays = FileAttachment("one-way-streets.geojson").json()
Insert cell
Insert cell
vanCrime2 = {
let i=vanCrime.features.length;
let j=0;
let k=0;
let temp ={};
let newCrime=[];
for(k=0;k<i; k++)
{
newCrime[k]= vanCrime.features[k].properties;
}
return (newCrime);
}
Insert cell
Insert cell
Insert cell
crimePoints = FilteredGeoCrimes.features.map(feature =>
feature.geometry.coordinates.slice().reverse().concat([0.1]))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more