Public
Edited
Jun 10, 2023
1 star
Also listed in…
Intro to Vega-Lite
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
nbaLocations = FileAttachment("nba_team_locations@4.csv")
.csv({ typed: true })
.then((data) => data.map((d) => ({ ...d, Year: new Date(d['Opening Year'], 0) }))) // this correctly parses the dates
Insert cell
printTable(nbaLocations.slice(0, 5))
Insert cell
Insert cell
printTableTypes(nbaLocations)
Insert cell
Insert cell
{
// Plot long/lat as x,y on a scatterplot
const pointLocations = vl.markPoint()
.data(nbaLocations)
.encode(
vl.x().fieldQ('Long').scale({domain: [-125, -60]}),
vl.y().fieldQ('Lat').scale({domain: [25, 48]}),
vl.color().fieldN('Region') // color code by region
);

// Add in text labels and set the text alignment left with a slight offset
// See: https://vega.github.io/vega-lite/docs/text.html#properties
const textLocations = vl.markText({align: 'left', dx: 5, dy: -2})
.data(nbaLocations)
.encode(
vl.x().fieldQ('Long'),
vl.y().fieldQ('Lat'),
vl.text().fieldN('Team')
);

return vl.layer(pointLocations, textLocations)
.width(800).height(500)
.render();
}
Insert cell
Insert cell
{
// Now using Vega-Lite's geographic position channels
const pointLocations = vl.markCircle()
.encode(
vl.longitude().fieldQ('Long'), // longitude encoding
vl.latitude().fieldQ('Lat'), // latitude encoding
vl.color().fieldN('Region')
);

const textLocations = vl.markText({align: 'left', dx: 5, dy: -2})
.encode(
vl.longitude().fieldQ('Long'), // longitude encoding
vl.latitude().fieldQ('Lat'), // latitude encoding
vl.text().fieldN('Team')
);

return vl.layer(pointLocations, textLocations)
.data(nbaLocations)
.width(800).height(500)
.render();
}
Insert cell
Insert cell
{
// Plot circles for team locations
const pointLocations = vl.markCircle()
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region').legend({offset: 100})
);

// Add text labels
const textLocations = vl.markText({align: 'left', dx: 5, dy: -2})
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.text().fieldN('Team')
);

return vl.layer(pointLocations, textLocations)
.data(nbaLocations)
.project(vl.projection('albersUsa')) // add projection
.width(800).height(500)
.render();
}
Insert cell
Insert cell
{
// Create an adjusted
for(let nbaLoc of nbaLocations){
nbaLoc.AdjustedLat = nbaLoc.Lat;
if(nbaLoc.Team.includes("Clippers")){
nbaLoc.AdjustedLat += -0.6;
}else if(nbaLoc.Team.includes("Nets")){
nbaLoc.AdjustedLat += -0.3;
}else if(nbaLoc.Team.includes("Knicks")){
nbaLoc.AdjustedLat += 0.2;
}else if(nbaLoc.Team.includes("Bucks")){
nbaLoc.AdjustedLat += 0.2;
}else if(nbaLoc.Team.includes("Pistons")){
nbaLoc.AdjustedLat -= 0.1;
}else if(nbaLoc.Team.includes("Spurs")){
nbaLoc.AdjustedLat -= 0.4;
}else if(nbaLoc.Team.includes("Thunder")){
nbaLoc.AdjustedLat += 0.2;
}
}
return nbaLocations; // an optional return (just makes it easier to see data)
}
Insert cell
Insert cell
{
// Plot circles for team locations
const pointLocations = vl.markCircle()
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region').legend({offset: 100})
);

// Add text labels
const textLocations = vl.markText({align: 'left', dx: 5, dy: -2})
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'),
vl.text().fieldN('Team')
);

return vl.layer(pointLocations, textLocations)
.data(nbaLocations)
.project(vl.projection('albersUsa')) // add projection
.width(800).height(500)
.render();
}
Insert cell
Insert cell
Insert cell
usBoundaries = FileAttachment("us-10m.json").json()
Insert cell
Insert cell
Insert cell
Insert cell
usBoundaries
Insert cell
Insert cell
Insert cell
Insert cell
vl.markGeoshape()
.data(vl.topojson(usBoundaries).feature('states'))
.render()
Insert cell
Insert cell
vl.markGeoshape()
.data(vl.topojson(usBoundaries).feature('states'))
.project(vl.projection('albersUsa')) // add projection
.render()
Insert cell
Insert cell
vl.markGeoshape({fill: '#E0E0E0', stroke: '#F8F8F8', strokewidth: 1})
.data(vl.topojson(usBoundaries).feature('states'))
.project(vl.projection('albersUsa'))
.width(800).height(500)
.render()
Insert cell
Insert cell
vl.markGeoshape({fill: '#E0E0E0', stroke: '#F8F8F8', strokewidth: 1})
.data(vl.topojson(usBoundaries).feature('counties')) // change to counties
.project(vl.projection('albersUsa'))
.width(800).height(500)
.render()
Insert cell
Insert cell
Insert cell
vl.markGeoshape({fill: '#E0E0E0', stroke: '#F8F8F8', strokewidth: 1})
.data(vl.topojson(usBoundaries).feature(selectTopoFeatures)) // grab the selected topo feature from interactive input
.project(vl.projection('albersUsa'))
.width(800).height(500)
.render()
Insert cell
Insert cell
Insert cell
{
// Create the us map, which we'll draw as the initial layer
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

// Create the circle marks for the lat, long of team locations
const pointLocations = vl.markCircle({size: 40}) // make point locations slightly larger
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region').legend({offset: 30}),
);

// Create the text name of the teams
const textLocations = vl.markText({align: 'left', dx: 6, dy: -2.5})
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'),
vl.text().fieldN('Team'),
)

return vl.layer(usMap, pointLocations, textLocations)
.data(nbaLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render()
}
Insert cell
Insert cell
{
// Add in the us map, which we'll draw as the initial layer
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

// Create the circle marks for the lat, long of team locations
const pointLocations = vl.markCircle({size: 90}) // make point locations slightly larger
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
.scale({scheme: 'category10'}) // set color scheme
.legend({offset: 30}),
);

// Create the text name of the teams
const textLocations = vl.markText({align: 'left', dx: 7, dy: -2})
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'),
vl.text().fieldN('Team'),
)

return vl.layer(usMap, pointLocations, textLocations)
.data(nbaLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render()
}
Insert cell
Insert cell
{
// Create the us map, which we'll draw as the initial layer
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

// Create the circle marks for the lat, long of team locations
const pointLocations = vl.markCircle()
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
.scale({scheme: 'category10'}).legend({offset: 50}),
vl.size().fieldQ('City Population') // Set size proportional to city population
);

// Create the text name of the teams
const textLocations = vl.markText({align: 'left', dx: 6, dy: -2})
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'), // used adjusted lat to avoid overlap for some names
vl.text().fieldN('Team'),
)

return vl.layer(usMap, pointLocations, textLocations)
.data(nbaLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render()
}
Insert cell
Insert cell
nbaColorsAndLogos = FileAttachment("nba_color_codes@1.csv").csv({typed: true})
Insert cell
Insert cell
printTable(nbaColorsAndLogos.slice(0,5))
Insert cell
Insert cell
{
// Create the us map, which we'll draw as the initial layer
const colorScheme = 'category10'; // color scheme from https://vega.github.io/vega/docs/schemes/
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

// Add the locations of NBA teams
const logoLocations = vl.markImage({width: 20, height: 20, align: 'center'})
.transform(
// The logo url is stored in a different dataset. Use vl.lookup to combine the nbaLocations
// and nbaColorsAndLogos data
vl.lookup('Team').from(vl.data(nbaColorsAndLogos).key('Team').fields(['LogoSvgUrl']))
).encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'), // use the adjusted lats to avoid overlap
vl.url().fieldN('LogoSvgUrl')
);

// Add text labels
const textLocations = vl.markText({align: 'left', dx: 12, dy: -1})
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'),
vl.text().fieldN('Team'),
vl.color().fieldN('Region').scale({scheme: colorScheme}) // color code by region
);

return vl.layer(usMap, logoLocations, textLocations)
.data(nbaLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render();
}
Insert cell
Insert cell
Insert cell
{
const regionSelector = vl.selectPoint()
.fields('Region')
.on('mouseover').nearest(true)
.clear('mouseout') // clear on mouseout, see: https://vega.github.io/vega/docs/event-streams/
.bind('legend'); // also support clicking on the legend
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

const pointLocations = vl.markCircle({size: 90})
.params(regionSelector)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
.scale({scheme: 'category10'}) // set color scheme
.legend({offset: 50}),
vl.size().fieldQ('City Population'),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1)
);

const textLocations = vl.markText({align: 'left', dx: 6, dy: -2})
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'),
vl.text().fieldN('Team'),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1)
)

return vl.layer(usMap, pointLocations, textLocations)
.data(nbaLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render()
}
Insert cell
Insert cell
nbaTeamConnections = {
// Create a hashmap of regions to teams
const mapRegionsToTeams = new Map();
const mapTeamNameToTeam = new Map();
for(let nbaTeam of nbaLocations){
if(!mapRegionsToTeams.has(nbaTeam.Region)){
mapRegionsToTeams.set(nbaTeam.Region, []);
}
mapRegionsToTeams.get(nbaTeam.Region).push(nbaTeam.Team);
mapTeamNameToTeam.set(nbaTeam.Team, nbaTeam);
}

// Create a data table of nba teams and their connections to other teams in region
let nbaTeamConnections = [];
for(const originNbaTeam of nbaLocations){
//console.log(nbaTeam);
for(const destNbaTeamName of mapRegionsToTeams.get(originNbaTeam.Region)){
if(destNbaTeamName !== originNbaTeam.Team){
const destNbaTeam = mapTeamNameToTeam.get(destNbaTeamName);
nbaTeamConnections.push(
{
OriginTeam : originNbaTeam.Team,
Region : originNbaTeam.Region,
Conference : originNbaTeam.Conference,
DestinationTeam : destNbaTeamName,
}
);
}
}
}
//console.log(nbaTeamConnections);
return nbaTeamConnections;
}
Insert cell
Insert cell
printTable(nbaTeamConnections.slice(0, 7));
Insert cell
Insert cell
{
const regionSelector = vl.selectPoint()
.fields('Region')
.on('mouseover').nearest(true)
.clear('mouseout') // clear on mouseout
.bind('legend'); // also support clicking on the legend
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

// Location of teams
const pointLocations = vl.markCircle({size: 90})
.data(nbaLocations)
.params(regionSelector)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
.scale({scheme: 'category10'}) // set color scheme
.legend({offset: 50}),
vl.size().fieldQ('City Population'),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1)
);

// shared data reference for lookup transforms
const foreign = vl.data(nbaLocations).key('Team').fields('Lat', 'Long');

// add connections between current team and all other teams in region
const routes = vl.markRule({color: '#000', opacity: 0.35})
.data(nbaTeamConnections)
.transform(
vl.filter(regionSelector.empty(false)), // filter to selected origin only
vl.lookup('OriginTeam').from(foreign), // origin lat/lon
vl.lookup('DestinationTeam').from(foreign).as('Lat2', 'Long2') // dest lat/lon
)
.encode(
vl.latitude().fieldQ('Lat'),
vl.longitude().fieldQ('Long'),
vl.latitude2().field('Lat2'),
vl.longitude2().field('Long2'),
vl.color().fieldN('Region'),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1)
);

const textLocations = vl.markText({align: 'left', dx: 6, dy: -2})
.data(nbaLocations)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'),
vl.text().fieldN('Team'),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1)
//vl.color().fieldN('Region')
)

return vl.layer(usMap, routes, pointLocations, textLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render()
}
Insert cell
Insert cell
{
const regionSelector = vl.selectPoint()
.fields('Region')
.on('mouseover').nearest(true)
.clear('mouseout') // clear on mouseout
.bind('legend'); // also support clicking on the legend

const teamSelector = vl.selectPoint()
.fields('Team')
.on('mouseover').nearest(true)
.clear('mouseout'); // clear on mouseout
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

const pointLocations = vl.markCircle({size: 90})
.data(nbaLocations)
.params(regionSelector, teamSelector)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
.scale({scheme: 'category10'}) // set color scheme
.legend({offset: 50}),
vl.size().fieldQ('City Population'),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1)
);

// shared data reference for lookup transforms
const foreign = vl.data(nbaLocations).key('Team').fields('Lat', 'Long');

// add connections between current team and all other teams in region
const routes = vl.markRule({color: '#000', opacity: 0.35})
.data(nbaTeamConnections)
.transform(
vl.calculate("datum.OriginTeam").as("Team"), // necessary for the filter, which filters on 'Team'
vl.filter(teamSelector.empty(false)), // filter to selected city
vl.lookup('OriginTeam').from(foreign), // this team's lat/lon
vl.lookup('DestinationTeam').from(foreign).as('Lat2', 'Long2') // connected team lat/lon
)
.encode(
vl.latitude().fieldQ('Lat'),
vl.longitude().fieldQ('Long'),
vl.latitude2().field('Lat2'),
vl.longitude2().field('Long2'),
vl.color().fieldN('Region'),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1)
);

const textLocations = vl.markText({align: 'left', dx: 6, dy: -2})
.data(nbaLocations)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'),
vl.text().fieldN('Team'),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1),
vl.size().if(teamSelector.empty(false), vl.value(14)).value(11)
//vl.color().fieldN('Region')
);

return vl.layer(usMap, routes, pointLocations, textLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render()
}
Insert cell
Insert cell
Insert cell
demoMap = {
const regionSelector = vl.selectPoint()
.fields('Region')
.on('mouseover').nearest(true)
.clear('mouseout') // clear on mouseout,
.bind('legend'); // also support clicking on the legend

const teamSelector = vl.selectPoint()
.fields('Team')
.on('mouseover').nearest(true)
.clear('mouseout'); // clear on mouseout
const colorScheme = 'category10'; // color scheme from https://vega.github.io/vega/docs/schemes/
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

// shared data reference for lookup transforms
const foreign = vl.data(nbaLocations).key('Team').fields('Lat', 'Long');
// add connections between current team and all other teams in region
const routes = vl.markRule({color: '#000', opacity: 0.35})
.data(nbaTeamConnections)
.transform(
vl.calculate("datum.OriginTeam").as("Team"), // necessary for the filter, which filters on 'Team'
vl.filter(teamSelector.empty(false)), // filter to selected city
vl.lookup('OriginTeam').from(foreign), // this team's lat/lon
vl.lookup('DestinationTeam').from(foreign).as('Lat2', 'Long2') // connected team lat/lon
)
.encode(
vl.latitude().fieldQ('Lat'),
vl.longitude().fieldQ('Long'),
vl.latitude2().field('Lat2'),
vl.longitude2().field('Long2'),
vl.color().fieldN('Region'),
vl.opacity().if(regionSelector, vl.value(0.5)).value(0.1)
);

const logoLocations = vl.markImage({width: 30, height: 30, align: 'center'})
.data(nbaLocations)
.transform(
vl.lookup('Team').from(vl.data(nbaColorsAndLogos).key('Team').fields(['LogoSvgUrl']))
).encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.url().fieldN('LogoSvgUrl'),
vl.opacity().if(regionSelector, vl.value(1)).value(0.1)
);

const textLocations = vl.markText({align: 'left', dx: 12, dy: -1, size: 13})
.data(nbaLocations)
.params(regionSelector, teamSelector)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('AdjustedLat'),
vl.text().fieldN('Team'),
vl.color().fieldN('Region').scale({scheme: colorScheme}),
vl.opacity().if(regionSelector, vl.value(0.7)).value(0.1),
vl.size().if(teamSelector.empty(false), vl.value(14)).value(11)
);

return vl.layer(usMap, routes, logoLocations, textLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
}
Insert cell
Insert cell
Insert cell
Insert cell
// Converting regions for region expression
nbaRegionsOld = {
let mapRegion = new Map();
for(const team of nbaLocations){
mapRegion.set(team.Region, team.Region);
}
mapRegion.set(null, "No Team");
return Array.from(mapRegion, ([key, value]) => ({ [key]: value }));
}
Insert cell
nbaRegionStr = {
let nbaRegionStr = JSON.stringify(nbaRegionsOld)
nbaRegionStr = nbaRegionStr.substring(1, nbaRegionStr.length - 1);
nbaRegionStr += "[datum.Region]";
console.log(nbaRegionStr);
return nbaRegionStr;
}
Insert cell
d3Voronoi = require('d3-geo-voronoi')
Insert cell
nbaLocations
Insert cell
nbaLongLatLocs = nbaLocations.map(nbaLoc => {
return [nbaLoc.Long, nbaLoc.Lat];
});
Insert cell
voronoi2 = d3Voronoi.geoVoronoi(nbaLongLatLocs)
Insert cell
nbaVoronoi = voronoi2.polygons()
Insert cell
voronoi = d3Voronoi.geoVoronoi(nbaLocations)
Insert cell
voronoi.polygons(nbaLocations)
Insert cell
usStatesTopojson = fetch(
"https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"
).then(d => d.json())
Insert cell
usStatesTopojson.objects.states.geometries[0].properties
Insert cell
nbaLocations
Insert cell
nbaVoronoi
Insert cell
{
// Use states topojson from https://github.com/topojson/us-atlas
// Unlike the Vega-Lite states data, this JSON has state names, which lets us filter out Alaska and Hawaii
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson("https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json").feature('states'))
.transform(
vl.filter("datum.properties.name !== 'Alaska' && datum.properties.name !== 'Hawaii'")
);
const voronoiPlot = vl.markGeoshape({fill: null, stroke: "gray"})
.data(vl.json(nbaVoronoi).property("features"));
const teamLocPlot = vl.markCircle()
.data(nbaLocations)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
);
return vl.layer(usMap, teamLocPlot, voronoiPlot)
.project(vl.projection('mercator').scale(860).center([-97, 38.5])) // add projection
.width(900).height(500)
.render();
}
Insert cell
{
let nbaVoronoiCopy = JSON.parse(JSON.stringify(nbaVoronoi));
let removedHawaii = nbaVoronoiCopy.features.splice(9, 1);
let removedAlaska = nbaVoronoiCopy.features.splice(22, 1);
// Use states topojson from https://github.com/topojson/us-atlas
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson("https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json").feature('states'))
.transform(
vl.filter("datum.properties.name !== 'Alaska' && datum.properties.name !== 'Hawaii'")
);
const voronoiPlot = vl.markGeoshape({fill: null, stroke: "gray"})
.data(vl.json(nbaVoronoiCopy).property("features"));
//.transform(
//vl.filter("datum.properties.site[0] > -150")
//vl.filter("length(datum.properties.neighbours) <= 8")
//)
const teamLocPlot = vl.markCircle()
.data(nbaLocations)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
);
return vl.layer(usMap, teamLocPlot, voronoiPlot)
.project(vl.projection('albersUsa')) // add projection
.width(900).height(500)
.render();
}
Insert cell
vl.markCircle()
.data(nbaLocations)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
)
.project(vl.projection('albersUsa')) // add projection
.width(900).height(500)
.render()
Insert cell
{
// Add in the us map, which we'll draw as the initial layer
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));

// Create the circle marks for the lat, long of team locations
const pointLocations = vl.markCircle({size: 40}) // make point locations slightly larger
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region').legend({offset: 30}),
);

// Create the text name of the teams
const textLocations = vl.markText({align: 'left', dx: 7, dy: -2.5})
.transform(
// Added latitude jitter for overlapping labels where teams share a similar location
// ideally, we'd do this in pixel space rather than lat/long space but I couldn't
// figure out how to dynamically set the dy offset
vl.calculate("indexof(datum.Team, 'Clippers') > 0 || indexof(datum.Team, 'Nets') > 0\
? datum.Lat - (datum.Lat * 0.013) : datum.Lat").as('Lat2')
).encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat2'),
//vl.latitude().if("indexof(datum.Team, 'Clippers') > 0", vl.value(50)).value(50),
vl.text().fieldN('Team'),
)

return vl.layer(usMap, pointLocations, textLocations)
.data(nbaLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render()
}
Insert cell
vl.markLine()
.data(nbaLocations)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region')
)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.render()
Insert cell
{
const dotLocations = vl.markCircle()
.data(nbaLocations)
.encode(
vl.x().fieldQ('Long').scale({domain: [-125, -60]}),
vl.y().fieldQ('Lat').scale({domain: [25, 48]}),
vl.color().fieldN('Region')
);

const logoLocations = vl.markImage({width: 20, height: 20, align: 'left', xOffset: 5, yOffset: -3})
.data(nbaLocations)
.transform(
vl.lookup('Team').from(vl.data(nbaColorsAndLogos).key('Team').fields(['LogoSvgUrl']))
).encode(
vl.x().fieldQ('Long'),
vl.y().fieldQ('Lat'),
vl.url().fieldN('LogoSvgUrl')
);

const textLocations = vl.markText({align: 'left', dx: 26, dy: -2})
.data(nbaLocations)
.encode(
vl.x().fieldQ('Long'),
vl.y().fieldQ('Lat'),
vl.text().fieldN('Team')
);

return vl.layer(dotLocations, textLocations, logoLocations)
.width(900).height(500)
.render();
}
Insert cell
{
const usMap = vl.markGeoshape({fill: 'rgb(225, 225, 225)', stroke: 'rgb(240, 240, 240)', strokeWidth: 1})
.data(vl.topojson(usBoundaries).feature('states'));
const dotLocations = vl.markPoint({size: 450})
.data(nbaLocations)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.color().fieldN('Region').legend({offset: 100})
);

const logoLocations = vl.markImage({width: 20, height: 20, align: 'center', xOffset: 25})
.data(nbaLocations)
.transform(
vl.lookup('Team').from(vl.data(nbaColorsAndLogos).key('Team').fields(['LogoSvgUrl']))
).encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.url().fieldN('LogoSvgUrl')
);

const textLocations = vl.markText({align: 'left', dx: 12, dy: -2})
.data(nbaLocations)
.encode(
vl.longitude().fieldQ('Long'),
vl.latitude().fieldQ('Lat'),
vl.text().fieldN('Team'),
vl.color().fieldN('Region')
);

return vl.layer(usMap, textLocations, logoLocations, dotLocations)
.project(vl.projection('albersUsa'))
.width(900).height(500)
.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
// https://github.com/dcousens/haversine-distance
haversine = require('https://bundle.run/haversine-distance@1.2.1')
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