Compare commits

...

10 commits

Author SHA1 Message Date
04bbfb09a5
Fix map display issues on blog output 2024-07-03 20:28:02 +02:00
4ec27ed73b
Add pillow dependency 2024-07-03 20:27:25 +02:00
4306a7d246
Fix plot links for astro static files 2024-07-03 20:00:37 +02:00
e90b423ebc
Split into default and blog-ready render profiles 2024-07-03 20:00:21 +02:00
8cbe6c3571
Extend gitignore for outputs and intermediate files
Ignore new output directory, any intermediary file directories and
python notebook files (also usually intermediate in quarto projects).
2024-07-03 16:32:54 +02:00
d067d41267
Add quarto project file 2024-07-03 16:30:42 +02:00
fc22e0cc02
Add selenium as dependency
Required for folium maps to png file pipeline.
2024-07-03 16:30:23 +02:00
824ad25e60
Add meta-article
Contains thoughts about the process writing the article.
2024-07-03 16:29:48 +02:00
7d1d929b0e
Update article and rename to index.qmd 2024-07-03 16:29:23 +02:00
fed35fcfd2
Add metadata sheet to data directory 2024-07-03 16:28:40 +02:00
10 changed files with 650 additions and 148 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/.quarto/
/output/
/*_files/
*.ipynb

19
_quarto-blog.yml Normal file
View file

@ -0,0 +1,19 @@
project:
type: default
output-dir: /home/marty/projects/hosting/webpage/src/content/blog/2024-07-02-nuclear-explosions-analysis
render:
- index.qmd
post-render:
- tools/fix-astro-img.py
format:
hugo-md:
preserve-yaml: true
code-fold: true
typst:
toc: true
echo: false
citeproc: true
docx:
toc: true
echo: false

35
_quarto-default.yml Normal file
View file

@ -0,0 +1,35 @@
project:
type: default
output-dir: output
render:
- index.qmd
- meta.md
execute:
cache: true
format:
html:
code-fold: true
toc: true
echo: true
typst:
toc: true
echo: false
citeproc: true
docx:
toc: true
echo: false
# pdf: # BREAKS ON 'GREAT TABLES' python lib tables
# echo: false # since we want to see the code in this case
# papersize: A4
# # geometry:
# # - left=2cm
# # - right=2.5cm
# # - top=2.5cm
# # - bottom=2.5cm
# indent: true
# linestretch: 1.5
# fontfamily: lmodern
# fontsize: "12"
# pdf-engine: tectonic

6
_quarto.yml Normal file
View file

@ -0,0 +1,6 @@
author: Marty Oehme
csl: https://www.zotero.org/styles/apa
profile:
group:
- [default, blog]

40
data/metasheet.md Normal file
View file

@ -0,0 +1,40 @@
# Nuclear Explosions
This week's [**data**](nuclear_explosions.csv) is from [Stockholm International Peace Research Institute](https://github.com/data-is-plural/nuclear-explosions/blob/master/documents/sipri-report-original.pdf), by way of [data is plural](https://github.com/data-is-plural/nuclear-explosions) with credit to [Jesus Castagnetto](https://github.com/rfordatascience/tidytuesday/issues/91) for sharing the dataset.
Additional information can be found on [Wikipedia](https://en.wikipedia.org/wiki/List_of_nuclear_weapons_tests) or via the original report [PDF](https://github.com/data-is-plural/nuclear-explosions/blob/master/documents/sipri-report-original.pdf).
Additional related datasets can be found at [Our World in Data](https://ourworldindata.org/nuclear-weapons).
For details around units for yield/magnitude, please see the [Nuclear Yield](https://seismo.berkeley.edu/~rallen/research/nuke/yield.html) formulas.
# Get the data!
```
nuclear_explosions <- readr::read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2019/2019-08-20/nuclear_explosions.csv")
```
# Data Dictionary
## `nuclear_explosions.csv`
|variable |class |description |
|:--- |:--- |:-----------|
|date_long |date | ymd date|
|year |double | year of explosion |
|id_no |double | unique ID |
|country |character | Country deploying the nuclear device |
|region |character | Region where nuclear device was deployed |
|source |character | Source the reported the explosion event |
|latitude |double | Latitude position |
|longitude |double | Longitude position |
|magnitude_body |double | Body wave magnitude of explosion (mb)|
|magnitude_surface |double | Surface wave magnitude of explosion (Ms) |
|depth |double | Depth at detonation in Km (could be underground or above ground) -- please note that positive = depth (below ground), while negative = height (above ground) |
|yield_lower |double | Explosion yield lower estimate in kilotons of TNT |
|yield_upper |double | Explosion yield upper estimate in kilotons of TNT |
|purpose |character | Purpose of detonation: COMBAT (WWII bombs dropped over Japan), FMS (Soviet test, study phenomenon of nuclear explosion), ME (Military Exercise), PNE (Peaceful nuclear explosion), SAM (Soviet test, accidental mode/emergency), SSE (French/US tests - testing safety of nuclear weapons in case of accident), TRANSP (Transportation-storage purposes), WE (British, French, US, evaluate effects of nuclear detonation on various targets), WR (Weapons development program) |
|name |character | Name of event or bomb |
|type |character | type - method of deployment -- ATMOSPH (Atmospheric), UG (underground), BALLOON (Balloon drop), AIRDROP (Airplane deployed), ROCKET (Rocket deployed), TOWER (deplyed at top of constructed tower), WATERSURFACE (on surface of body of water), BARGE (on barge boat), SURFACE (on surface or in shallow crater), UW (Underwater), SHAFT (Vertical Shaft underground), TUNNEL/GALLERY (Horizontal tunnel) |

View file

@ -1,7 +1,7 @@
--- ---
title: Nuclear Explosions title: Nuclear Explosions
author: Marty Oehme subtitle: "Using python polars and seaborn to visualize global detonations"
output-dir: out description: "Using python polars and seaborn to visualize global detonations"
references: references:
- type: techreport - type: techreport
id: Bergkvist2000 id: Bergkvist2000
@ -18,16 +18,10 @@ references:
title: "Nuclear Explosions 1945 - 1998" title: "Nuclear Explosions 1945 - 1998"
page: 1-42 page: 1-42
issn: 1104-9154 issn: 1104-9154
format: pubDate: "2024-07-03T18:36:26"
html: weight: 10
toc: true tags:
code-fold: true - python
typst:
toc: true
echo: false
docx:
toc: true
echo: false
--- ---
```{python} ```{python}
@ -43,7 +37,21 @@ from matplotlib import pyplot as plt
sns.set_theme(style="darkgrid") sns.set_theme(style="darkgrid")
sns.set_context("notebook") sns.set_context("notebook")
cp=sns.color_palette()
country_colors = {
"US": cp[0],
"USSR": cp[3],
"France": cp[6],
"UK": cp[5],
"China": cp[4],
"India": cp[1],
"Pakistan": cp[2],
}
```
```{python}
# | label: data-prep
# | code-fold: true
schema_overrides = ( schema_overrides = (
{ {
col: pl.Categorical col: pl.Categorical
@ -53,13 +61,31 @@ schema_overrides = (
| {col: pl.String for col in ["year", "name"]} | {col: pl.String for col in ["year", "name"]}
) )
cty_alias = {
"PAKIST": "Pakistan",
"FRANCE": "France",
"CHINA": "China",
"INDIA": "India",
"USA": "US",
}
def cty_replace(name: str) -> str:
if name in cty_alias:
return cty_alias[name]
return name
df = ( df = (
pl.read_csv( pl.read_csv(
"data/nuclear_explosions.csv", "data/nuclear_explosions.csv",
schema_overrides=schema_overrides, schema_overrides=schema_overrides,
null_values=["NA"], null_values=["NA"],
) )
.with_columns(date=pl.col("year").str.strptime(pl.Date, "%Y")) .with_columns(
date=pl.col("year").str.strptime(pl.Date, "%Y"),
country=pl.col("country").map_elements(cty_replace, return_dtype=pl.String),
)
.with_columns(year=pl.col("date").dt.year().cast(pl.Int32)) .with_columns(year=pl.col("date").dt.year().cast(pl.Int32))
) )
``` ```
@ -69,7 +95,7 @@ df = (
The following is a re-creation and expansion of some of the graphs found in the The following is a re-creation and expansion of some of the graphs found in the
@Bergkvist2000 produced report on nuclear explosions between 1945 and 1998. It @Bergkvist2000 produced report on nuclear explosions between 1945 and 1998. It
is primarily a reproduction of key plots from the original report. is primarily a reproduction of key plots from the original report.
Additionally, it serves as a exercise in plotting with the python library Additionally, it serves as an exercise in plotting with the python library
seaborn and the underlying matplotlib. Lastly, it approaches some less well seaborn and the underlying matplotlib. Lastly, it approaches some less well
tread territory for data science in the python universe as it uses the python tread territory for data science in the python universe as it uses the python
library polars-rs for data loading and transformation. All the code used to library polars-rs for data loading and transformation. All the code used to
@ -77,9 +103,9 @@ transform the data and create the plots is available directly within the full
text document, and separately as well. PDF and Docx formats are available with text document, and separately as well. PDF and Docx formats are available with
the plotting results only. the plotting results only.
Their original purpose was the collection of a long list of all the nuclear The authors' original purpose was the collection of a long list of all the
explosions occurring between those years, as well as analysing the responsible nuclear explosions occurring between those years, as well as analysing the
nations, tracking the types and purposes of the explosions, as well as responsible nations, tracking the types and purposes of the explosions and
connecting the rise and fall of nuclear explosion numbers to historical events connecting the rise and fall of nuclear explosion numbers to historical events
throughout. throughout.
@ -90,13 +116,13 @@ throughout.
## Nuclear devices ## Nuclear devices
There are two main kinds of nuclear device: those based entirely, on fission, There are two main kinds of nuclear device: those based entirely, on fission,
or the splitting of heavy atomic nucleii (previously known as atomic devices) or the splitting of heavy atomic nuclei (previously known as atomic devices)
and those in which the main energy is obtained by means of fusion, or of -light and those in which the main energy is obtained by means of fusion, or of -light
atomic nucleii (hydrogen or thermonuclear devices). A fusion explosion must atomic nuclei (hydrogen or thermonuclear devices). A fusion explosion must
however be initiated with the help of a fission device. The strength of a however be initiated with the help of a fission device. The strength of a
fusion explosion can be practically unlimited. The explosive power of a fusion explosion can be practically unlimited. The explosive power of a
nuclear explosion is expressed in ktlotons, (kt) or megatons (Mt), which nuclear explosion is expressed in kilotons, (kt) or megatons (Mt), which
correspond to 1000 and i million'tonnes, of conventional explosive (TNT), correspond to 1000 and 1 million tonnes, of conventional explosive (TNT),
respectively. respectively.
[@Bergkvist2000, 6] [@Bergkvist2000, 6]
@ -108,8 +134,9 @@ each country had explode, seen in @tbl-yields.
```{python} ```{python}
# | label: tbl-yields # | label: tbl-yields
# | tbl-cap: "Total number and yields of explosions" # | tbl-cap: "Total number and yields of explosions"
# | output: asis
from great_tables import GT, md from great_tables import GT
df_yields = ( df_yields = (
df.select(["country", "id_no", "yield_lower", "yield_upper"]) df.select(["country", "id_no", "yield_lower", "yield_upper"])
@ -119,11 +146,17 @@ df_yields = (
pl.col("id_no").len().alias("count"), pl.col("id_no").len().alias("count"),
pl.col("yield_avg").sum(), pl.col("yield_avg").sum(),
) )
# .with_columns(country=pl.col("country").cast(pl.String).str.to_titlecase()) .with_columns(yield_per_ex=pl.col("yield_avg") / pl.col("count"))
.sort("count", descending=True) .sort("count", descending=True)
) )
( us_row = df_yields.filter(pl.col("country") == "US")
yields_above_us = df_yields.filter(
pl.col("yield_per_ex") > us_row["yield_per_ex"]
).sort("yield_per_ex", descending=True)
assert len(yields_above_us) == 3, "Yield per explosion desc needs updating!"
tab=(
GT(df_yields) GT(df_yields)
.tab_source_note( .tab_source_note(
source_note="Source: Author's elaboration based on Bergkvist and Ferm (2000)." source_note="Source: Author's elaboration based on Bergkvist and Ferm (2000)."
@ -131,22 +164,33 @@ df_yields = (
.tab_spanner(label="Totals", columns=["count", "yield_avg"]) .tab_spanner(label="Totals", columns=["count", "yield_avg"])
.tab_stub(rowname_col="country") .tab_stub(rowname_col="country")
.tab_stubhead(label="Country") .tab_stubhead(label="Country")
.cols_label( .cols_label(count="Count", yield_avg="Yield in kt", yield_per_ex="Yield average")
count="Count",
yield_avg="Yield in kt",
)
.fmt_integer(columns="count") .fmt_integer(columns="count")
.fmt_number(columns="yield_avg", decimals=1) .fmt_number(columns="yield_avg", decimals=1)
.fmt_number(columns="yield_per_ex", decimals=1)
) )
del df_yields
tab
``` ```
It is interesting to note that while the US undoubtedly had the highest raw
number of explosions, it did not, in fact, output the highest estimated
detonation yields.
In fact, `{python} len(yields_above_us)` countries have a higher average
explosion yield per detonation than the US:
`{python} yields_above_us[0]["country"].item()` leads with an average of
`{python} f"{yields_above_us[0]['yield_per_ex'].item():.2f}"` kt,
before
`{python} yields_above_us[1]["country"].item()` with an average of
`{python} f"{yields_above_us[1]['yield_per_ex'].item():.2f}"` kt.
## Numbers over time ## Numbers over time
When investigating the nuclear explosions in the world, let us first start by In the examination of global nuclear detonations, our initial focus shall be
looking at how many explosions occurred each year in total. This hides the quantifying the annual incidence of the events in aggregate. While it obscures
specific details of who was responsible and which types were involved but the specific details of the responsible nations and which diversity of types
instead paints a much stronger picture of the overall dimension of nuclear tested, it instead paints a much stronger picture of the overall abstracted
testing, as can be seen in @fig-total. dimension of nuclear testing throughout history, as depicted in @fig-total.
```{python} ```{python}
# | label: fig-total # | label: fig-total
@ -171,17 +215,29 @@ with sns.axes_style(
del per_year del per_year
``` ```
As we can see, the numbers of explosions rise increasingly towards 1957 and As we can see, the number of explosions rises increasingly towards 1957 and
sharply until 1958, before dropping off for a year in 1959. The reasons for sharply until 1958, before dropping off for a year in 1959. The reason for this
this drop are not entirely clear, but it is very likely that the data are drop should primarily be found in the start of the 'Treaty of Test Ban' which
simply missing for these years. put limits and restraints on the testing of above-ground nuclear armaments, as
<!-- FIXME: The reasons for this are a non-proliferation pact, in article --> discussed in the original article. Above all the contract signals the
prohibition of radioactive debris to fall beyond a nation's respective
territorial bounds.
However, this contract should perhaps not be viewed as the only reason: With
political and cultural shifts throughout the late 1950s and early 1960s
increasingly focusing on the fallout and horror of nuclear warfare a burgeoning
public opposition to nuclear testing and instead a push towards disarmament was
taking hold. The increased focus on the space race between the US and USSR may
have detracted from the available funds, human resources and agenda attention
for nuclear testing. Lastly, with nuclear testing policies strongly shaped by
the political dynamics of the Cold War, a period of improved diplomatic
relations such as the late 1950s prior to the Cuban missile crisis may directly
affect the output of nuclear testing facilities between various powers.
<!-- TODO: Extract exact numbers from data on-the-fly --> <!-- TODO: Extract exact numbers from data on-the-fly -->
There is another, very steep, rise in 1962 with over 175 recorded explosions, There is another, very steep, rise in 1962 with over 175 recorded explosions,
before an even sharper drop-off the following year down to just 50 explosions. before an even sharper drop-off the following year down to just 50 explosions.
Afterward the changes appear less sharp and the changes remain between 77 and
Afterwards the changes appear less sharp and the changes remain between 77 and
24 explosions per year, with a slight downward tendency. 24 explosions per year, with a slight downward tendency.
While these numbers show the overall proliferation of nuclear power, let us now While these numbers show the overall proliferation of nuclear power, let us now
@ -191,6 +247,7 @@ of explosions over time by country can be seen in @fig-percountry.
```{python} ```{python}
# | label: fig-percountry # | label: fig-percountry
# | fig-cap: "Nuclear explosions by country, 1945-98" # | fig-cap: "Nuclear explosions by country, 1945-98"
keys = df.select("date").unique().join(df.select("country").unique(), how="cross") keys = df.select("date").unique().join(df.select("country").unique(), how="cross")
per_country = keys.join( per_country = keys.join(
df.group_by(["date", "country"], maintain_order=True).len(), df.group_by(["date", "country"], maintain_order=True).len(),
@ -199,7 +256,7 @@ per_country = keys.join(
coalesce=True, coalesce=True,
).with_columns(pl.col("len").fill_null(0)) ).with_columns(pl.col("len").fill_null(0))
g = sns.lineplot(data=per_country, x="date", y="len", hue="country") g = sns.lineplot(data=per_country, x="date", y="len", hue="country", palette=country_colors)
g.set_xlabel("Year") g.set_xlabel("Year")
g.set_ylabel("Count") g.set_ylabel("Count")
plt.setp( plt.setp(
@ -211,14 +268,14 @@ del per_country
Once again we can see the visibly steep ramp-up to 1962, though it becomes Once again we can see the visibly steep ramp-up to 1962, though it becomes
clear that this was driven both by the USSR and the US. Of course the graph clear that this was driven both by the USSR and the US. Of course the graph
also makes visible the sheer unmatched number of explosions emenating from both also makes visible the sheer unmatched number of explosions emanating from both
of the countries, with only France catching up to the US numbers and China of the countries, with only France catching up to the US numbers and China
ultimately overtaking them in the 1990s. ultimately overtaking them in the 1990s.
However, here it also becomes more clear how the UK was responsible for some However, here it also becomes more clear how the UK was responsible for some
early explosions in the late 1950s and early 1960s already, as well as the rise early explosions in the late 1950s and early 1960s already, as well as the rise
in France's nuclear testing from the early 1960s onwards to around 1980, before in France's nuclear testing from the early 1960s onwards to around 1980, before
slowly decreasing in intensity afterwards. slowly decreasing in intensity afterward.
Let us turn to a cross-cut through the explosions in @fig-groundlevel, focusing Let us turn to a cross-cut through the explosions in @fig-groundlevel, focusing
on the number of explosions that have occurred underground and above-ground on the number of explosions that have occurred underground and above-ground
@ -273,6 +330,7 @@ with sns.axes_style("darkgrid", {"xtick.bottom": True, "ytick.left": True}):
hue="country", hue="country",
multiple="stack", multiple="stack",
binwidth=365, binwidth=365,
palette=country_colors,
) )
g.xaxis.set_major_locator(mdates.YearLocator(base=5)) g.xaxis.set_major_locator(mdates.YearLocator(base=5))
@ -293,25 +351,23 @@ shift from above-ground to underground tests, starting with the year 1962.
## Locations ## Locations
Finally, let's view a map of the world with the explosions marked. Finally, let's view a map of the world with the explosions marked, separated by country.
::: {.content-visible when-format="html"}
Hovering over individual explosions will show their year
while a click will open more information in a panel.
The map can be seen in @fig-worldmap-html.
:::
::: {.content-visible unless-format="html"}
The map can be seen in @fig-worldmap-static.
:::
```{python} ```{python}
# | label: fig-worldmap # | label: worldmap-setup
# | fig-cap: "World map of nuclear explosions, 1945-98" # | output: false
import folium import folium
import geopandas as gpd import geopandas as gpd
from shapely.geometry import Point
def set_style() -> pl.Expr:
return (
pl.when(pl.col("country") == "USSR")
.then(pl.lit({"color": "red"}, allow_object=True))
.otherwise(pl.lit({"color": "blue"}, allow_object=True))
)
geom = [Point(xy) for xy in zip(df["longitude"], df["latitude"])]
# df_pd = df.with_columns(style=set_style()).to_pandas().set_index("date")
df_pd = df.with_columns().to_pandas().set_index("date") df_pd = df.with_columns().to_pandas().set_index("date")
gdf = gpd.GeoDataFrame( gdf = gpd.GeoDataFrame(
df_pd, df_pd,
@ -320,25 +376,18 @@ gdf = gpd.GeoDataFrame(
) )
del df_pd del df_pd
country_colors = { def rgb_to_hex(rgb: tuple[float,float,float]) -> str:
"USA": "darkblue", return "#" + "".join([format(int(c*255), '02x') for c in rgb])
"USSR": "darkred",
"FRANCE": "pink",
"UK": "black",
"CHINA": "purple",
"INDIA": "orange",
"PAKIST": "green",
}
m = folium.Map(tiles="cartodb positron") m = folium.Map(tiles="cartodb positron")
for country in country_colors.keys(): for country in country_colors.keys():
fg = folium.FeatureGroup(name=country, show=True).add_to(m) fg = folium.FeatureGroup(name=country, show=True).add_to(m)
folium.GeoJson( folium.GeoJson(
gdf[gdf["country"].str.contains(country)], gdf[gdf["country"] == country],
name="Nuclear Explosions", name="Nuclear Explosions",
marker=folium.Circle(radius=3, fill_opacity=0.4), marker=folium.Circle(radius=3, fill_opacity=0.4),
style_function=lambda x: { style_function=lambda x: {
"color": country_colors[x["properties"]["country"]], "color": rgb_to_hex(country_colors[x["properties"]["country"]]),
"radius": ( "radius": (
x["properties"]["magnitude_body"] x["properties"]["magnitude_body"]
if x["properties"]["magnitude_body"] > 0 if x["properties"]["magnitude_body"] > 0
@ -368,15 +417,60 @@ for country in country_colors.keys():
), ),
).add_to(fg) ).add_to(fg)
folium.LayerControl().add_to(m) folium.LayerControl().add_to(m)
```
::: {.content-visible when-format="html"}
```{python}
# | label: fig-worldmap-html
# | fig-cap: World map of nuclear explosions, 1945-98
m m
``` ```
That is all for now. :::
There are undoubtedly more explorations to undertake,
but this is it for the time being.
<!-- Ideas TODO: ::: {.content-visible unless-format="html" width=80%}
- do not just use 'count' of explosions but yields
- compare number to yields for ctrys ```{python}
- count up total number per country in table # | label: fig-worldmap-static
--> # | fig-cap: World map of nuclear explosions, 1945-98
# ENSURE SELENIUM IS INSTALLED
from PIL import Image
from IPython.display import Image as IImage
import io
img = m._to_png()
bimg = io.BytesIO(img)
Image.open(bimg).save("map.png")
IImage(url="map.png")
```
:::
::: {.callout-warning .content-visible when-format="markdown"}
Interactive maps not working
Unfortunately, as of right now folium maps rendered within a quarto document do
not seem to translate terribly well into an astro blog such as this.
This is why, for now, there is only a static image here.
This is very sad, but for the time being feel free to download and peruse
the ipynb notebook [here](./index.ipynb), or the [pdf](./index.pdf)
or [docx](./index.docx) versions.
:::
While there are undoubtedly more aspects of the data that provide interesting
patterns for analysis, this shall be the extent of review for the time being
for this reproduction.
We can see how the combination of python polars and seaborn makes the process
relatively approachable, understandable and, combined with the rendering output
by quarto, fully reproducible.
Additionally, we can see how additional projects can be included to produce
interactive graphs and maps with tools such as folium and geopandas.
## References
::: {#refs}
:::

157
meta.md Normal file
View file

@ -0,0 +1,157 @@
This page documents some meta observations about my time recreating the nuclear explosions in this post,
mostly some little tips to work well with python polars and seaborn, or little tricks to integrate them and geopandas visualizations.
## From a lat/long polars dataframe to geopandas
To go from a polars frame to one we can use for GIS operations with geopandas is fairly simple:
We first move from a polars to an indexed pandas frame, in this case I have indexed on the date of each explosion.
We can use this intermediate dataframe to fill a geopandas frame which is built from the points of lat/long columns,
using the `gpd.points_from_xy()` function to create spatial `Point` objects from simple pandas Series.
Finally, we need to set a 'crs=' mapping for which this visualization simply uses the `EPSG:4326` global offsets (will generally be the same for global mappings).
```python
df_pd = df.with_columns().to_pandas().set_index("date")
gdf = gpd.GeoDataFrame(
df_pd,
crs="EPSG:4326",
geometry=gpd.points_from_xy(x=df_pd["longitude"], y=df_pd["latitude"]),
)
del df_pd
```
## Keeping the same seaborn color palette for the same categories
For the analysis, I have multiple plots which distinguish between the different countries undertaking nuclear detonations.
The country category thus appears repeatedly, and with static values (i.e. it will always contain 'US', 'USSR', 'China', 'France' and so on).
Now, seaborn has very nice functionality to automatically give different hues to categories like these in plots,
but how do we ensure that the hues given remain _the same_ throughout?
One way of achieving it would be to keep the order of categories the same throughout all plots.
However, this seems hidden,
often adds to the strain of just getting to the right data frame calculations,
appears a little too magic for my liking and, to top it off,
is even harder to achieve with some of polars' parallelized operations.
Instead we can explicitly map our categories to colors.
In my case, my categories for this example will always be the different countries:
```python
country_colors = {
"US": 'blue',
"USSR": 'red',
"France": 'pink'
"UK": 'black'
"China": 'purple'
"India": 'orange'
"Pakistan": 'green'
}
```
These are colors seaborn understands and can be given to a plot via the keyword option `palette=country_colors` which will pass along the colors above to the respective plot.
However, one advantage of seaborn is its nice in-built color schemes (i.e. palettes) which we will not make use of if we instead hard-code our color preferences like this.
Instead, we can directly access seaborn's color palette with `sns.color_palette()` which we can then use to explicitly map our categories to colors:
```python
cp=sns.color_palette()
country_colors = {
"US": cp[0],
"USSR": cp[3],
"France": cp[6],
"UK": cp[5],
"China": cp[4],
"India": cp[1],
"Pakistan": cp[2],
}
```
This mapping is passed exactly the same way as the other.
Now, we've ensured that colors in plots (that have the countries as hue category) will all have the same color for the same country throughout.
At the same time we have a single spot in which we can change the actual color theme seaborn uses, instead of hard-coding our preferences throughout.
This I find very useful when creating analyses with similar categories throughout,
In the nuclear analysis there is a folium geospatial (GeoJson) map at the very end which uses colors to distinguish between the countries once again.
Here we can make use of almost the same strategy, with the one caveat that folium expects the colors in hexadecimal format, while seaborn internally stores them as RGB value tuples.
What we can do, then is to use a simple translation function which converts from one format to the other on the fly,
and inject that into the map creation method of folium:
```python
def rgb_to_hex(rgb: tuple[float,float,float]) -> str:
return "#" + "".join([format(int(c*255), '02x') for c in rgb])
map = folium.Map(tiles="cartodb positron")
folium.GeoJson(
gdf,
name="Nuclear Explosions",
marker=folium.Circle(radius=3, fill_opacity=0.4),
style_function=lambda x: {
"color": rgb_to_hex(country_colors[x["properties"]["country"]]),
"radius": (
x["properties"]["magnitude_body"]
if x["properties"]["magnitude_body"] > 0
else 1.0
)
* 10,
},
).add_to(map)
```
## Using dictionary keys to create folium map layers
As a bonus we can even use our color category keys to create different layers on the folium map which can be turned on and off individually.
Thus we can decide which country's detonations we want to visualize.
Of course, we could also create these keys dynamically from the polars dataframe by extracting the `.unique()` elements of its "country" column (even though we use pandas geoframe for display),
but here I am using my explicit mapping instead.
The implementation works already with two additional lines and a loop,
by looping through our keys and adding a new layer for each one,
filtering out all the rows which do not exactly match the key using a pandas filter.
```python
m = folium.Map(tiles="cartodb positron")
for country in country_colors.keys():
fg = folium.FeatureGroup(name=country, show=True).add_to(m)
folium.GeoJson(
gdf[gdf["country"] == country],
name="Nuclear Explosions",
marker=folium.Circle(radius=3, fill_opacity=0.4),
style_function=lambda x: {
"color": rgb_to_hex(country_colors[x["properties"]["country"]]),
"radius": (
x["properties"]["magnitude_body"]
if x["properties"]["magnitude_body"] > 0
else 1.0
)
* 10,
},
).add_to(fg)
folium.LayerControl().add_to(m)
```
## Remaining issues
While working with polars is wonderful and seaborn takes a lot of the stress of creating half-way nicely formatted plots out of mind while first creating them,
some pain points remain.
While I am cautiously optimistic, seaborn's 'objects-style' interface still remains woefully undercooked.
It is already possible to create some basic plots with it and its declarative style is wonderful
(as in, it really matches the mental model I have of drawing individual plot elements into a coherent whole).
But for anything more complex --- which in my opinion is exactly where this interface will really shine ---
it remains out of reach because of missing methods and implementations.
This is, of course, ideally just a temporary issue until the implementation gets better,
but until then we are still stuck with the more strange mish-mash of seaborn simplicity with matplotlib exactness,
and having to know when to leave the former behind and delve into the arcane API of the latter.
Additionally, when combined with quarto for publishing some more pain points appear.
One that has been true for the longest time, and will likely remain so for the foreseeable future,
is that tables beyond a certain complexity are just _painful_ in quarto multi-output publishing.
This project made use the fantastic python library [great tables]() which indeed lives up to its name and produces absolutely great tables with very little effort.
However, it primarily targets the html format.
Getting this format into shape for quarto to then translate it into the pandoc AST and ultimately whatever format is not pretty.
For example LaTeX routinely just crashes instead of rendering the table correctly into a PDF file.

262
poetry.lock generated
View file

@ -2028,6 +2028,20 @@ files = [
{file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"}, {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"},
] ]
[[package]]
name = "outcome"
version = "1.3.0.post0"
description = "Capture the outcome of Python function calls."
optional = false
python-versions = ">=3.7"
files = [
{file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"},
{file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"},
]
[package.dependencies]
attrs = ">=19.2.0"
[[package]] [[package]]
name = "overrides" name = "overrides"
version = "7.7.0" version = "7.7.0"
@ -2161,84 +2175,95 @@ ptyprocess = ">=0.5"
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "10.3.0" version = "10.4.0"
description = "Python Imaging Library (Fork)" description = "Python Imaging Library (Fork)"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"},
{file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"},
{file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"},
{file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"},
{file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"},
{file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"},
{file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"},
{file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"},
{file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"},
{file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"},
{file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"},
{file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"},
{file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"},
{file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"},
{file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"},
{file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"},
{file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"},
{file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"},
{file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"},
{file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"},
{file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"},
{file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"},
{file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"},
{file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"},
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"},
{file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"},
] ]
[package.extras] [package.extras]
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
fpx = ["olefile"] fpx = ["olefile"]
mic = ["olefile"] mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
@ -2563,6 +2588,18 @@ files = [
[package.dependencies] [package.dependencies]
certifi = "*" certifi = "*"
[[package]]
name = "pysocks"
version = "1.7.1"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@ -3018,6 +3055,25 @@ dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest
docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"]
stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
[[package]]
name = "selenium"
version = "4.22.0"
description = "Official Python bindings for Selenium WebDriver"
optional = false
python-versions = ">=3.8"
files = [
{file = "selenium-4.22.0-py3-none-any.whl", hash = "sha256:e424991196e9857e19bf04fe5c1c0a4aac076794ff5e74615b1124e729d93104"},
{file = "selenium-4.22.0.tar.gz", hash = "sha256:903c8c9d61b3eea6fcc9809dc7d9377e04e2ac87709876542cc8f863e482c4ce"},
]
[package.dependencies]
certifi = ">=2021.10.8"
trio = ">=0.17,<1.0"
trio-websocket = ">=0.9,<1.0"
typing_extensions = ">=4.9.0"
urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
websocket-client = ">=1.8.0"
[[package]] [[package]]
name = "send2trash" name = "send2trash"
version = "1.8.3" version = "1.8.3"
@ -3128,6 +3184,17 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
] ]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false
python-versions = "*"
files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
]
[[package]] [[package]]
name = "soupsieve" name = "soupsieve"
version = "2.5" version = "2.5"
@ -3232,6 +3299,40 @@ files = [
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
[[package]]
name = "trio"
version = "0.25.1"
description = "A friendly Python library for async concurrency and I/O"
optional = false
python-versions = ">=3.8"
files = [
{file = "trio-0.25.1-py3-none-any.whl", hash = "sha256:e42617ba091e7b2e50c899052e83a3c403101841de925187f61e7b7eaebdf3fb"},
{file = "trio-0.25.1.tar.gz", hash = "sha256:9f5314f014ea3af489e77b001861c535005c3858d38ec46b6b071ebfa339d7fb"},
]
[package.dependencies]
attrs = ">=23.2.0"
cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""}
idna = "*"
outcome = "*"
sniffio = ">=1.3.0"
sortedcontainers = "*"
[[package]]
name = "trio-websocket"
version = "0.11.1"
description = "WebSocket library for Trio"
optional = false
python-versions = ">=3.7"
files = [
{file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"},
{file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"},
]
[package.dependencies]
trio = ">=0.11"
wsproto = ">=0.14"
[[package]] [[package]]
name = "types-python-dateutil" name = "types-python-dateutil"
version = "2.9.0.20240316" version = "2.9.0.20240316"
@ -3290,6 +3391,9 @@ files = [
{file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
] ]
[package.dependencies]
pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""}
[package.extras] [package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"] h2 = ["h2 (>=4,<5)"]
@ -3360,6 +3464,20 @@ files = [
{file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"}, {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"},
] ]
[[package]]
name = "wsproto"
version = "1.2.0"
description = "WebSockets state-machine based protocol implementation"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"},
{file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"},
]
[package.dependencies]
h11 = ">=0.9.0,<1"
[[package]] [[package]]
name = "xyzservices" name = "xyzservices"
version = "2024.6.0" version = "2024.6.0"
@ -3389,4 +3507,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "d070f377faf4ef9fdaf2f401e4af39ccf8d5989219c9b572a3e97d278d6cd438" content-hash = "40151747be7ccbaf8d89b9d49c90416afcd1483bfd8a62f14de0dbc693aa8df2"

View file

@ -15,6 +15,8 @@ pyarrow = "^16.1.0"
great-tables = "^0.9.0" great-tables = "^0.9.0"
geopandas = "^0.14.4" geopandas = "^0.14.4"
folium = "^0.17.0" folium = "^0.17.0"
selenium = "^4.22.0"
pillow = "^10.4.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]

26
tools/fix-astro-img.py Normal file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# Replaces all img tags with markdown ![image-tags](./aiming/at/output/dir)
import re
import os
import sys
if not os.getenv("QUARTO_PROJECT_RENDER_ALL"):
sys.exit(0)
q_output_dir = os.getenv("QUARTO_PROJECT_OUTPUT_DIR")
q_output_files = os.getenv("QUARTO_PROJECT_OUTPUT_FILES")
if not q_output_files:
sys.exit(1)
for fname in q_output_files.splitlines():
if not fname.endswith(".md"):
continue
with open(fname, "r") as f:
content = f.read()
modified = re.sub(r'<img src="(.+?)".*/>', r"![fig](./\1)", content)
with open(fname, "w") as f:
f.write(modified)