diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 7b7ae55..6799772 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -1,6 +1,7 @@ project: type: website +notebook-links: false format: html: @@ -20,11 +21,7 @@ website: - text: Get Started file: get-started/index.qmd - text: Examples - menu: - - demos/twitter-followers.qmd - - demos/pypi-downloads/index.qmd - - demos/great-tables.qmd - - demos/cookbook/index.qmd + file: demos/index.qmd - text: Reference file: reference/index.qmd #- href: reference/index.qmd diff --git a/docs/demos/cookbook/images/beaver.png b/docs/demos/cookbook/images/beaver.png new file mode 100644 index 0000000..f035ecd Binary files /dev/null and b/docs/demos/cookbook/images/beaver.png differ diff --git a/docs/demos/cookbook/images/cow.png b/docs/demos/cookbook/images/cow.png new file mode 100644 index 0000000..f24a03e Binary files /dev/null and b/docs/demos/cookbook/images/cow.png differ diff --git a/docs/demos/cookbook/images/goat.png b/docs/demos/cookbook/images/goat.png new file mode 100644 index 0000000..b25cefb Binary files /dev/null and b/docs/demos/cookbook/images/goat.png differ diff --git a/docs/demos/cookbook/images/wolf.png b/docs/demos/cookbook/images/wolf.png new file mode 100644 index 0000000..9b1d09c Binary files /dev/null and b/docs/demos/cookbook/images/wolf.png differ diff --git a/docs/demos/cookbook/index.qmd b/docs/demos/cookbook/index.qmd new file mode 100644 index 0000000..c0009c9 --- /dev/null +++ b/docs/demos/cookbook/index.qmd @@ -0,0 +1,645 @@ +--- +title: Cookbook +resources: + - "images/beaver.png" + - "images/cow.png" + - "images/goat.png" + - "images/wolf.png" +jupyter: python3 + +--- + + +```{python} +# | label: setup +from reactable import embed_css + +embed_css() +``` + +## Insert links + + +```{python} +# | label: insert-links +import polars as pl +import htmltools as ht + +from reactable import Reactable, Column + +data = pl.DataFrame( + { + "Address": ["https://google.com", "https://yahoo.com", "https://duckduckgo.com"], + "Site": ["Google", "Yahoo", "DuckDuckGo"], + } +) + +Reactable( + data, + columns={ + "Address": Column( + cell=lambda info: ht.a(info.value, href=info.value, target="_blank"), + ), + "Site": Column( + html=True, + cell=lambda info: f'{info.value}', + ), + }, +) + +``` + +## Format color scales + +### Single column + +```{python} +# | label: format-color-scales +from reactable import Reactable, Column, CellInfo +from reactable.data import cars_93 + +from mizani.palettes import gradient_n_pal + + +data = cars_93[["manufacturer", "model", "price"]] + +pal = gradient_n_pal(["#ffe4cc", "#ff9500"]) + + +def fmt_fill(ci: CellInfo): + val_range = max(data["price"]) - min(data["price"]) + normalized = (ci.value - min(data["price"])) / val_range + return {"background": pal(normalized)} + + +Reactable( + data, + columns={"price": Column(style=fmt_fill)}, + default_page_size=5, +) +``` + +### Grid + +```{python} +# | label: format-color-scales2 +from reactable import Reactable, Column, ColFormat, CellInfo +from reactable.data import nottem + +from mizani.palettes import gradient_n_pal + +pal = gradient_n_pal(["#7fb7d7", "#ffffbf", "#fc8d59"]) + +# flatten out monthly columns into a single list +# this lets us calculate the overall min and max +flat_vals = sum(nottem[:, 1:].to_dict().values(), []) + + +def fmt_fill(ci: CellInfo): + if not isinstance(ci.value, float): + return + + val_range = max(flat_vals) - min(flat_vals) + normalized = (ci.value - min(flat_vals)) / val_range + color = pal(normalized) + + return {"background": color} + + +Reactable( + nottem, + default_col_def=Column( + style=fmt_fill, + format=ColFormat(digits=1), + min_width=50, + ), + # TODO: make year rowname + columns={ + "year": Column( + format=ColFormat(digits=0), + row_header=True, + ), + }, + bordered=True, + default_page_size=5, +) + +``` + +## Format changes + +```{python} +# | label: format-changes +import polars as pl +from reactable import Reactable, Column, CellInfo + +data = pl.DataFrame( + { + "Symbol": ["GOOG", "FB", "AMZN", "NFLX", "TSLA"], + "Price": [1265.13, 187.89, 1761.33, 276.82, 328.13], + "Change": [4.14, 1.51, -19.45, 5.32, -12.45], + } +) + +Reactable( + data, + columns={ + "Change": Column( + # TODO: we should stringify, so people can + # return ci.value directly + cell=lambda ci: f"+{ci.value}" if ci.value >= 0 else str(ci.value), + style=lambda ci: { + "font-weight": 600, + "color": "#008000" if ci.value > 0 else "#e00000", + }, + ) + }, +) +``` + +## Format tags and badges + + +```{python} +import polars as pl +from reactable import Reactable, Column, CellInfo + +orders = pl.DataFrame( + { + "Order": [2300, 2301, 2302, 2303, 2304], + "Created": ["2019-04-01", "2019-04-02", "2019-04-03", "2019-04-04", "2019-04-05"], + # "Customer": ["Degas", "Cezanne", "Monet", "Manet", "Renoir"], + "Status": ["Pending", "Paid", "Canceled", "Pending", "Paid"], + } +) + +tbl = Reactable( + orders, + columns={ + "Status": Column( + cell=lambda ci: f'{ci.value}', + html=True, + ) + }, +) +``` + +```{python} +# | label: format-tags +from IPython.display import display, HTML + +display( + HTML( + """ + +""" + ) +) + +tbl +``` + + + + + +```{python} +# | label: format-badge +import htmltools + + +def status_badge(color="#aaa", width="0.55rem", height=None): + height = height or width + return htmltools.span( + style=( + "display: inline-block;" + "margin-right: 0.5rem;" + f"width: {width};" + f"height: {height};" + f"background-color: {color};" + "border-radius: 50%" + ) + ) + + +status_hsl = { + "Paid": "hsl(214, 45%, 50%)", + "Pending": "hsl(30, 97%, 70%)", + "Canceled": "hsl(3, 69%, 50%)", +} + +Reactable( + orders, + columns={ + "Status": Column( + cell=lambda ci: htmltools.div(status_badge(color=status_hsl[ci.value]), str(ci.value)), + html=True, + ) + }, +) +``` + +## Bar charts + + +```{python} +# | label: bar-charts +import htmltools + +from reactable import Reactable, Column, CellInfo +from reactable.data import cars_93 + +data = cars_93[:5, ["make", "mpg_city", "mpg_highway"]] + + +def html_barchart(label, width="100%", height="1rem", fill="#00bfc4", background=None): + """Create general purpose html fill bar.""" + + bar = htmltools.div(style=f"background: {fill}; width: {width}; height: {height}") + chart = htmltools.div( + bar, + style=htmltools.css( + flex_grow=1, + margin_left="0.5rem", + background=background, + ), + ) + return htmltools.div( + label, + chart, + style=htmltools.css( + display="flex", + align_items="center", + ), + ) + + +def fmt_barchart(ci: CellInfo, **kwargs): + """Format cell value into html fill bar.""" + + width = f"{ci.value / max(data['mpg_city']) * 100}%" + return html_barchart(ci.value, width=width, **kwargs) + + +Reactable( + data, + columns={ + "mpg_city": Column( + name="MPG (city)", + align="left", + cell=fmt_barchart, + ), + "mpg_highway": Column( + name="MPG (highway)", + align="left", + cell=lambda ci: fmt_barchart(ci, fill="#fc5185", background="#e1e1e1"), + ), + }, + default_page_size=5, +) +``` + + + +### Positive and negative values + +TODO + +### Background bar charts + +TODO + +## Embed images + + +```{python} +# | label: embed-images +import polars as pl +import htmltools + +from reactable import Reactable, Column, CellInfo + +data = pl.DataFrame( + { + "Animal": ["beaver", "cow", "wolf", "goat"], + "Body": [1.35, 465, 36.33, 27.66], + "Brain": [8.1, 423, 119.5, 115], + } +) + + +def fmt_image(ci: CellInfo): + image = htmltools.img( + src=f"/demos/cookbook/images/{ci.value}.png", + style="height: 24px;", + alt=ci.value, + ) + return htmltools.TagList( + htmltools.div( + image, + style="display: inline-block; width: 45px;", + ), + ci.value, + ) + + +Reactable( + data, + columns={ + "Animal": Column(cell=fmt_image), + "Body": Column(name="Body (kg)"), + "Brain": Column(name="Brain (g)"), + }, +) + +``` + +Note that this example assumes the images are available (we did that by setting the `resources:` field in quarto). + +## Rating stars + + +```{python} +# | label: rating-stars +# pip install faicons +import polars as pl +import htmltools + +from faicons import icon_svg +from reactable import Reactable, Column, CellInfo + +ratings = pl.DataFrame( + { + "Movie": [ + "Silent Serpent", + "Nowhere to Hyde", + "The Ape-Man Goes to Mars", + "A Menace in Venice", + ], + "Rating": [3.65, 2.35, 4.5, 1.4], + "Votes": [115, 37, 60, 99], + } +) + + +def rating_stars(ci: CellInfo): + to_fill = round(ci.value) + # TODO: how to set aria? + stars = [ + icon_svg( + "star", stroke="orange" if ii <= to_fill else "#edf0f2", stroke_width=100, fill="white" + ) + for ii in range(1, 6) + ] + return htmltools.div(*stars, title="{ci.value} out of 5 stars") + + +Reactable( + ratings, + columns={ + "Rating": Column( + cell=rating_stars, + html=True, + ) + }, +) + + + +``` + + +## Show data from other columns + +```{python} +#| label: show-data-from-other-columns +import htmltools + +from reactable import Reactable, Column, CellInfo +from reactable.data import starwars + +data = starwars[["name", "height", "mass", "gender", "homeworld", "species"]] + + +def fmt_name(ci: CellInfo): + species = data["species"][ci.row_index] + species = species if species is not None else "Unknown" + + return htmltools.div( + htmltools.div(ci.value, style="font-weight: 600;"), + htmltools.div(species, style="font-size: 0.75rem;"), + ) + + +Reactable( + data, + columns={ + "name": Column( + cell=fmt_name, + name="Character", + ), + "species": Column(show=False), + }, + default_col_def=Column(v_align="center"), + default_page_size=6, +) +``` + + +```{python} +from reactable import Reactable, Column, JS +from reactable.data import starwars + +data = starwars[["name", "height", "mass", "gender", "homeworld", "species"]] + +js_name = JS( + """ + function(cellInfo) { + const species = cellInfo.row["species"] || "Unknown" + return ` +
+
${cellInfo.value}
+
${species}
+
+ ` + } + """ +) + +Reactable( + data, + columns={ + "name": Column( + cell=js_name, + html=True, + ), + "species": Column(show=False), + }, + default_col_def=Column(v_align="center"), + default_page_size=6, +) + +``` + +## Total rows + +### Fixed + +```{python} +# | label: total-rows +from reactable import Reactable, Column +from reactable.data import cars_93 + +data = cars_93[["manufacturer", "model", "type", "price"]] + +Reactable( + data, + default_page_size=5, + columns={ + "manufacturer": Column(footer="Total"), + "price": Column(footer=f"${sum(data['price']):.2f}"), + }, + default_col_def=Column(footer_style={"font-weight": "bold"}), +) +``` + +### Dynamic + +```{python} +from reactable import JS + +js_sum_price = JS( + """ + function(column, state) { + let total = 0 + state.sortedData.forEach(function(row) { + total += row[column.id] + }) + return total.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) + } + """ +) + +Reactable( + data, + searchable=True, + default_page_size=5, + min_rows=5, + columns={ + "manufacturer": Column(footer="Total"), + "price": Column(footer=js_sum_price), + }, + default_col_def=Column(footer_style={"font-weight": "bold"}), +) +``` + +## Nested tables + +```{python} +# | label: nested-tables +import polars as pl +import polars.selectors as cs + +from reactable import Reactable, Column +from reactable.data import us_expenditures + +data = ( + us_expenditures.to_polars() + # tidy years from columns into rows + .unpivot(cs.starts_with("19"), index="index") +) + +year_dfs = list(g for k, g in data.group_by("variable")) +summary_df = data.group_by("variable").agg(n=pl.count()) + +Reactable( + summary_df, + # TODO: details should accept a function + details=Column( + details=lambda ri: Reactable(year_dfs[ri.row_index]).to_widget(), + ), +) +``` + +## Units on first row only + +```{python} +# | label: units-on-first-row +from reactable.data import cars_93 +from reactable import Reactable, Column + +data = cars_93[40:44, ["make", "length", "luggage_room"]] + + +def fmt_length(ci): + return f"{ci.value}″" + + +def fmt_ft(ci): + return f"{ci.value}
ft³
" + + +Reactable( + data, + class_="car-specs", + columns={ + "length": Column( + cell=lambda ci: fmt_length(ci) if ci.row_index == 0 else str(ci.value), + class_="number", + ), + "luggage_room": Column( + name="Luggage Room", + cell=lambda ci: fmt_ft(ci) if ci.row_index == 0 else str(ci.value), + html=True, + ), + }, +) +``` + +## Tooltips + +## Highlight cells + +## Highlight columns + +## Highlight rows + +## Highlight sorted headers + +## Highlight sorted columns + +## Borders between groups of data + +## Merge cells + +## Borders between columns + +## Style nested rows + +## Custom fonts + +## Custom sort indicators + diff --git a/docs/demos/demo_snippets/pypi-downloads.qmd b/docs/demos/demo_snippets/pypi-downloads.qmd new file mode 100644 index 0000000..2e03160 --- /dev/null +++ b/docs/demos/demo_snippets/pypi-downloads.qmd @@ -0,0 +1,10 @@ +--- +pagetitle: PyPI Downloads +jupyter: python3 +navbar: false +--- + +:::{.shrink-example} +{{< embed ../pypi-downloads/index.qmd#setup >}} +{{< embed ../pypi-downloads/index.qmd#table >}} +::: \ No newline at end of file diff --git a/docs/demos/demo_snippets/twitter-followers.qmd b/docs/demos/demo_snippets/twitter-followers.qmd new file mode 100644 index 0000000..afe31af --- /dev/null +++ b/docs/demos/demo_snippets/twitter-followers.qmd @@ -0,0 +1,11 @@ +--- +pagetitle: Twitter followers +navbar: false +jupyter: python3 +--- + +:::{.shrink-example} +{{< embed ../twitter-followers.qmd#setup >}} +{{< embed ../twitter-followers.qmd#css >}} +{{< embed ../twitter-followers.qmd#table >}} +::: diff --git a/docs/demos/index.qmd b/docs/demos/index.qmd new file mode 100644 index 0000000..87f2df2 --- /dev/null +++ b/docs/demos/index.qmd @@ -0,0 +1,125 @@ +--- +pagetitle: Examples +notebook-links: false +--- + + + + + + + + +{{< embed cookbook/index.qmd#setup >}} + +:::::: {.column-page} + +## Demos + +::::: {.grid} +:::{.g-col-lg-6 .g-col-12 .example} +### [PyPI downloads](./pypi-downloads) + +::: + +:::{.g-col-lg-6 .g-col-12 .example} +### [Twitter followers](./twitter-followers.qmd) + +::: + +::::: + +::::: {.grid} + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} + + + +::: + +::::: + + +## Cookbook examples + +::::: {.grid} + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Insert links](./cookbook/index.qmd#insert-links) +{{< embed cookbook/index.qmd#insert-links >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Format colors](./cookbook/index.qmd#single-column) +{{< embed cookbook/index.qmd#format-color-scales >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Format colors (2)](./cookbook/index.qmd#grid) +{{< embed cookbook/index.qmd#format-color-scales2 >}} +::: + + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Format changes](./cookbook/index.qmd#format-changes) +{{< embed cookbook/index.qmd#format-changes >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Format tags](./cookbook/index.qmd#format-tags-and-badges) +{{< embed cookbook/index.qmd#format-tags >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Format badge](./cookbook/index.qmd#format-tags-and-badges) +{{< embed cookbook/index.qmd#format-badge >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Bar charts](./cookbook/index.qmd#bar-charts) +{{< embed cookbook/index.qmd#bar-charts >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Embed images](./cookbook/index.qmd#embed-images) +{{< embed cookbook/index.qmd#embed-images >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Rating stars](./cookbook/index.qmd#rating-stars) +{{< embed cookbook/index.qmd#rating-stars >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Combine cols](./cookbook/index.qmd#show-data-from-other-columns) +{{< embed cookbook/index.qmd#show-data-from-other-columns >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Total rows](./cookbook/index.qmd#total-rows) +{{< embed cookbook/index.qmd#total-rows >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Nested tables](./cookbook/index.qmd#nested-tables) +{{< embed cookbook/index.qmd#nested-tables >}} +::: + +:::{.g-col-lg-3 .g-col-12 .example .shrink-example} +## [Units on first row](./cookbook/index.qmd#units-on-first-row) +{{< embed cookbook/index.qmd#units-on-first-row >}} +::: + + +::::: +:::::: + + diff --git a/docs/demos/pypi-downloads/index.qmd b/docs/demos/pypi-downloads/index.qmd index da4691a..2f11cf0 100644 --- a/docs/demos/pypi-downloads/index.qmd +++ b/docs/demos/pypi-downloads/index.qmd @@ -7,6 +7,7 @@ format: --- ```{python} +# | label: setup # | echo: false import json import polars as pl @@ -159,18 +160,26 @@ html = """ ``` ```{python} -#| echo: false +# | label: css +# | echo: false from IPython.display import HTML, display display(HTML(html)) ``` + ```{python} # | echo: false +# | label: table + +from IPython.display import HTML, display + +display(HTML(html)) + tbl = Reactable( outer.head(50), default_sorted=["downloads_month"], - default_page_size=20, + default_page_size=10, show_page_size_options=True, page_size_options=[10, 20, 50, 100], on_click="expand", @@ -199,7 +208,6 @@ tbl = Reactable( theme=Theme(cell_padding="8px 12px"), ) - to_widget( ht.div( # ht.h2("Top PyPI Monthly Downloads (Aug 1, 2024)"), @@ -225,3 +233,10 @@ print_pre("python", code_cell) print_pre("html", html) ``` + + +```{python} +# | label: zzz +print("yo2") + +``` \ No newline at end of file diff --git a/docs/demos/twitter-followers.qmd b/docs/demos/twitter-followers.qmd index 18b004d..10dce3a 100644 --- a/docs/demos/twitter-followers.qmd +++ b/docs/demos/twitter-followers.qmd @@ -6,6 +6,11 @@ execute: daemon: false --- +```{python} +# | label: css +from IPython.display import display, HTML + +html = """ +""" + +display(HTML(html)) +``` + ```{python} +# | label: setup +from reactable import embed_css + +embed_css() -from reactable import bigblock, embed_css -from reactable.models import Column, Props, CellInfo, JS +``` +```{python} +# | label: table +from reactable import Reactable, embed_css +from reactable.tags import to_widget +from reactable.models import Column, CellInfo, JS from htmltools import tags import polars as pl -embed_css() data = pl.read_csv("twitter_followers.csv") @@ -106,71 +123,72 @@ def f_followers(ci: CellInfo): ) -tbl = bigblock( - Props( - data, - pagination=False, - default_sorted=["exclusive_followers_pct"], - default_col_def=Column(header_class="header", align="left"), - columns=dict( - account=Column( - cell=lambda ci: ( - tags.a( - f"@{ci.value}", - href=f"https://twitter.com/{ci.value}", - target="_blank", - ) - ), - width=150, - ), - followers=Column(default_sort_order="desc", cell=f_followers), - exclusive_followers_pct=Column( - name="Exclusive Followers", - default_sort_order="desc", - cell=JS( - """function(cellInfo) { - // Format as percentage - const pct = (cellInfo.value * 100).toFixed(1) + "%" - // Pad single-digit numbers - let value = pct.padStart(5) - // Show % on first row only - if (cellInfo.viewIndex > 0) { - value = value.replace("%", " ") - } - // Render bar chart - return ` -
- ${value} -
-
-
+js_exclusive_percent = JS( + """ + function(cellInfo) { + // Format as percentage + const pct = (cellInfo.value * 100).toFixed(1) + "%" + // Pad single-digit numbers + let value = pct.padStart(5) + // Show % on first row only + if (cellInfo.viewIndex > 0) { + value = value.replace("%", " ") + } + // Render bar chart + return ` +
+ ${value} +
+
- ` - }""" - ), - html=True, +
+ ` + }""" +) + +tbl = Reactable( + data, + pagination=False, + default_sorted=["exclusive_followers_pct"], + default_col_def=Column(header_class="header", align="left"), + columns=dict( + account=Column( + cell=lambda ci: ( + tags.a( + f"@{ci.value}", + href=f"https://twitter.com/{ci.value}", + target="_blank", + ) ), + width=150, ), - compact=True, - class_="followers-tbl", - static=True, - ) + followers=Column(default_sort_order="desc", cell=f_followers), + exclusive_followers_pct=Column( + name="Exclusive Followers", + default_sort_order="desc", + cell=js_exclusive_percent, + html=True, + ), + ), + compact=True, + class_="followers-tbl", + static=True, ) -# tags.div( -# tags.div( -# tags.h2("Candidates whose followers are loyal only to them", class_="followers-title"), -# "Share of each 2020 candidate's followers who don't follow any other candidates", -# class_="followers-header", -# ), -# tbl, -# class_="twitter-followers", -# ) - -tbl +to_widget( + tags.div( + tags.div( + tags.h2("Candidates whose followers are loyal only to them", class_="followers-title"), + "Share of each 2020 candidate's followers who don't follow any other candidates", + class_="followers-header", + ), + tbl, + class_="twitter-followers", + ) +) ``` ---- + Source: [FiveThirtyEight](https://fivethirtyeight.com/features/which-2020-candidates-have-the-most-in-common-on-twitter/) @@ -180,4 +198,17 @@ How it was made: [Building the Twitter Followers Demo](../building-twitter-follo

Source Code

-* TODO: how to show code and css down here? \ No newline at end of file + +```{python} +# | output: asis +# | echo: false +code_cell = _ih[-2] + + +def print_pre(name, code): + print(f"\n```{name}\n{code}\n```\n\n") + + +print_pre("python", code_cell) +print_pre("html", html) +``` \ No newline at end of file diff --git a/docs/styles.css b/docs/styles.css index fa137bd..c907082 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -70,4 +70,10 @@ div.sidebar-item-container .active { .sidebar-section { padding-left: 0px !important; +} + +/* Examples */ + +.shrink-example .cell-output { + zoom: 75%; } \ No newline at end of file diff --git a/reactable/data/nottem.csv b/reactable/data/nottem.csv new file mode 100644 index 0000000..c4cdfeb --- /dev/null +++ b/reactable/data/nottem.csv @@ -0,0 +1,21 @@ +year,Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec +1920,40.6,40.8,44.4,46.7,54.1,58.5,57.7,56.4,54.3,50.5,42.9,39.8 +1921,44.2,39.8,45.1,47,54.1,58.7,66.3,59.9,57,54.2,39.7,42.8 +1922,37.5,38.7,39.5,42.1,55.7,57.8,56.8,54.3,54.3,47.1,41.8,41.7 +1923,41.8,40.1,42.9,45.8,49.2,52.7,64.2,59.6,54.4,49.2,36.3,37.6 +1924,39.3,37.5,38.3,45.5,53.2,57.7,60.8,58.2,56.4,49.8,44.4,43.6 +1925,40,40.5,40.8,45.1,53.8,59.4,63.5,61,53,50,38.1,36.3 +1926,39.2,43.4,43.4,48.9,50.6,56.8,62.5,62,57.5,46.7,41.6,39.8 +1927,39.4,38.5,45.3,47.1,51.7,55,60.4,60.5,54.7,50.3,42.3,35.2 +1928,40.8,41.1,42.8,47.3,50.9,56.4,62.2,60.5,55.4,50.2,43,37.3 +1929,34.8,31.3,41,43.9,53.1,56.9,62.5,60.3,59.8,49.2,42.9,41.9 +1930,41.6,37.1,41.2,46.9,51.2,60.4,60.1,61.6,57,50.9,43,38.8 +1931,37.1,38.4,38.4,46.5,53.5,58.4,60.6,58.2,53.8,46.6,45.5,40.6 +1932,42.4,38.4,40.3,44.6,50.9,57,62.1,63.5,56.3,47.3,43.6,41.8 +1933,36.2,39.3,44.5,48.7,54.2,60.8,65.5,64.9,60.1,50.2,42.1,35.8 +1934,39.4,38.2,40.4,46.9,53.4,59.6,66.5,60.4,59.2,51.2,42.8,45.8 +1935,40,42.6,43.5,47.1,50,60.5,64.6,64,56.8,48.6,44.2,36.4 +1936,37.3,35,44,43.9,52.7,58.6,60,61.1,58.1,49.6,41.6,41.3 +1937,40.8,41,38.4,47.4,54.1,58.6,61.4,61.8,56.3,50.9,41.4,37.1 +1938,42.1,41.2,47.3,46.6,52.4,59,59.6,60.4,57,50.7,47.8,39.2 +1939,39.4,40.9,42.4,47.8,52.4,58,60.7,61.8,58.2,46.7,46.6,37.8