Skip to content

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:

  1. we write some code to scrape the data emma critiqued from the 2023 state of js survey
  2. transform the data into a tidy dataframe
  3. 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")
scraped javascript survey data showing the usage of specific frameworks
  2016 2017 2018 2019 2020 2021 2022 2023
react 52.000000 61.000000 71.000000 80.000000 80.000000 79.000000 81.000000 84.000000
vuejs 10.000000 21.000000 31.000000 46.000000 48.000000 51.000000 46.000000 50.000000
angular 19.000000 28.000000 56.000000 56.000000 55.000000 53.000000 48.000000 45.000000
preact 7.000000 12.000000 13.000000 14.000000 12.000000 13.000000
svelte 7.000000 14.000000 19.000000 21.000000 25.000000
alpinejs 3.000000 5.000000 6.000000 7.000000
litelement 5.000000 7.000000 6.000000 7.000000
solid 2.000000 6.000000 8.000000
qwik 1.000000 4.000000
stencil 4.000000 4.000000
htmx 5.000000

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.")
a sample of the long data represented.
    year usage next
framework framework      
solid solid 2021 2.000000 6.000000
svelte svelte 2022 21.000000 25.000000
litelement litelement 2021 7.000000 6.000000
alpinejs alpinejs 2021 5.000000 6.000000
htmx htmx 2023 5.000000 5.000000
svelte svelte 2021 19.000000 21.000000
%%
<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")
framework framework year usage next
alpinejs alpinejs 2020 3.00 5.00
alpinejs alpinejs 2021 5.00 6.00
alpinejs alpinejs 2022 6.00 7.00
alpinejs alpinejs 2023 7.00 7.00
angular angular 2016 19.00 28.00
angular angular 2017 28.00 56.00
angular angular 2018 56.00 56.00
angular angular 2019 56.00 55.00
angular angular 2020 55.00 53.00
angular angular 2021 53.00 48.00
angular angular 2022 48.00 45.00
angular angular 2023 45.00 45.00
htmx htmx 2023 5.00 5.00
litelement litelement 2020 5.00 7.00
litelement litelement 2021 7.00 6.00
litelement litelement 2022 6.00 7.00
litelement litelement 2023 7.00 7.00
preact preact 2018 7.00 12.00
preact preact 2019 12.00 13.00
preact preact 2020 13.00 14.00
preact preact 2021 14.00 12.00
preact preact 2022 12.00 13.00
preact preact 2023 13.00 13.00
qwik qwik 2022 1.00 4.00
qwik qwik 2023 4.00 4.00
react react 2016 52.00 61.00
react react 2017 61.00 71.00
react react 2018 71.00 80.00
react react 2019 80.00 80.00
react react 2020 80.00 79.00
react react 2021 79.00 81.00
react react 2022 81.00 84.00
react react 2023 84.00 84.00
solid solid 2021 2.00 6.00
solid solid 2022 6.00 8.00
solid solid 2023 8.00 8.00
stencil stencil 2022 4.00 4.00
stencil stencil 2023 4.00 4.00
svelte svelte 2019 7.00 14.00
svelte svelte 2020 14.00 19.00
svelte svelte 2021 19.00 21.00
svelte svelte 2022 21.00 25.00
svelte svelte 2023 25.00 25.00
vuejs vuejs 2016 10.00 21.00
vuejs vuejs 2017 21.00 31.00
vuejs vuejs 2018 31.00 46.00
vuejs vuejs 2019 46.00 48.00
vuejs vuejs 2020 48.00 51.00
vuejs vuejs 2021 51.00 46.00
vuejs vuejs 2022 46.00 50.00
vuejs vuejs 2023 50.00 50.00
%%
### 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>
```

sloppy css, but it freaking works!

that looks like a line plot brah.

<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>