Compare commits
10 Commits
f6b4870339
...
3ca2f347b4
Author | SHA1 | Date | |
---|---|---|---|
3ca2f347b4 | |||
ff05e37b59 | |||
b95ebeedf4 | |||
1a7eec7baa | |||
98114b9f7d | |||
418fc87281 | |||
94c69f5d91 | |||
0c48005ce5 | |||
9815181bca | |||
49068a469f |
94
11.animation-interaction/slides.html
Normal file
84
11.animation-interaction/slides.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Animation & Interaction
|
||||
## CAPP 30239
|
||||
|
||||
---
|
||||
|
||||
### How can we use animation and interaction to improve understanding?
|
||||
|
||||
### How can we implement this with the tools we have?
|
||||
|
||||
---
|
||||
|
||||
## Review: Potential Benefits of Interactives
|
||||
|
||||
- Reducing Cognitive Load
|
||||
- Personalization for Reader
|
||||
- Making Information Playful
|
||||
- Prompting Self-Reflection
|
||||
|
||||
(examples in 06.narrative slides)
|
||||
|
||||
---
|
||||
|
||||
## Applications of Animation
|
||||
|
||||
- Demonstrate change over time
|
||||
- Highlight relationships
|
||||
- Focus attention:
|
||||
- Show subsets of data
|
||||
- Draw attention to changes
|
||||
- Show uncertainty
|
||||
|
||||
More Examples:
|
||||
- <https://informationisbeautiful.net>
|
||||
- <https://www.visualcinnamon.com/portfolio/>
|
||||
|
||||
---
|
||||
|
||||
![bg fit](https://clauswilke.com/dataviz/visualizing_uncertainty_files/figure-html/mpg-uncertain-HOP-animated-1.gif)
|
||||
|
||||
---
|
||||
|
||||
## Reasons for Interaction
|
||||
|
||||
- Enable user-driven exploration of data.
|
||||
- Allow personalization (e.g. enter your zip code)
|
||||
- Increased engagement/retention
|
||||
|
||||
---
|
||||
|
||||
- Huge amount of data w/ user exploration: <https://pudding.cool/2024/11/love-songs/>
|
||||
- Personalization: <https://www.nytimes.com/interactive/2024/upshot/buy-rent-calculator.html>
|
||||
|
||||
---
|
||||
|
||||
## Selection
|
||||
|
||||
For user-driven data explorations, **selection** is an important concept.
|
||||
|
||||
How do you want to let a user select individual records or groups of records?
|
||||
|
||||
### Spectrum: From Click to Query Lang
|
||||
|
||||
- click on selection
|
||||
- hover effect
|
||||
- drag/region selection
|
||||
- pre-written query with dropdowns/selects
|
||||
- query lang
|
||||
|
||||
Altair Interactive Charts <https://altair-viz.github.io/user_guide/interactions.html>
|
||||
|
||||
---
|
||||
|
||||
### Example: Voronai Diagram For Selection of Small Points
|
||||
|
||||
- <https://d3js.org/d3-delaunay/voronoi>
|
||||
- <https://www.visualcinnamon.com/2015/07/voronoi/>
|
||||
|
||||
---
|
||||
|
||||
## More Resources
|
||||
|
||||
- <https://vis.berkeley.edu/papers/animated_transitions/>
|
||||
- <https://explorabl.es>
|
||||
- <https://worrydream.com>
|
BIN
12.gis/282eb677-c3c3-4d85-a652-5b1b7eb30c8d.jpg
Normal file
After Width: | Height: | Size: 7.7 MiB |
BIN
12.gis/392c3b25-9766-47f9-b7af-8e1ffab814f9.png
Normal file
After Width: | Height: | Size: 329 KiB |
BIN
12.gis/a3585f84-8cf4-451c-b079-0bb2ac338c6d.jpg
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
12.gis/a4c744e0-7b3b-4a1e-9989-cf67a9b53583.png
Normal file
After Width: | Height: | Size: 275 KiB |
BIN
12.gis/bad-proj.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
12.gis/d63823a9-23a1-4ac3-999c-6a72798236e5.jpg
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
12.gis/f65f091f-54e8-4249-833f-a5ff19bf9dff.JPG
Normal file
After Width: | Height: | Size: 122 KiB |
329
12.gis/slides.html
Normal file
277
12.gis/slides.md
Normal file
@ -0,0 +1,277 @@
|
||||
# GIS Fundamentals
|
||||
## CAPP 30239
|
||||
|
||||
---
|
||||
|
||||
## Today
|
||||
|
||||
Establish a baseline understanding of GIS data.
|
||||
|
||||
(Not focused on Data Viz)
|
||||
|
||||
### Next
|
||||
|
||||
Browser Mapping Libraries
|
||||
|
||||
---
|
||||
|
||||
## GIS Fundamentals
|
||||
|
||||
**GIS** - Geographic Information System - Specialized set of tools for dealing with geospatial data.
|
||||
|
||||
### What's special about GIS?
|
||||
|
||||
![bg fit right](282eb677-c3c3-4d85-a652-5b1b7eb30c8d.jpg)
|
||||
(source: NASA.gov)
|
||||
|
||||
---
|
||||
|
||||
## Quantity of data: coastline paradox
|
||||
|
||||
![bg right fit](a4c744e0-7b3b-4a1e-9989-cf67a9b53583.png)
|
||||
(source: Coastline Paradox on Wikipedia)
|
||||
|
||||
---
|
||||
|
||||
## GIS Data Caveats
|
||||
|
||||
### Resolution
|
||||
|
||||
We have to decide what resolution is important. Even at a fairly low fidelity, a shape is likely to have tens of thousands of points.
|
||||
|
||||
### Projection
|
||||
|
||||
The earth is not flat, but we typically work in (x, y, [altitude]). This requires **projection**.
|
||||
|
||||
---
|
||||
|
||||
**What's wrong with this picture?**
|
||||
|
||||
![bg right](f65f091f-54e8-4249-833f-a5ff19bf9dff.JPG)
|
||||
Mercator Projection, Wikipedia
|
||||
|
||||
---
|
||||
|
||||
## Africa is 14x larger
|
||||
|
||||
![greenland-on-africa.jpg](d63823a9-23a1-4ac3-999c-6a72798236e5.jpg)
|
||||
<https://mortenjonassen.dk/maps/greenland-vs-africa-size-comparison>
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Spatial Data Types
|
||||
|
||||
| | Raster | Vector |
|
||||
|----------------|------------------------------------------------------------|-----------------------------------------------------------------|
|
||||
| **Image Format** | JPEG/PNG | SVG/PDF |
|
||||
| **GIS File Types** | GeoTIFF/NetCDF | Shapefile/GeoJSON/Geopackage/KML |
|
||||
| **Example Data** | Satellite imagery | - Points: cities<br>- Lines: roads<br>- Polygons: land parcels |
|
||||
| **Typical Tool** | Camera | GPS |
|
||||
|
||||
|
||||
---
|
||||
|
||||
| | Raster | Vector |
|
||||
|----------------|------------------------------------------------------------|-----------------------------------------------------------------|
|
||||
| **Pros** | Efficient for continuous surfaces (can sample any point) | Arbitrary precision; Ease of calculation; Network efficiency |
|
||||
| **Cons** | Limited by image resolution (sensor & storage) | Harder to create as specific relationships must be calculated and defined |
|
||||
|
||||
<!--
|
||||
This is not unique to GIS, image files can be raster or vector as well, with nearly the same meaning.
|
||||
|
||||
A raster is a grid or matrix, such as the pixels that make up an image. In GIS terms this is often a satellite image, but any representation of the space where each cell in a grid has properties with a given meaning is a raster.
|
||||
|
||||
A raster can be though of as a series of values `(x, y, val)` where `val` is a color or similar. The `x` and `y` coordinates could be specified in any unit, allowing one "pixel" to represent 1 sq km or 1 sq mm depending on the needs of the program.
|
||||
|
||||
Vector data is instead a series of geometric primitives. They are defined in relationship to one another, allowing groups and other relationships to be estabished. We'll address the specifics more after we compare Vector data to Raster data.
|
||||
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
## GIS Vector Data Types
|
||||
|
||||
- **Point** - `(lat, lon)` or `(lat, lon, altitude)` - example: address, tree, bus stop
|
||||
- **Line** aka **Polyline** - series of ordered, connected points - examples: roads, rivers
|
||||
- **Polygon** - enclosed areas - lakes, land parcels, county
|
||||
|
||||
Container Types:
|
||||
|
||||
- **MultiPoint** - example: fleet location, fire hydrants, GPS pings
|
||||
- **MultiLineString** - example: branching transit line
|
||||
- **MultiPolygon** - example: islands
|
||||
|
||||
This allows us to think about a group of related polygons as one:
|
||||
e.g. does this storm path (Polyline) intersect with Hawaii (MultiPolygon)?
|
||||
|
||||
---
|
||||
|
||||
### Coordinate Reference Systems
|
||||
|
||||
These systems define what the `x` and `y` values actually mean in a given file.
|
||||
|
||||
Two kinds:
|
||||
- **Geographic Coordinate Systems** - attempt to map a lat, lon to every point on Earth
|
||||
- **Projected Coordinate Systems** - attempt to project earth's curved surface onto a plane (as needed for drawing)
|
||||
|
||||
---
|
||||
|
||||
Two kinds, infinite variations.
|
||||
|
||||
**Why are there multiple of each?**
|
||||
|
||||
GCS - regional or local variations in earth's curvature, and different estimates as to exact size of earth.
|
||||
PCS - no perfect mapping of sphere to x,y plane exists, as seen with Mercator projection, accuracy in one area of globe means distortion in others
|
||||
|
||||
**WGS84** is used in GPS and therefore is by far the most common GCS.
|
||||
|
||||
---
|
||||
|
||||
PCS is not so uniform, but most common is the **UTM** is the Transverse Mercator projection, which minimizes distortion within a region via a set of guarantees.
|
||||
|
||||
![bg right](392c3b25-9766-47f9-b7af-8e1ffab814f9.png)
|
||||
|
||||
---
|
||||
|
||||
Localities will often publish in a more local projections:
|
||||
|
||||
SPCS - State Plane Coordinate System, actually 125 zones with their own system across US. They are fine-tuned to fit the shape of each state to maximize accuracy. (Some use transverse Mercator, some use a Lambert conformal conic projection, all with different parameters.)
|
||||
|
||||
![bg right](a3585f84-8cf4-451c-b079-0bb2ac338c6d.jpg)
|
||||
(ce: conservation.ca.gov)
|
||||
|
||||
---
|
||||
|
||||
## Correct Projection is Essential
|
||||
|
||||
In practice, it is **essential** that you know what CRS your data is in and what CRS you are displaying it in.
|
||||
|
||||
Failure leads to incorrect results, heavily distorted visualizations, and real-world trouble.
|
||||
|
||||
![bg left fit](bad-proj.jpg)
|
||||
|
||||
---
|
||||
|
||||
### Features
|
||||
|
||||
Another property of most GIS filetypes is that they have **features**. You can think of features as a database or spreadsheet of attributes associated with a given shape. It might take the form:
|
||||
|
||||
| County Name | Population (2020) | Area (sq mi) | County Seat | Georeference |
|
||||
|-------------|-------------------|--------------|-------------|--------------|
|
||||
| Cook | 5,275,541 | 1,635 | Chicago | {...} |
|
||||
| DuPage | 932,877 | 334 | Wheaton | {...} |
|
||||
| Lake | 714,342 | 448 | Waukegan | {...} |
|
||||
| Will | 690,743 | 837 | Joliet | {...} |
|
||||
| Kane | 516,522 | 524 | Geneva | {...} |
|
||||
|
||||
Georeference column is either the geometry definition itself, or a foreign key (primary ID) for the given datum.
|
||||
|
||||
---
|
||||
|
||||
### Formats
|
||||
|
||||
- **shapefile** - actually a bunch of files that need to live in the same directory
|
||||
- .shp - geometries
|
||||
- .shx - indexes for searching
|
||||
- .dbf - csv-like features
|
||||
- .prj - projection (CRS)
|
||||
- .sbn/.sbx/.shp.xml/.cpg - additional optional files that provide more metadata and features
|
||||
|
||||
- **GeoJSON/TopoJSON**: JSON representation of features and geometries
|
||||
- **KML**: Google XML-based format, mostly out of favor but still commonly found on GIS sites
|
||||
- **Geopackage**: Still uncommon, sqlite based successor to shapefile.
|
||||
|
||||
---
|
||||
|
||||
### Spatial Queries
|
||||
|
||||
These form the common spatial queries, which mostly take the form:
|
||||
|
||||
`operation(geometry1, geometry2, optional_tolerance) -> geometry_result`
|
||||
|
||||
Example operations: `nearest`, `contains`, `adjacent_to`
|
||||
|
||||
Let's convert all of these to GIS terms:
|
||||
|
||||
* Where is the nearest Waffle House?
|
||||
* nearest(my_location, waffle_house_multipoint)
|
||||
* What congressional district am I in?
|
||||
* contains(congressional_district_multipoly, my_location)
|
||||
* What cool attractions are close to my drive?
|
||||
* nearest(my_route, roadside_attractions_multipoint, tolerance=10miles)
|
||||
|
||||
---
|
||||
|
||||
## Spatial Overlays
|
||||
|
||||
* Union: combine two or more geometries into one
|
||||
* Intersection: identify overlap (IL & fresh water)
|
||||
* Difference: Cook County - Chicago
|
||||
|
||||
* What part of the US is either national park *or* state park?
|
||||
* `union(national_parks_mp, state_parks_mp)`
|
||||
* How much of Illinois is fresh water?
|
||||
* `intersection(il_boundaries, fresh_water_mp)`
|
||||
* What parts of Cook County are *not* within Chicago?
|
||||
* `difference(cook_co, chicago)`
|
||||
|
||||
---
|
||||
|
||||
### Buffering & Joins
|
||||
|
||||
Some other common functionality:
|
||||
|
||||
**Buffering** is taking an existing shape and pushing the edge out by some distance. This turns a point into a circle, or a line into a polygon, or expands polygons.
|
||||
|
||||
An example buffering operation:
|
||||
|
||||
`current_location.buffer(1000).contains(waffle_houses)`
|
||||
|
||||
Turn a point into a circle with radius 1km, and see if that circle contains any waffle houses.
|
||||
|
||||
**Geospatial Joins** combine two or more datasets on a condition (like overlaps or contains). This technique is used to discover overlaps between datasets, for example, to find what counties are in which cities:
|
||||
|
||||
`joined_data = [(county, city) for county in counties for city in cities if county.geometry.intersects(city.geometry)]`
|
||||
|
||||
---
|
||||
|
||||
## Geospatial Software
|
||||
|
||||
(skip in lecture)
|
||||
|
||||
**OGC Simple Features** - Defines common interface (types and operations) for GIS.
|
||||
|
||||
Most geospatial software implements some version of this.
|
||||
|
||||
The most common of these is **GEOS** a C++ implementation of the specification that provides the common types and operations. ( Geometry Engine, Open Source )
|
||||
|
||||
A related library is **GDAL** which handles loading file formats:
|
||||
|
||||
https://gdal.org/index.html for list of formats
|
||||
|
||||
### PostGIS
|
||||
|
||||
GIS primitives as first-class types in database. Allows querying directly from database, saving conversion and minimizing memory footprint. (Like most things Postgres, one of the most solid pieces of software ever built.)
|
||||
|
||||
---
|
||||
|
||||
### GeoPandas
|
||||
|
||||
Wraps Shapely and provides a pandas like interface.
|
||||
|
||||
*NOT EFFICIENT* fine for simple use, but quickly baloons to unusably large memory footprints without polygon simplification. (Reducing detail by eliding points.)
|
||||
|
||||
### ArcGIS
|
||||
|
||||
Commercial/proprietary service. Developed by ESRI, the key commercial player in GIS for decades.
|
||||
|
||||
Offers cloud/server/desktop based tools.
|
||||
|
||||
Favored in government, large corporations, full suite is most feature complete offering around. Under increasing competition from OSS competitors which are often more lean, cutting lesser-used features and antiquated formats.
|
||||
|
||||
---
|
||||
|
||||
## Next
|
||||
|
||||
Mapping Frameworks
|
BIN
13.js-mapping/XYZ_Tiles.png
Normal file
After Width: | Height: | Size: 286 KiB |
BIN
13.js-mapping/clustering.png
Normal file
After Width: | Height: | Size: 381 KiB |
191
13.js-mapping/d3.html
Normal file
@ -0,0 +1,191 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
/* style for tooltip as well as the base map container */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border: 1px solid #333333;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="https://unpkg.com/topojson@3"></script>
|
||||
<script>
|
||||
const euMemberStates = {
|
||||
Belgium: 1,
|
||||
France: 1,
|
||||
Germany: 1,
|
||||
Italy: 1,
|
||||
Luxembourg: 1,
|
||||
Netherlands: 1,
|
||||
Denmark: 2,
|
||||
Ireland: 2,
|
||||
Greece: 3,
|
||||
Portugal: 4,
|
||||
Spain: 4,
|
||||
Austria: 5,
|
||||
Finland: 5,
|
||||
Sweden: 5,
|
||||
Cyprus: 6,
|
||||
Czechia: 6,
|
||||
Estonia: 6,
|
||||
Hungary: 6,
|
||||
Latvia: 6,
|
||||
Lithuania: 6,
|
||||
Malta: 6,
|
||||
Poland: 6,
|
||||
Slovakia: 6,
|
||||
Slovenia: 6,
|
||||
Bulgaria: 7,
|
||||
Romania: 7,
|
||||
Croatia: 8,
|
||||
};
|
||||
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
const svg = d3
|
||||
.select("#map")
|
||||
.append("svg")
|
||||
.attr("viewBox", [0, 0, width, height])
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
// d3 tooltip pattern: create a placeholder div which will be
|
||||
// hidden at first & revealed/moved as needed
|
||||
const tooltip = d3
|
||||
.select("body")
|
||||
.append("div")
|
||||
.attr("class", "tooltip")
|
||||
.style("opacity", 0);
|
||||
|
||||
// color function used for choropleth
|
||||
const colorScale = d3
|
||||
.scaleSequential()
|
||||
.domain([8, 1]) // Reversed domain so earlier members are darker
|
||||
.interpolator(d3.interpolateViridis);
|
||||
|
||||
// set up projection centered on Europe
|
||||
const projection = d3
|
||||
// DEMO: change projection
|
||||
// https://d3js.org/d3-geo/conic
|
||||
// https://github.com/d3/d3-geo-projection
|
||||
.geoMercator()
|
||||
//.geoConicEqualArea()
|
||||
.center([15, 54])
|
||||
.scale(700)
|
||||
.translate([width / 2, height / 2]);
|
||||
const path = d3.geoPath().projection(projection);
|
||||
|
||||
let globalData = null;
|
||||
|
||||
// load TopoJSON
|
||||
d3.json(
|
||||
// DEMO change to 110m/10m
|
||||
"https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json",
|
||||
).then((data) => {
|
||||
// using topojson 3rd party library to convert to GeoJSON which D3 understands
|
||||
const geojson = topojson.feature(data, data.objects.countries);
|
||||
|
||||
// entire map render happens after data is loaded, in the then() function
|
||||
const europeCountries = geojson.features.filter(
|
||||
(d) => euMemberStates[d.properties.name] !== undefined,
|
||||
);
|
||||
console.log(europeCountries);
|
||||
|
||||
svg
|
||||
.selectAll("path")
|
||||
.data(europeCountries)
|
||||
.join("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", (d) => {
|
||||
const score = euMemberStates[d.properties.name];
|
||||
return score ? colorScale(score) : "#ccc";
|
||||
})
|
||||
.attr("stroke", "white")
|
||||
.attr("stroke-width", 0.5)
|
||||
.on("mouseover", function (event, d) {
|
||||
tooltip.transition().duration(200).style("opacity", 0.9);
|
||||
tooltip
|
||||
.html(
|
||||
`
|
||||
<strong>${d.properties.name}</strong><br/>
|
||||
Wave: ${euMemberStates[d.properties.name]}
|
||||
`,
|
||||
)
|
||||
.style("left", event.pageX + 10 + "px")
|
||||
.style("top", event.pageY - 28 + "px");
|
||||
})
|
||||
.on("mouseout", function () {
|
||||
tooltip.transition().duration(500).style("opacity", 0);
|
||||
});
|
||||
|
||||
// example of a legend, not related to map topics
|
||||
const legendWidth = 200;
|
||||
const legendHeight = 20;
|
||||
const legendScale = d3
|
||||
.scaleLinear()
|
||||
.domain([1, 8])
|
||||
.range([0, legendWidth]);
|
||||
|
||||
const legendAxis = d3
|
||||
.axisBottom(legendScale)
|
||||
.ticks(8)
|
||||
.tickFormat((d) => d);
|
||||
|
||||
const legend = svg
|
||||
.append("g")
|
||||
.attr(
|
||||
"transform",
|
||||
`translate(${width - legendWidth - 20}, ${height - 50})`,
|
||||
);
|
||||
|
||||
const defs = svg.append("defs");
|
||||
const gradient = defs
|
||||
.append("linearGradient")
|
||||
.attr("id", "legend-gradient")
|
||||
.attr("x1", "0%")
|
||||
.attr("x2", "100%")
|
||||
.attr("y1", "0%")
|
||||
.attr("y2", "0%");
|
||||
|
||||
// Add gradient stops
|
||||
const stops = d3.range(0, 8);
|
||||
stops.forEach((stop, i) => {
|
||||
gradient
|
||||
.append("stop")
|
||||
.attr("offset", `${(i / (stops.length - 1)) * 100}%`)
|
||||
.attr("stop-color", colorScale(stop));
|
||||
});
|
||||
|
||||
legend
|
||||
.append("rect")
|
||||
.attr("width", legendWidth)
|
||||
.attr("height", legendHeight)
|
||||
.style("fill", "url(#legend-gradient)");
|
||||
|
||||
legend
|
||||
.append("g")
|
||||
.attr("transform", `translate(0, ${legendHeight})`)
|
||||
.call(legendAxis);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
97
13.js-mapping/leaflet.html
Normal file
@ -0,0 +1,97 @@
|
||||
<!--
|
||||
See Also:
|
||||
https://leafletjs.com/examples/quick-start/
|
||||
https://leafletjs.com/examples.html
|
||||
-->
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
/>
|
||||
<style>
|
||||
#map-goes-here {
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
#info {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
#controls {
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map-goes-here"></div>
|
||||
<div id="info">placeholder</div>
|
||||
<div id="controls">
|
||||
<button id="zoom-fit">Fit All Points</button>
|
||||
<button id="zoom-us">Fit US</button>
|
||||
</div>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const chicago = [41.8781, -87.6298];
|
||||
const initialZoom = 12;
|
||||
// Leaflet makes a global variable "L" available.
|
||||
// this creates a map in the div above
|
||||
const map = L.map("map-goes-here");
|
||||
map.setView(chicago, initialZoom);
|
||||
|
||||
// OSM base layer
|
||||
// DEMO change of base layer
|
||||
const watercolorURL =
|
||||
"https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg";
|
||||
const osmURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
L.tileLayer(watercolorURL, {
|
||||
maxZoom: 19,
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
|
||||
// points could be loaded from GeoJSON or any other source
|
||||
// usage (below) does not depend on format
|
||||
const points = [
|
||||
{ lat: 41.8826, lng: -87.6233, name: "Millennium Park" },
|
||||
{ lat: 41.8916, lng: -87.6083, name: "Navy Pier" },
|
||||
{ lat: 41.8526, lng: -87.6188, name: "Museum of Science and Industry" },
|
||||
];
|
||||
|
||||
// Add points to the map
|
||||
const markers = [];
|
||||
points.forEach((point) => {
|
||||
const marker = L.marker([point.lat, point.lng]).addTo(map);
|
||||
|
||||
// adds a popup to the marker, can use HTML
|
||||
marker.bindPopup(`<strong>${point.name}</strong>`);
|
||||
|
||||
// add custom events to markers in typical JS fashion
|
||||
marker.on("click", function () {
|
||||
const infoDiv = document.getElementById("info");
|
||||
infoDiv.textContent = `Selected Location: ${point.name}`;
|
||||
});
|
||||
|
||||
markers.push(marker);
|
||||
});
|
||||
|
||||
// example of binding button events to modify the map
|
||||
document.getElementById("zoom-fit").addEventListener("click", () => {
|
||||
const groupBounds = new L.featureGroup(markers).getBounds();
|
||||
map.fitBounds(groupBounds);
|
||||
});
|
||||
document.getElementById("zoom-us").addEventListener("click", () => {
|
||||
const usBounds = [
|
||||
[24.396308, -125.0], // Southwest corner (latitude, longitude)
|
||||
[49.384358, -66.93457], // Northeast corner (latitude, longitude)
|
||||
];
|
||||
map.fitBounds(usBounds);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
13.js-mapping/map1.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
13.js-mapping/map2.png
Normal file
After Width: | Height: | Size: 202 KiB |
142
13.js-mapping/maplibre.html
Normal file
@ -0,0 +1,142 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- maplibre needs custom CSS loaded to style its maps and controls -->
|
||||
<link
|
||||
href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
#map-goes-here {
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
#info {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
#controls {
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map-goes-here"></div>
|
||||
<div id="info">placeholder</div>
|
||||
<div id="controls">
|
||||
<button id="zoom-fit">Fit All Points</button>
|
||||
<button id="zoom-us">Fit US</button>
|
||||
</div>
|
||||
<script src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const watercolor = {
|
||||
version: 8,
|
||||
sources: {
|
||||
"raster-tiles": {
|
||||
type: "raster",
|
||||
tiles: [
|
||||
// NOTE: Layers from Stadia Maps do not require an API key for localhost development or most production
|
||||
// web deployments. See https://docs.stadiamaps.com/authentication/ for details.
|
||||
"https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg",
|
||||
],
|
||||
tileSize: 256,
|
||||
attribution:
|
||||
'Map tiles by <a target="_blank" href="https://stamen.com">Stamen Design</a>; Hosting by <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a>. Data © <a href="https://www.openstreetmap.org/about" target="_blank">OpenStreetMap</a> contributors',
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: "simple-tiles",
|
||||
type: "raster",
|
||||
source: "raster-tiles",
|
||||
minzoom: 0,
|
||||
maxzoom: 22,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tileJSON =
|
||||
"https://tiles.stadiamaps.com/styles/alidade_smooth.json";
|
||||
const map = new maplibregl.Map({
|
||||
container: "map-goes-here",
|
||||
center: [-87.6298, 41.8781],
|
||||
zoom: 12,
|
||||
style: tileJSON,
|
||||
// DEMO toggle style
|
||||
//style: watercolor,
|
||||
});
|
||||
|
||||
const points = [
|
||||
{ lat: 41.8826, lng: -87.6233, name: "Millennium Park" },
|
||||
{ lat: 41.8916, lng: -87.6083, name: "Navy Pier" },
|
||||
{ lat: 41.8526, lng: -87.6188, name: "Museum of Science and Industry" },
|
||||
];
|
||||
|
||||
// some features expect GeoJSON in the format shown here.
|
||||
// in practice we may load from an API or source file
|
||||
// if possible, it is OK to embed data directly like this
|
||||
// (Outside of this example I would probably move this to a function for reuse.)
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
features: points.map((point) => ({
|
||||
type: "Feature",
|
||||
// this should be one of the OGR geometries (Point/Line/Polygon/etc.)
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [point.lng, point.lat],
|
||||
},
|
||||
// can put arbitrary data here in properties
|
||||
properties: {
|
||||
name: point.name,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
// looping over the GeoJSON, we add a marker & popup for each
|
||||
// point in the data. the parameter here is a GeoJSON object
|
||||
geojson.features.forEach((feature) => {
|
||||
// these variables are just extracting data from the format seen above
|
||||
// which is GeoJSON's representation of a geometry
|
||||
const coordinates = feature.geometry.coordinates;
|
||||
const name = feature.properties.name;
|
||||
|
||||
// add a marker for each point
|
||||
const marker = new maplibregl.Marker()
|
||||
.setLngLat(coordinates)
|
||||
.addTo(map);
|
||||
|
||||
// add a popup with the point name
|
||||
const popup = new maplibregl.Popup().setText(name);
|
||||
marker.setPopup(popup);
|
||||
|
||||
// bind event listener to marker (identical to leaflet)
|
||||
marker.getElement().addEventListener("click", () => {
|
||||
const infoDiv = document.getElementById("info");
|
||||
infoDiv.textContent = `Selected Location: ${name}`;
|
||||
});
|
||||
});
|
||||
|
||||
// fit all points -- a bit more complicated than Leaflet's version
|
||||
document.getElementById("zoom-fit").addEventListener("click", () => {
|
||||
const bounds = geojson.features.reduce((bounds, feature) => {
|
||||
return bounds.extend(feature.geometry.coordinates);
|
||||
}, new maplibregl.LngLatBounds());
|
||||
|
||||
map.fitBounds(bounds, { padding: 20 });
|
||||
});
|
||||
|
||||
// fit entire US
|
||||
document.getElementById("zoom-us").addEventListener("click", () => {
|
||||
const usBounds = [
|
||||
[-125.0, 24.396308], // Southwest corner [lng, lat]
|
||||
[-66.93457, 49.384358], // Northeast corner [lng, lat]
|
||||
];
|
||||
map.fitBounds(usBounds, { padding: 20 });
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
171
13.js-mapping/slides.html
Normal file
187
13.js-mapping/slides.md
Normal file
@ -0,0 +1,187 @@
|
||||
# Mapping in JavaScript
|
||||
|
||||
---
|
||||
|
||||
## Today
|
||||
|
||||
- Discuss types of maps & appropriate library choices.
|
||||
- Explore examples in D3, Leaflet, and MapLibreGL.
|
||||
|
||||
---
|
||||
|
||||
## Which Tool?
|
||||
|
||||
**What kind of map?**
|
||||
|
||||
![bg left fit](map1.png)
|
||||
|
||||
Abstract/geometric representation of data.
|
||||
|
||||
D3 will provide the most flexibility & can use existing tools for color scales/interactivity/etc.
|
||||
|
||||
---
|
||||
|
||||
## Map w/ Cartographic Features
|
||||
|
||||
![bg right fit](map2.png)
|
||||
|
||||
A different library will be necessary. Depending on needs **raster** or **vector** tiles.
|
||||
|
||||
---
|
||||
|
||||
## Choropleths in D3
|
||||
|
||||
<https://d3-graph-gallery.com/graph/choropleth_basic.html>
|
||||
|
||||
### Projection
|
||||
|
||||
<https://d3-graph-gallery.com/graph/backgroundmap_changeprojection.html>
|
||||
|
||||
---
|
||||
|
||||
## Leaflet and MapLibreGL Choropleths
|
||||
|
||||
- Projection choice constrained by tile layers.
|
||||
- Need additional libraries (or pure JS) for interactivity & other graphics.
|
||||
|
||||
Demo/Tutorial: <https://leafletjs.com/examples/choropleth/>
|
||||
|
||||
---
|
||||
|
||||
## Base Maps
|
||||
|
||||
Instead of starting with a blank slate, it can be helpful to have a base map, but a base map is often comprised of many feature layers:
|
||||
|
||||
- Country/Ocean borders
|
||||
- National Sub-divisions
|
||||
- Roads
|
||||
- Terrain
|
||||
- Points of Interest
|
||||
- ...
|
||||
|
||||
Drawing all of these layers adds up.
|
||||
|
||||
---
|
||||
|
||||
## Raster Tiles
|
||||
|
||||
An innovation that made web mapping scalable in the mid-2000s was pre-rendering base layers into image tiles.
|
||||
|
||||
![](XYZ_Tiles.png)
|
||||
|
||||
---
|
||||
|
||||
## Raster Tiles
|
||||
|
||||
### Pros
|
||||
|
||||
- Simple to serve: just a lot of images.
|
||||
- Each image at a zoom level ~approx same size.
|
||||
- 100% consistent rendering.
|
||||
- Any kind of imagery (watercolor, satellite maps, etc.)
|
||||
- Minimal client-side processing needed.
|
||||
- Good for dense data.
|
||||
|
||||
---
|
||||
|
||||
### Cons
|
||||
|
||||
- Fixed zoom levels and projection.
|
||||
- Large file sizes for large Z.
|
||||
- Need to create variants for all desired permutations of features.
|
||||
- No modifications to styling after the fact.
|
||||
|
||||
---
|
||||
|
||||
## Vector Tiles
|
||||
|
||||
Instead of images, data is sent as images.
|
||||
|
||||
- Can create multiple variations per JSON, change road colors/etc without regenerating millions of images.
|
||||
- Can be more efficient on bandwidth.
|
||||
- Significantly more complexity on client and server side.
|
||||
- Older devices may struggle to render them.
|
||||
- Rendering relies on client library, can vary.
|
||||
- Not suitable for dense data such as satellite imagery.
|
||||
|
||||
Blog Post from *today* on OSM switch to Vector tiles: <https://tech.marksblogg.com/osm-mvt-vector-tiles.html>
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Leaflet
|
||||
|
||||
**Raster Tiles w/ Vector Layer on Top**
|
||||
|
||||
<https://leafletjs.com/reference.html>
|
||||
|
||||
(Has plugins to use Vector Tiles via MapLibreGL)
|
||||
|
||||
---
|
||||
|
||||
## MapLibreGL
|
||||
|
||||
Vector-tile based library. Community fork of MapboxGL.
|
||||
|
||||
Mapbox is an innovator in the space, but also quite expensive.
|
||||
|
||||
MaplibreGL/MapboxGL are mostly compatible.
|
||||
|
||||
---
|
||||
|
||||
## General Workflow
|
||||
|
||||
- Use library to render map(s) to div(s) on page.
|
||||
- Pick base layer, either raster or vector.
|
||||
- If vector, possibly apply additional styling to base.
|
||||
- Add additional vector layers based on your data & its features. Do this using methods on the `map` object you got back in the earlier steps.
|
||||
- Attach event handlers for interactivity on your features and/or input elements adjacent to the map.
|
||||
|
||||
---
|
||||
|
||||
## Clustering
|
||||
|
||||
![bg fit right](clustering.png)
|
||||
|
||||
Helps reduce visual clutter and increase performance when there are many points.
|
||||
|
||||
- <https://github.com/Leaflet/Leaflet.markercluster>
|
||||
- <https://maplibre.org/maplibre-gl-js/docs/examples/cluster/>
|
||||
|
||||
---
|
||||
|
||||
## Pop-ups
|
||||
|
||||
Leaflet/MapLibre offer a simple API for adding pop-ups to features.
|
||||
|
||||
Reminder: hover/pop-up alone is **not enough interactivity** for final product.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
Leaflet & MapLibre examples
|
||||
|
||||
---
|
||||
|
||||
## Custom Base Layers
|
||||
|
||||
- [Stadia Maps Hosted Tiles](https://stadiamaps.com/products/map-tiles/), free for noncommercial/academic use. You will need an account though.
|
||||
- [OpenMapTiles](https://openmaptiles.org/styles/) - free, meant for self hosting.
|
||||
- [Mapbox](https://www.mapbox.com) - another paid solution, $$$.
|
||||
|
||||
---
|
||||
|
||||
## Other Libraries
|
||||
|
||||
- [Deck.gl](https://deck.gl) - Alternative renderer similar to Map*GL. TypeScript/React focused mostly, hard to use with JS we covered.
|
||||
- [OpenLayers](https://openlayers.org) -
|
||||
- [Cesium](https://cesium.com) - 3D-focused geospatial.
|
||||
- Note: You can also get 3D rendering w/ D3 or MapLibreGL.
|
||||
|
||||
---
|
||||
|
||||
## More Examples
|
||||
|
||||
- <https://projects.fivethirtyeight.com/redistricting-maps/alabama/#Competitive>
|
||||
- <https://projects.fivethirtyeight.com/partisan-gerrymandering-north-carolina/>
|
BIN
13.js-mapping/tiles.png
Normal file
After Width: | Height: | Size: 190 KiB |
BIN
14.misc/mosaic.jpg
Normal file
After Width: | Height: | Size: 281 KiB |
BIN
14.misc/plotly-dash.png
Normal file
After Width: | Height: | Size: 192 KiB |
228
14.misc/slides.html
Normal file
238
14.misc/slides.md
Normal file
@ -0,0 +1,238 @@
|
||||
# Data Visualization for Public Policy
|
||||
|
||||
## Miscellaneous Odds & Ends
|
||||
|
||||
---
|
||||
|
||||
## This Week
|
||||
|
||||
- Project Questions / Deployment
|
||||
- Code Quality & Style
|
||||
- Dashboards
|
||||
- Visual Style Guides
|
||||
- 100 Visualizations
|
||||
- Animation & Interaction
|
||||
|
||||
---
|
||||
|
||||
## Code Quality
|
||||
|
||||
When you are writing a data pipeline or application, code quality is of high importance.
|
||||
|
||||
- Readability & Documentation => easier maintenance & bugs prevented.
|
||||
- Small speed-ups from better algorithm/data structure choices can make big differences when that task executes millions of times.
|
||||
- Test coverage makes refactoring easier, prevents regressions.
|
||||
|
||||
---
|
||||
|
||||
### Unique Considerations for Data Viz
|
||||
|
||||
- Typically little to no ongoing reuse/maintenance.
|
||||
- Visualization itself unlikely to be performance bottleneck compared to data manipulation.
|
||||
- Focus is on **immediate visual output**, testing de-emphasized.
|
||||
- Often written by solo developer, even in larger organizations.
|
||||
|
||||
Code quality still matters, but your main goal should be code that you can trust is correct. Testing, documentation, and the "right way" are less essential.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Dashboards
|
||||
|
||||
![bg fit left](plotly-dash.png)
|
||||
|
||||
Long-lived data visualizations that typically run against a central repository of data.
|
||||
|
||||
You can use the same techniques & tools, or custom dashboard-focused tools like Tableau or Dash.
|
||||
|
||||
Key difference: You will likely need some degree of dynamic refresh (instead of loading CSV/JSON load data from DB/API). Comes with caching and other performance considerations.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Psuedocode
|
||||
|
||||
```
|
||||
every interval {
|
||||
data = update()
|
||||
visualize(data)
|
||||
}
|
||||
```
|
||||
|
||||
Can make use of animation to provide context:
|
||||
|
||||
- scrolling time series
|
||||
- animated dials to show directional changes
|
||||
|
||||
---
|
||||
|
||||
## Are Dashboards Bad?
|
||||
|
||||
Dashboards saw a surge in popularity a decade or so ago, and there are now plenty of bad dashboards out there.
|
||||
|
||||
Golden rule of dashboards: **answer a question & make them actionable**.
|
||||
|
||||
Too often people just throw all their data on a dashboard.
|
||||
|
||||
*OK, I can see that 6 errors occurred in the last 24 hours...*
|
||||
|
||||
- Is that a lot? **Show trends where appropriate!**
|
||||
- What can I do? **Provide links/action items!**
|
||||
|
||||
Without this focus, dashboards become decorations.
|
||||
|
||||
---
|
||||
|
||||
## Style Guide
|
||||
|
||||
It can be helpful to create or build from a style guide. Even for your own work.
|
||||
|
||||
Examples:
|
||||
|
||||
- [Sunlight Foundation](https://www.amycesal.com/portfolio#/data-visualization-style-guidelines/)
|
||||
- [CFPB](https://www.amycesal.com/portfolio#/cfpb-design-manual-data-visualization/)
|
||||
|
||||
---
|
||||
|
||||
## Key Elements
|
||||
|
||||
### Typography
|
||||
|
||||
Select 2 complementary fonts:
|
||||
|
||||
- Prefer a very legible sans-serif font for data/axes labels.
|
||||
- Any legible font for chart titles/narrative/etc.
|
||||
|
||||
### Color Selection
|
||||
|
||||
Best to have:
|
||||
|
||||
- Nominal data: Distinct, contrasting hues
|
||||
- Quantitative data: Linear or divergent gradients
|
||||
- Consider color-blindness and accessibility
|
||||
|
||||
---
|
||||
|
||||
## Style Guide: Chart Selection
|
||||
- Match chart type to **data characteristics** and **audience**.
|
||||
- Consider:
|
||||
- Data dimensionality
|
||||
- Comparison needs
|
||||
- Narrative goals
|
||||
|
||||
---
|
||||
|
||||
## Creativity: 1 Dataset 100 Visualizations
|
||||
|
||||
<https://100.datavizproject.com>
|
||||
|
||||
---
|
||||
|
||||
## Applications of Animation
|
||||
|
||||
- Demonstrate change over time: Data being added to chart as time "plays."
|
||||
- Highlight relationships: Hover/highlight/select modifies display of other data on page.
|
||||
- Focus attention: Show subsets of data at a time.
|
||||
- Show uncertainty: "wiggle", shifting trend line (next page)
|
||||
|
||||
More Examples:
|
||||
- <https://informationisbeautiful.net>
|
||||
- <https://www.visualcinnamon.com/portfolio/>
|
||||
|
||||
|
||||
---
|
||||
|
||||
![bg fit](https://clauswilke.com/dataviz/visualizing_uncertainty_files/figure-html/mpg-uncertain-HOP-animated-1.gif)
|
||||
|
||||
---
|
||||
|
||||
## Applications of Interaction
|
||||
|
||||
- Enable user-driven exploration of data.
|
||||
- "How do these two variables compare?"
|
||||
- "What happens if this price increases?"
|
||||
- Allow personalization (e.g. enter your zip code)
|
||||
- "What is this like in my city?"
|
||||
- Increased engagement/retention. Lots of evidence showing we learn best by participating.
|
||||
|
||||
---
|
||||
|
||||
## JS setInterval
|
||||
|
||||
```js
|
||||
// will call `func` every `everyMS`
|
||||
let intervalId = setInterval(func, everyMS)
|
||||
|
||||
// stop calling func
|
||||
clearInterval(intervalId)
|
||||
```
|
||||
|
||||
<https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval>
|
||||
|
||||
---
|
||||
|
||||
## Interaction: Making Data Selections
|
||||
|
||||
For user-driven data explorations, **selection** is an important concept.
|
||||
|
||||
How do you want to let a user select individual records or groups of records?
|
||||
|
||||
### Selection Spectrum: Simple to Complex
|
||||
|
||||
- Menu/Select Box
|
||||
- Hover/click on items on page (tooltips, etc.)
|
||||
- Drag/Region selection
|
||||
- Pre-written SQL queries with dropdowns/selects. (Common on dashboards.)
|
||||
- Allow user to write queries themselves in SQL or a custom query language. Common on advanced dashboards.
|
||||
|
||||
Altair Selection: <https://altair-viz.github.io/user_guide/interactions.html>
|
||||
D3 Selection: <https://observablehq.com/collection/@d3/d3-selection>
|
||||
|
||||
---
|
||||
|
||||
## Discussion: Major Visualization Challenges
|
||||
|
||||
- Missing/Incomplete data
|
||||
- Huge quantities of data
|
||||
- Complex, high-dimensional data
|
||||
- Uncertainty
|
||||
- Challenges of Scale
|
||||
|
||||
---
|
||||
|
||||
### Missing/Incomplete Data
|
||||
|
||||
- Imputation of missing values.
|
||||
- Label missing data.
|
||||
- Regardless of choice. Be transparent.
|
||||
|
||||
---
|
||||
|
||||
### Big Data
|
||||
|
||||
- Aggregation
|
||||
- Sampling
|
||||
- Filtering/Interactives
|
||||
|
||||
---
|
||||
|
||||
### Lots of Attributes/Dimensions
|
||||
|
||||
- Small multiples approach
|
||||
- Pairwise charts. (XY, YZ, XZ)
|
||||
- Advanced: Dimensionality Reduction Algorithms (PCA, TSNE, etc.)
|
||||
- Interactive exploration
|
||||
|
||||
---
|
||||
|
||||
### Handling Uncertainty
|
||||
|
||||
- Frequency Approach
|
||||
- Confidence intervals & error bars
|
||||
- Probabilistic visualizations
|
||||
|
||||
---
|
||||
|
||||
### Visualizing Scale
|
||||
|
||||
- Hierarchical visualizations (treemaps)
|
||||
- Logarithmic scales when appropriate.
|
119
15.conclusion/slides.html
Normal file
119
15.conclusion/slides.md
Normal file
@ -0,0 +1,119 @@
|
||||
# Data Visualization for Public Policy
|
||||
|
||||
![bg fit right](mosaic.jpg)
|
||||
|
||||
---
|
||||
|
||||
## Recap: Why Do We Create Visualizations?
|
||||
|
||||
- To **better understand** large, complex datasets.
|
||||
- To **influence others** through compelling, evidence-based storytelling.
|
||||
|
||||
---
|
||||
|
||||
## Influence: The Power of Visual Communication
|
||||
|
||||
Effective data visualizations can:
|
||||
|
||||
- Draw attention to critical problems or potential solutions.
|
||||
- Argue for specific policy interventions.
|
||||
- Connect an audience with large and potentially abstract data concepts.
|
||||
|
||||
---
|
||||
|
||||
## Key Ideas Exercise
|
||||
|
||||
What are *your* golden rules of data visualization?
|
||||
|
||||
---
|
||||
|
||||
## (Some) Key Rules for Effective Data Visualization
|
||||
|
||||
### 1. Audience-Centered Design
|
||||
|
||||
- Take time to consider and understand your audience's background, expertise, and information needs.
|
||||
- The "best" data visualization is one that the audience understands & remembers.
|
||||
|
||||
---
|
||||
|
||||
### 2. **Prioritize Truthful Representation**
|
||||
|
||||
- Correct chart types & encodings.
|
||||
- Never sacrifice **data integrity** in the name of a "better" chart.
|
||||
- Avoid misleading choices: truncated axes, dual axes, etc.
|
||||
- Consider the role of **uncertainity** in representing your data.
|
||||
|
||||
---
|
||||
|
||||
<!-- Mackinlay's Effectiveness Hierarchy-->
|
||||
|
||||
![bg fit](../01.gog-altair/effectiveness.png)
|
||||
|
||||
---
|
||||
|
||||
### 3. **Maximize Clarity and Comprehension**
|
||||
|
||||
- Simplify complex information where possible. It is OK to refer a user to a table or other source for deeper analysis.
|
||||
- Remove unnecessary visual elements -- "chart junk"
|
||||
- Guide the viewer's attention to key insights with **labeling**.
|
||||
|
||||
---
|
||||
|
||||
## Tufte's Key Ideas Revisited
|
||||
|
||||
![bg fit right](../03.charts/tufte.png)
|
||||
|
||||
- Graphical Integrity: Above all else, show the data.
|
||||
- Maximize the data-ink ratio.
|
||||
- Minimize chart junk.
|
||||
- Aim for high chart density, consider *small multiples*.
|
||||
- Revision & Editing are essential.
|
||||
|
||||
---
|
||||
|
||||
### 4. **Optimize for Accessibility**
|
||||
|
||||
- Use color-blind friendly palettes.
|
||||
- Ensure readability for viewers with different visual capabilities. (Contrast,font size, etc.)
|
||||
- Provide *alternative text descriptions* in web presentations.
|
||||
- `<img src="..." alt="A graphic representing the length of rivers..." />`
|
||||
- Accessibility tools: contrast/color/WCAG checkers.
|
||||
|
||||
---
|
||||
|
||||
### 5. **Build a Compelling Narrative**
|
||||
|
||||
- Create a clear, coherent story and use graphics to support it.
|
||||
- Each chart should have a clear "why" -- don't make users wonder why you're showing them something.
|
||||
- Use visual elements & conventions to guide the viewer through key arguments and order.
|
||||
- Connect data to broader context and implications.
|
||||
|
||||
---
|
||||
|
||||
### 6. **Embrace Iterative Improvement**
|
||||
|
||||
- Seek feedback from diverse perspectives, especially those represented in your audience.
|
||||
- Be willing to revise and refine, if someone had an issue others will too.
|
||||
|
||||
---
|
||||
|
||||
### 7. **Consider Ethical Implications**
|
||||
|
||||
- Represent marginalized groups respectfully: color choices, language.
|
||||
- Remember that pixels often represent people, dismissing outliers/etc. should not be done without consideration.
|
||||
- Be transparent about data sources and limitations.
|
||||
- Use visualization as a tool for **understanding and persuasion, not manipulation**.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Effective data visualization is both an art and a science.
|
||||
|
||||
Understand your data and what you people to understand.
|
||||
|
||||
Center your audience.
|
||||
|
||||
Prioritize clarity & truth.
|
||||
|
||||
Be creative & have fun!
|
378
examples/sorting_and_grouping_bars.ipynb
Normal file
@ -0,0 +1,378 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "228d57af-2bd1-458d-860b-e17b2fe7d445",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example: Bar Ordering\n",
|
||||
"\n",
|
||||
"Don't neglect the role of ordering when working with categorical variables. These provide an additional opportunity to emphasize or highlight.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "279c3be4-53f3-4db5-a4e3-8f55019529af",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import altair as alt\n",
|
||||
"import polars as pl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "b071e9be-1fad-4852-87b8-38157be6de66",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"countries = {\n",
|
||||
" \"AA\": 13, \"AB\": 45, \"AC\": 30, \"AD\": 14, \"AE\": 21,\n",
|
||||
" \"BA\": 17, \"BB\": 25, \"BC\": 29, \"BD\": 16, \"BE\": 21,\n",
|
||||
" \"CA\": 20, \"CB\": 22, \"CC\": 28, \"CD\": 18, \"CE\": 24,\n",
|
||||
" \"DA\": 40, \"DB\": 33, \"DC\": 30, \"DD\": 13, \"CE\": 28,\n",
|
||||
" \"EA\": 16, \"EB\": 45, \"EC\": 27, \"ED\": 80, \"CE\": 33,\n",
|
||||
" }"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "f9d54624-a50e-414e-a29c-c8371fe5b98f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df = pl.DataFrame(countries).unpivot(variable_name=\"country\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "334fe526-2536-478c-9457-aad9e166eb1d",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"\n",
|
||||
"<style>\n",
|
||||
" #altair-viz-6128fc8afb7940f1ad0eba49c5b71e04.vega-embed {\n",
|
||||
" width: 100%;\n",
|
||||
" display: flex;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" #altair-viz-6128fc8afb7940f1ad0eba49c5b71e04.vega-embed details,\n",
|
||||
" #altair-viz-6128fc8afb7940f1ad0eba49c5b71e04.vega-embed details summary {\n",
|
||||
" position: relative;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<div id=\"altair-viz-6128fc8afb7940f1ad0eba49c5b71e04\"></div>\n",
|
||||
"<script type=\"text/javascript\">\n",
|
||||
" var VEGA_DEBUG = (typeof VEGA_DEBUG == \"undefined\") ? {} : VEGA_DEBUG;\n",
|
||||
" (function(spec, embedOpt){\n",
|
||||
" let outputDiv = document.currentScript.previousElementSibling;\n",
|
||||
" if (outputDiv.id !== \"altair-viz-6128fc8afb7940f1ad0eba49c5b71e04\") {\n",
|
||||
" outputDiv = document.getElementById(\"altair-viz-6128fc8afb7940f1ad0eba49c5b71e04\");\n",
|
||||
" }\n",
|
||||
" const paths = {\n",
|
||||
" \"vega\": \"https://cdn.jsdelivr.net/npm/vega@5?noext\",\n",
|
||||
" \"vega-lib\": \"https://cdn.jsdelivr.net/npm/vega-lib?noext\",\n",
|
||||
" \"vega-lite\": \"https://cdn.jsdelivr.net/npm/vega-lite@5.20.1?noext\",\n",
|
||||
" \"vega-embed\": \"https://cdn.jsdelivr.net/npm/vega-embed@6?noext\",\n",
|
||||
" };\n",
|
||||
"\n",
|
||||
" function maybeLoadScript(lib, version) {\n",
|
||||
" var key = `${lib.replace(\"-\", \"\")}_version`;\n",
|
||||
" return (VEGA_DEBUG[key] == version) ?\n",
|
||||
" Promise.resolve(paths[lib]) :\n",
|
||||
" new Promise(function(resolve, reject) {\n",
|
||||
" var s = document.createElement('script');\n",
|
||||
" document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
|
||||
" s.async = true;\n",
|
||||
" s.onload = () => {\n",
|
||||
" VEGA_DEBUG[key] = version;\n",
|
||||
" return resolve(paths[lib]);\n",
|
||||
" };\n",
|
||||
" s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
|
||||
" s.src = paths[lib];\n",
|
||||
" });\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" function showError(err) {\n",
|
||||
" outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
|
||||
" throw err;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" function displayChart(vegaEmbed) {\n",
|
||||
" vegaEmbed(outputDiv, spec, embedOpt)\n",
|
||||
" .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" if(typeof define === \"function\" && define.amd) {\n",
|
||||
" requirejs.config({paths});\n",
|
||||
" require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
|
||||
" } else {\n",
|
||||
" maybeLoadScript(\"vega\", \"5\")\n",
|
||||
" .then(() => maybeLoadScript(\"vega-lite\", \"5.20.1\"))\n",
|
||||
" .then(() => maybeLoadScript(\"vega-embed\", \"6\"))\n",
|
||||
" .catch(showError)\n",
|
||||
" .then(() => displayChart(vegaEmbed));\n",
|
||||
" }\n",
|
||||
" })({\"config\": {\"view\": {\"continuousWidth\": 300, \"continuousHeight\": 300}}, \"data\": {\"name\": \"data-9bd5b15557267b851df082199b597c82\"}, \"mark\": {\"type\": \"bar\"}, \"encoding\": {\"x\": {\"field\": \"country\", \"type\": \"nominal\"}, \"y\": {\"field\": \"value\", \"type\": \"quantitative\"}}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.20.1.json\", \"datasets\": {\"data-9bd5b15557267b851df082199b597c82\": [{\"country\": \"AA\", \"value\": 13}, {\"country\": \"AB\", \"value\": 45}, {\"country\": \"AC\", \"value\": 30}, {\"country\": \"AD\", \"value\": 14}, {\"country\": \"AE\", \"value\": 21}, {\"country\": \"BA\", \"value\": 17}, {\"country\": \"BB\", \"value\": 25}, {\"country\": \"BC\", \"value\": 29}, {\"country\": \"BD\", \"value\": 16}, {\"country\": \"BE\", \"value\": 21}, {\"country\": \"CA\", \"value\": 20}, {\"country\": \"CB\", \"value\": 22}, {\"country\": \"CC\", \"value\": 28}, {\"country\": \"CD\", \"value\": 18}, {\"country\": \"CE\", \"value\": 33}, {\"country\": \"DA\", \"value\": 40}, {\"country\": \"DB\", \"value\": 33}, {\"country\": \"DC\", \"value\": 30}, {\"country\": \"DD\", \"value\": 13}, {\"country\": \"EA\", \"value\": 16}, {\"country\": \"EB\", \"value\": 45}, {\"country\": \"EC\", \"value\": 27}, {\"country\": \"ED\", \"value\": 80}]}}, {\"mode\": \"vega-lite\"});\n",
|
||||
"</script>"
|
||||
],
|
||||
"text/plain": [
|
||||
"alt.Chart(...)"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"alt.Chart(df).mark_bar().encode(x=\"country\", y=\"value\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e163a712-3674-48bf-b0d3-f1d279c5ba2b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This alphabetical ordering may serve you well if you want people to quickly be able to find their country. But you can consider other orderings & groupings that might make your point better."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "7883c59e-0bb4-4713-8772-aca9441e6800",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"\n",
|
||||
"<style>\n",
|
||||
" #altair-viz-1d055bbfea574170835834169a6b5fbf.vega-embed {\n",
|
||||
" width: 100%;\n",
|
||||
" display: flex;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" #altair-viz-1d055bbfea574170835834169a6b5fbf.vega-embed details,\n",
|
||||
" #altair-viz-1d055bbfea574170835834169a6b5fbf.vega-embed details summary {\n",
|
||||
" position: relative;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<div id=\"altair-viz-1d055bbfea574170835834169a6b5fbf\"></div>\n",
|
||||
"<script type=\"text/javascript\">\n",
|
||||
" var VEGA_DEBUG = (typeof VEGA_DEBUG == \"undefined\") ? {} : VEGA_DEBUG;\n",
|
||||
" (function(spec, embedOpt){\n",
|
||||
" let outputDiv = document.currentScript.previousElementSibling;\n",
|
||||
" if (outputDiv.id !== \"altair-viz-1d055bbfea574170835834169a6b5fbf\") {\n",
|
||||
" outputDiv = document.getElementById(\"altair-viz-1d055bbfea574170835834169a6b5fbf\");\n",
|
||||
" }\n",
|
||||
" const paths = {\n",
|
||||
" \"vega\": \"https://cdn.jsdelivr.net/npm/vega@5?noext\",\n",
|
||||
" \"vega-lib\": \"https://cdn.jsdelivr.net/npm/vega-lib?noext\",\n",
|
||||
" \"vega-lite\": \"https://cdn.jsdelivr.net/npm/vega-lite@5.20.1?noext\",\n",
|
||||
" \"vega-embed\": \"https://cdn.jsdelivr.net/npm/vega-embed@6?noext\",\n",
|
||||
" };\n",
|
||||
"\n",
|
||||
" function maybeLoadScript(lib, version) {\n",
|
||||
" var key = `${lib.replace(\"-\", \"\")}_version`;\n",
|
||||
" return (VEGA_DEBUG[key] == version) ?\n",
|
||||
" Promise.resolve(paths[lib]) :\n",
|
||||
" new Promise(function(resolve, reject) {\n",
|
||||
" var s = document.createElement('script');\n",
|
||||
" document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
|
||||
" s.async = true;\n",
|
||||
" s.onload = () => {\n",
|
||||
" VEGA_DEBUG[key] = version;\n",
|
||||
" return resolve(paths[lib]);\n",
|
||||
" };\n",
|
||||
" s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
|
||||
" s.src = paths[lib];\n",
|
||||
" });\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" function showError(err) {\n",
|
||||
" outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
|
||||
" throw err;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" function displayChart(vegaEmbed) {\n",
|
||||
" vegaEmbed(outputDiv, spec, embedOpt)\n",
|
||||
" .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" if(typeof define === \"function\" && define.amd) {\n",
|
||||
" requirejs.config({paths});\n",
|
||||
" require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
|
||||
" } else {\n",
|
||||
" maybeLoadScript(\"vega\", \"5\")\n",
|
||||
" .then(() => maybeLoadScript(\"vega-lite\", \"5.20.1\"))\n",
|
||||
" .then(() => maybeLoadScript(\"vega-embed\", \"6\"))\n",
|
||||
" .catch(showError)\n",
|
||||
" .then(() => displayChart(vegaEmbed));\n",
|
||||
" }\n",
|
||||
" })({\"config\": {\"view\": {\"continuousWidth\": 300, \"continuousHeight\": 300}}, \"data\": {\"name\": \"data-9bd5b15557267b851df082199b597c82\"}, \"mark\": {\"type\": \"bar\"}, \"encoding\": {\"x\": {\"field\": \"country\", \"sort\": {\"field\": \"value\", \"order\": \"ascending\"}, \"type\": \"nominal\"}, \"y\": {\"field\": \"value\", \"type\": \"quantitative\"}}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.20.1.json\", \"datasets\": {\"data-9bd5b15557267b851df082199b597c82\": [{\"country\": \"AA\", \"value\": 13}, {\"country\": \"AB\", \"value\": 45}, {\"country\": \"AC\", \"value\": 30}, {\"country\": \"AD\", \"value\": 14}, {\"country\": \"AE\", \"value\": 21}, {\"country\": \"BA\", \"value\": 17}, {\"country\": \"BB\", \"value\": 25}, {\"country\": \"BC\", \"value\": 29}, {\"country\": \"BD\", \"value\": 16}, {\"country\": \"BE\", \"value\": 21}, {\"country\": \"CA\", \"value\": 20}, {\"country\": \"CB\", \"value\": 22}, {\"country\": \"CC\", \"value\": 28}, {\"country\": \"CD\", \"value\": 18}, {\"country\": \"CE\", \"value\": 33}, {\"country\": \"DA\", \"value\": 40}, {\"country\": \"DB\", \"value\": 33}, {\"country\": \"DC\", \"value\": 30}, {\"country\": \"DD\", \"value\": 13}, {\"country\": \"EA\", \"value\": 16}, {\"country\": \"EB\", \"value\": 45}, {\"country\": \"EC\", \"value\": 27}, {\"country\": \"ED\", \"value\": 80}]}}, {\"mode\": \"vega-lite\"});\n",
|
||||
"</script>"
|
||||
],
|
||||
"text/plain": [
|
||||
"alt.Chart(...)"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# ordering by value to emphasize outliers\n",
|
||||
"alt.Chart(df).mark_bar().encode(\n",
|
||||
" alt.X(\"country\", sort=alt.EncodingSortField(field=\"value\", order=\"ascending\")\n",
|
||||
" ),\n",
|
||||
" y=\"value\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"id": "1b965691-7e32-45d7-b1db-833e353c230d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# adding a grouping column and using it for coloring/grouping\n",
|
||||
"# using first letter of country for a pretend grouping -- \n",
|
||||
"# in reality you could group by region/characteristics\n",
|
||||
"df_grouped = df.with_columns(grouping=pl.col(\"country\").str.slice(0, 1))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 24,
|
||||
"id": "c7735212-e6aa-405d-9b76-87a92565e88b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"\n",
|
||||
"<style>\n",
|
||||
" #altair-viz-21fe0d7284524faaa93049adb64dd794.vega-embed {\n",
|
||||
" width: 100%;\n",
|
||||
" display: flex;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" #altair-viz-21fe0d7284524faaa93049adb64dd794.vega-embed details,\n",
|
||||
" #altair-viz-21fe0d7284524faaa93049adb64dd794.vega-embed details summary {\n",
|
||||
" position: relative;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<div id=\"altair-viz-21fe0d7284524faaa93049adb64dd794\"></div>\n",
|
||||
"<script type=\"text/javascript\">\n",
|
||||
" var VEGA_DEBUG = (typeof VEGA_DEBUG == \"undefined\") ? {} : VEGA_DEBUG;\n",
|
||||
" (function(spec, embedOpt){\n",
|
||||
" let outputDiv = document.currentScript.previousElementSibling;\n",
|
||||
" if (outputDiv.id !== \"altair-viz-21fe0d7284524faaa93049adb64dd794\") {\n",
|
||||
" outputDiv = document.getElementById(\"altair-viz-21fe0d7284524faaa93049adb64dd794\");\n",
|
||||
" }\n",
|
||||
" const paths = {\n",
|
||||
" \"vega\": \"https://cdn.jsdelivr.net/npm/vega@5?noext\",\n",
|
||||
" \"vega-lib\": \"https://cdn.jsdelivr.net/npm/vega-lib?noext\",\n",
|
||||
" \"vega-lite\": \"https://cdn.jsdelivr.net/npm/vega-lite@5.20.1?noext\",\n",
|
||||
" \"vega-embed\": \"https://cdn.jsdelivr.net/npm/vega-embed@6?noext\",\n",
|
||||
" };\n",
|
||||
"\n",
|
||||
" function maybeLoadScript(lib, version) {\n",
|
||||
" var key = `${lib.replace(\"-\", \"\")}_version`;\n",
|
||||
" return (VEGA_DEBUG[key] == version) ?\n",
|
||||
" Promise.resolve(paths[lib]) :\n",
|
||||
" new Promise(function(resolve, reject) {\n",
|
||||
" var s = document.createElement('script');\n",
|
||||
" document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
|
||||
" s.async = true;\n",
|
||||
" s.onload = () => {\n",
|
||||
" VEGA_DEBUG[key] = version;\n",
|
||||
" return resolve(paths[lib]);\n",
|
||||
" };\n",
|
||||
" s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
|
||||
" s.src = paths[lib];\n",
|
||||
" });\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" function showError(err) {\n",
|
||||
" outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
|
||||
" throw err;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" function displayChart(vegaEmbed) {\n",
|
||||
" vegaEmbed(outputDiv, spec, embedOpt)\n",
|
||||
" .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" if(typeof define === \"function\" && define.amd) {\n",
|
||||
" requirejs.config({paths});\n",
|
||||
" require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
|
||||
" } else {\n",
|
||||
" maybeLoadScript(\"vega\", \"5\")\n",
|
||||
" .then(() => maybeLoadScript(\"vega-lite\", \"5.20.1\"))\n",
|
||||
" .then(() => maybeLoadScript(\"vega-embed\", \"6\"))\n",
|
||||
" .catch(showError)\n",
|
||||
" .then(() => displayChart(vegaEmbed));\n",
|
||||
" }\n",
|
||||
" })({\"config\": {\"view\": {\"continuousWidth\": 300, \"continuousHeight\": 300}}, \"data\": {\"name\": \"data-e05227b311c61468e366e00d8c0d9aa3\"}, \"mark\": {\"type\": \"bar\"}, \"encoding\": {\"color\": {\"field\": \"grouping\", \"type\": \"nominal\"}, \"x\": {\"field\": \"country\", \"sort\": {\"field\": \"group\", \"order\": \"ascending\"}, \"type\": \"nominal\"}, \"y\": {\"field\": \"value\", \"type\": \"quantitative\"}}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.20.1.json\", \"datasets\": {\"data-e05227b311c61468e366e00d8c0d9aa3\": [{\"country\": \"AA\", \"value\": 13, \"grouping\": \"A\"}, {\"country\": \"AB\", \"value\": 45, \"grouping\": \"A\"}, {\"country\": \"AC\", \"value\": 30, \"grouping\": \"A\"}, {\"country\": \"AD\", \"value\": 14, \"grouping\": \"A\"}, {\"country\": \"AE\", \"value\": 21, \"grouping\": \"A\"}, {\"country\": \"BA\", \"value\": 17, \"grouping\": \"B\"}, {\"country\": \"BB\", \"value\": 25, \"grouping\": \"B\"}, {\"country\": \"BC\", \"value\": 29, \"grouping\": \"B\"}, {\"country\": \"BD\", \"value\": 16, \"grouping\": \"B\"}, {\"country\": \"BE\", \"value\": 21, \"grouping\": \"B\"}, {\"country\": \"CA\", \"value\": 20, \"grouping\": \"C\"}, {\"country\": \"CB\", \"value\": 22, \"grouping\": \"C\"}, {\"country\": \"CC\", \"value\": 28, \"grouping\": \"C\"}, {\"country\": \"CD\", \"value\": 18, \"grouping\": \"C\"}, {\"country\": \"CE\", \"value\": 33, \"grouping\": \"C\"}, {\"country\": \"DA\", \"value\": 40, \"grouping\": \"D\"}, {\"country\": \"DB\", \"value\": 33, \"grouping\": \"D\"}, {\"country\": \"DC\", \"value\": 30, \"grouping\": \"D\"}, {\"country\": \"DD\", \"value\": 13, \"grouping\": \"D\"}, {\"country\": \"EA\", \"value\": 16, \"grouping\": \"E\"}, {\"country\": \"EB\", \"value\": 45, \"grouping\": \"E\"}, {\"country\": \"EC\", \"value\": 27, \"grouping\": \"E\"}, {\"country\": \"ED\", \"value\": 80, \"grouping\": \"E\"}]}}, {\"mode\": \"vega-lite\"});\n",
|
||||
"</script>"
|
||||
],
|
||||
"text/plain": [
|
||||
"alt.Chart(...)"
|
||||
]
|
||||
},
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"alt.Chart(df_grouped).mark_bar().encode(\n",
|
||||
" alt.X(\"country\", sort=alt.EncodingSortField(field=\"group\", order=\"ascending\")),\n",
|
||||
" y=\"value\",\n",
|
||||
" color=\"grouping:N\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a721eddb-a9b1-457a-9648-e9606b62f044",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.15"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|