creating a line plot from a table¤
ive been searching for the means to recreate line plots from tables and css usage. this is my first taste of that outcome. i was inspired to finally code this up after i saw emma dawson's more accessible line plots.
in this post:
- we write some code to scrape the data emma critiqued from the 2023 state of js survey
- transform the data into a tidy dataframe
- show the dataframe and customize the css to simulate a line plot.
the outcome is a table that screen readers can navigate with all the features of their native assistive technology. the data is on the page so we can scrape and access later. overall, using tables is such a win.
scrape the state of js dom for data with playwright¤
import playwright.async_api, bs4
from nbconvert_a11y.t import disp_table, Config
async with playwright.async_api.async_playwright() as pw:
browser = await pw.chromium.launch()
page = await browser.new_page()
await page.goto("https://2023.stateofjs.com/en-US/libraries/front-end-frameworks/")
front_end = bs4.BeautifulSoup(await page.content(), features="lxml")
extract the names of the frameworks¤
s = Series(g := front_end.select(".chart-line[data-id]"),
Index(map(compose_left(operator.attrgetter("attrs"), get("data-id")), g), )
).iloc[:-1]
extract the names of the years the survey accounts for¤
years = Series(
front_end.select(".chart-column")
).attrgetter("text").drop_duplicates().astype(int)[::-1].rename("year")
extract the usage numbers from the text elements in the svg¤
data = s.methodcaller("select", "text").apply(compose_left(
reversed, partial(zip, years), dict
)).series().stack().apply(first).apply(get(slice(-1))).astype(int).unstack().sort_index(axis=1)
data.fillna("").style.set_caption("scraped javascript survey data showing the usage of specific frameworks")
a long version of the table to style as a line plot with css¤
long = data.stack().to_frame("usage")
long.index.names = ("framework", "year")
long = long.reset_index("year")
long = long.groupby("framework").apply(
lambda df: df.assign(
next=[*df.usage[1:]] + [df.usage.iloc[-1]]
)
)
long.sample(6).style.set_caption("a sample of the long data represented.")
%%
<input checked="" id="plot" name="scales" onchange="document.getElementById('line-style').setAttribute('media', document.getElementById('plot').checked? 'screen':'no screen')" type="checkbox"/>
<label for="plot">visually line plot</label>
long.pipe(disp_table, Config(
summary_footer=False,
data_visibility=Config.DataVisibility(
max_rows=1000,
)
), id="usage")
%%
### sloppy css, but it freaking works!
that looks like a line plot brah.
```html
<style id="line-style" media="screen">
#usage {
--w: 600;
--h: 400;
--u: 1px;
height: calc(var(--h) * var(--u)); width: calc(var(--w) * var(--u));
--dyear: calc(var(--year-max) - var(--year-min));
--dusage: calc(var(--usage-max) - var(--usage-min));
thead {
display: none;
}
tbody {
position: relative;
height: calc(var(--h) * var(--u)); width: calc(var(--w) * var(--u));
tr {
--y: calc((var(--usage) - var(--usage-min))/var(--dusage));
--y0: calc((var(--next) - var(--usage-min))/var(--dusage));
--x: calc((var(--year) - var(--year-min))/var(--dyear));
--x0: calc((var(--year) + 1 - var(--year-min))/var(--dyear));
--d: calc(
pow(
pow(var(--h) * (var(--y) - var(--y0)), 2)
+ pow(var(--w) * 1 / var(--dyear), 2),
1/2
)
);
--x-pos: calc(var(--x) * var(--w) * var(--u));
--x0-pos: calc(var(--x0) * var(--w) * var(--u));
--y-pos: calc(var(--y) * var(--h) * var(--u));
--y0-pos: calc(var(--y0) * var(--h) * var(--u));
font-size: 0px;
display: block;
&::after {
top: var(--y-pos); left: var(--x-pos);
display: block;
position: absolute;
font-size: 12px;
content: var(--framework);
border: solid white 2px;
}
height: 0; width: 0;
--t: 5px;
--line-color: gray;
&::before {
top: var(--y-pos); left: var(--x-pos);
display: block;
position: absolute;
width: calc(var(--u) * var(--d));
border-top: var(--t) solid var(--line-color);
border-bottom: var(--t) dotted var(--line-color);
height: 0px;
content: "";
transform: rotate(atan2(var(--y0-pos) - var(--y-pos), var(--x0-pos) - var(--x-pos)));
transform-origin: top left;
}
}
&:last-child {
display: none;
}
}
}
</style>
```