Skip to content

progressive table enhancementยค

demodemodemodemodemo

%%
start with a dataframe. a dataframe is not a static idea. it is clay, we touch and deform it, it is malleable material.

    df =  DataFrame(numpy.random.randn(20, 4),  columns=list("wxyz"))
    from nbconvert_a11y.table import new, aria
    table = df.table(id="plot")

our first action is to attach interactive widgets that allow us to transform the table representation.

    table.html.form.append(fieldset := new("fieldset", new("legend", "plotting")))
    axes = "XY"
    for i, axis in enumerate("XYZ"):
        checked = {}
        fieldset.append(new("label", axis, new("select", *(
            new("option", column, value=i, name=column, **checked) for i, column in enumerate(["none"] + df.columns.tolist(), -1)), 
                            onchange="swapVar(this); swapClass(this);", name=axis, **aria(controls=table.id))))
    ...
```css
[href="#plot"] {
    display: block;
    height: 400px;
    width: 100%;
    background: -moz-element(#plot);
}
```

start with a dataframe. a dataframe is not a static idea. it is clay, we touch and deform it, it is malleable material.

df =  DataFrame(numpy.random.randn(20, 4),  columns=list("wxyz"))
from nbconvert_a11y.table import new, aria
table = df.table(id="plot")

our first action is to attach interactive widgets that allow us to transform the table representation.

table.html.form.append(fieldset := new("fieldset", new("legend", "plotting")))
axes = "XY"
for i, axis in enumerate("XYZ"):
    checked = {}
    fieldset.append(new("label", axis, new("select", *(
        new("option", column, value=i, name=column, **checked) for i, column in enumerate(["none"] + df.columns.tolist(), -1)), 
                        onchange="swapVar(this); swapClass(this);", name=axis, **aria(controls=table.id))))
...
[href="#plot"] {
    display: block;
    height: 400px;
    width: 100%;
    background: -moz-element(#plot);
}
%%
## degress of freedom

establishing the css variables for our visualization system.

    table.html.append(style := new("style", 
```css
@property --width {syntax: "<number>"; inherits: true; initial-value: 600;}
@property --uX {syntax: "<length>"; inherits: true; initial-value: 1px;}
@property --X-dir {syntax: "<number>"; inherits: true; initial-value: 1;}
@property --Y-dir {syntax: "<number>"; inherits: true; initial-value: 1;}
@property --X-dir {syntax: "<number>"; inherits: true; initial-value: 1;}
@property --height {syntax: "<number>"; inherits: true; initial-value: 400;}
@property --uY {syntax: "<length>"; inherits: true; initial-value: 1px;}
@property --depth {syntax: "<number>"; inherits: true; initial-value: 10;}
@property --distance {syntax: "<number>"; inherits: true; initial-value: 36;}
@property --uZ {syntax: "<length>"; inherits: true; initial-value: 1in;}
@property --time {syntax: "<time>"; inherits: true; initial-value: 2s;}

:root {
    --WIDTH: calc(var(--width) * var(--uX));
    --HEIGHT: calc(var(--height) * var(--uY));
    --DEPTH: calc(var(--depth) * var(--uZ));

}
```
    ))

degress of freedom

establishing the css variables for our visualization system.

table.html.append(style := new("style", 
@property --width {syntax: "<number>"; inherits: true; initial-value: 600;}
@property --uX {syntax: "<length>"; inherits: true; initial-value: 1px;}
@property --X-dir {syntax: "<number>"; inherits: true; initial-value: 1;}
@property --Y-dir {syntax: "<number>"; inherits: true; initial-value: 1;}
@property --X-dir {syntax: "<number>"; inherits: true; initial-value: 1;}
@property --height {syntax: "<number>"; inherits: true; initial-value: 400;}
@property --uY {syntax: "<length>"; inherits: true; initial-value: 1px;}
@property --depth {syntax: "<number>"; inherits: true; initial-value: 10;}
@property --distance {syntax: "<number>"; inherits: true; initial-value: 36;}
@property --uZ {syntax: "<length>"; inherits: true; initial-value: 1in;}
@property --time {syntax: "<time>"; inherits: true; initial-value: 2s;}

:root {
    --WIDTH: calc(var(--width) * var(--uX));
    --HEIGHT: calc(var(--height) * var(--uY));
    --DEPTH: calc(var(--depth) * var(--uZ));
    
}
))
%%
## visibility widgets

adding visibility toggles to the columns of table introduces a new, configurable marker style.
_the table does NOT update the screen reader experience yet._

    table.html.form.append(fieldset := new('fieldset', new("legend", "visibility")))
    for i, name in enumerate(df.index.names, 1):
        fieldset.append(
            new("label", new("input", onchange="swapStyle(this)", type="checkbox", checked="", **aria(controls=F"{table.id}-col--{i}-style")), str(name))
        )
        fieldset.append(new("style", """
            #%s tr th:nth-of-type(%i) {display: none;}
            """ % (name, i), id =F"{table.id}-col--{i}-style", media="none"))
    for j, row in enumerate(df.columns.values, 1):
        fieldset.append(
            new("label", new("input", onchange="swapStyle(this)", type="checkbox", checked="", **aria(controls=F"{table.id}-col-{j}-style")), str(row))
        )
        fieldset.append(new("style", """
        #%s {
            tbody tr td:nth-of-type(%i){display: none;}
            thead tr th:nth-of-type(%i) {display: none;}
        }
        """ % (table.id, j, i + j), id =F"{table.id}-col-{j}-style", media="none"))

visibility widgets

adding visibility toggles to the columns of table introduces a new, configurable marker style. the table does NOT update the screen reader experience yet.

table.html.form.append(fieldset := new('fieldset', new("legend", "visibility")))
for i, name in enumerate(df.index.names, 1):
    fieldset.append(
        new("label", new("input", onchange="swapStyle(this)", type="checkbox", checked="", **aria(controls=F"{table.id}-col--{i}-style")), str(name))
    )
    fieldset.append(new("style", """
        #%s tr th:nth-of-type(%i) {display: none;}
        """ % (name, i), id =F"{table.id}-col--{i}-style", media="none"))
for j, row in enumerate(df.columns.values, 1):
    fieldset.append(
        new("label", new("input", onchange="swapStyle(this)", type="checkbox", checked="", **aria(controls=F"{table.id}-col-{j}-style")), str(row))
    )
    fieldset.append(new("style", """
    #%s {
        tbody tr td:nth-of-type(%i){display: none;}
        thead tr th:nth-of-type(%i) {display: none;}
    }
    """ % (table.id, j, i + j), id =F"{table.id}-col-{j}-style", media="none"))
%%
## general properties of the visual table

    style.string +=\
```css
table#plot:is(.X, .Y, .Z) {
    width: var(--WIDTH);
    thead { display: none;}
    tbody {
        padding: 50px;
        transform-style: preserve-3d;
        border: solid 1px;
        position: relative;
        width: fit-content;
        display: flex;
        flex-direction: column;
        tr {
            display: flex;
            flex-direction: row;
            width: fit-content;
            transform-style: preserve-3d;
            transition: all calc(var(--time) + 1s) linear;
            border: solid 2px;
            th, td{
                border: solid 3px;
            }
        }
    }
}
```

general properties of the visual table

style.string +=\
table#plot:is(.X, .Y, .Z) {
    width: var(--WIDTH);
    thead { display: none;}
    tbody {
        padding: 50px;
        transform-style: preserve-3d;
        border: solid 1px;
        position: relative;
        width: fit-content;
        display: flex;
        flex-direction: column;
        tr {
            display: flex;
            flex-direction: row;
            width: fit-content;
            transform-style: preserve-3d;
            transition: all calc(var(--time) + 1s) linear;
            border: solid 2px;
            th, td{
                border: solid 3px;
            }
        }
    }
}
%%
## single axis

the ordering of the axis definition matters because of the cascading nature of css.
in each definition of the X, Y, Z axes we establish new degrees of freedom for our
visualization such as width, height, and depth respectively.

    style.string +=\
```css
table#plot.Z tbody {        
    tr {
        --dZ: calc((var(--Z) - var(--Z-min)) / (var(--Z-max) - var(--Z-min)));
        display: block;
        position: relative;
        transform-origin: 50% 50%;
        transform: 
            perspective(calc(var(--distance) * var(--uZ)))
            translateZ(calc(var(--dZ) * var(--DEPTH)))
            ;
    }
}
```



    style.string +=\
```css
table#plot.Y tbody {
    height: var(--HEIGHT);
    background:linear-gradient(0deg, transparent 99%, lightblue 1%);
    background-size:100px 100px;
    tr {
        position: absolute;
        --dY: calc((var(--Y) - var(--Y-min)) / (var(--Y-max) - var(--Y-min)));
        transform: translateY(calc(var(--dY) * var(--HEIGHT))) translate(0, -50%);
    }
}
```

    style.string +=\
```css
table#plot.X tbody {
    width: var(--WIDTH);
    background:
        linear-gradient(90deg, transparent 99%, lightblue 1%)
        ;
    background-size:
        100px 100px
        ;

    tr {
        --dX: calc((var(--X) - var(--X-min)) / (var(--X-max) - var(--X-min)));
        transform: 
            translateX(calc(var(--dX) * var(--WIDTH)))
            translate(-50%, 0)
            ;
    }
}
```

single axis

the ordering of the axis definition matters because of the cascading nature of css. in each definition of the X, Y, Z axes we establish new degrees of freedom for our visualization such as width, height, and depth respectively.

style.string +=\
table#plot.Z tbody {        
    tr {
        --dZ: calc((var(--Z) - var(--Z-min)) / (var(--Z-max) - var(--Z-min)));
        display: block;
        position: relative;
        transform-origin: 50% 50%;
        transform: 
            perspective(calc(var(--distance) * var(--uZ)))
            translateZ(calc(var(--dZ) * var(--DEPTH)))
            ;
    }
}
style.string +=\
table#plot.Y tbody {
    height: var(--HEIGHT);
    background:linear-gradient(0deg, transparent 99%, lightblue 1%);
    background-size:100px 100px;
    tr {
        position: absolute;
        --dY: calc((var(--Y) - var(--Y-min)) / (var(--Y-max) - var(--Y-min)));
        transform: translateY(calc(var(--dY) * var(--HEIGHT))) translate(0, -50%);
    }
}
style.string +=\
table#plot.X tbody {
    width: var(--WIDTH);
    background:
        linear-gradient(90deg, transparent 99%, lightblue 1%)
        ;
    background-size:
        100px 100px
        ;

    tr {
        --dX: calc((var(--X) - var(--X-min)) / (var(--X-max) - var(--X-min)));
        transform: 
            translateX(calc(var(--dX) * var(--WIDTH)))
            translate(-50%, 0)
            ;
    }
}
%%
## double axis

the 2 axis situations inherit properties from the single axes. further we define more complex transformations and origins.

    style.string +=\
```css
table#plot.X.Y tbody {
    background: linear-gradient(0deg, transparent 99%, lightblue 1%), linear-gradient(90deg, transparent 99%, lightblue 1%);
    background-size: 100px 100px, 100px 100px;
    tr {
        transform: 
            translateX(calc(var(--dX) * var(--WIDTH)))
            translateY(calc(var(--dY) * var(--HEIGHT)))
            translate(-50%, -50%);
    }
}
table#plot.Y.Z tbody {
    tr {
        transform-origin: 50% calc(var(--HEIGHT) / 2);
        transform: 
            perspective(calc(var(--distance) * var(--uZ)))
            translateZ(calc(var(--dZ) * var(--DEPTH)))
            translateY(calc(var(--dY) * var(--HEIGHT)))
            translate(0, -50%)
            ;
    }
}
table#plot.X.Z tbody {
    tr {
        transform-origin: calc(var(--WIDTH) / 2) 50%;
        transform: 
            perspective(calc(var(--distance) * var(--uZ)))
            translateZ(calc(var(--dZ) * var(--DEPTH)))
            translateX(calc(var(--dX) * var(--WIDTH)))
            ;
    }
}
```

## all together now 

3 axes situation

    style.string +=\
```css
table#plot.X.Y.Z tbody {
    tr {
        transform-origin: calc(var(--WIDTH) / 2) calc(var(--HEIGHT) / 2);
        transform: 
            perspective(calc(var(--distance) * var(--uZ)))
            translateZ(calc(var(--dZ) * var(--DEPTH)))
            translateX(calc(var(--dX) * var(--WIDTH)))
            translateY(calc(var(--dY) * var(--HEIGHT)))
            translate(-50%, -50%)
            ;
    }
}
```

double axis

the 2 axis situations inherit properties from the single axes. further we define more complex transformations and origins.

style.string +=\
table#plot.X.Y tbody {
    background: linear-gradient(0deg, transparent 99%, lightblue 1%), linear-gradient(90deg, transparent 99%, lightblue 1%);
    background-size: 100px 100px, 100px 100px;
    tr {
        transform: 
            translateX(calc(var(--dX) * var(--WIDTH)))
            translateY(calc(var(--dY) * var(--HEIGHT)))
            translate(-50%, -50%);
    }
}
table#plot.Y.Z tbody {
    tr {
        transform-origin: 50% calc(var(--HEIGHT) / 2);
        transform: 
            perspective(calc(var(--distance) * var(--uZ)))
            translateZ(calc(var(--dZ) * var(--DEPTH)))
            translateY(calc(var(--dY) * var(--HEIGHT)))
            translate(0, -50%)
            ;
    }
}
table#plot.X.Z tbody {
    tr {
        transform-origin: calc(var(--WIDTH) / 2) 50%;
        transform: 
            perspective(calc(var(--distance) * var(--uZ)))
            translateZ(calc(var(--dZ) * var(--DEPTH)))
            translateX(calc(var(--dX) * var(--WIDTH)))
            ;
    }
}

all together now

3 axes situation

style.string +=\
table#plot.X.Y.Z tbody {
    tr {
        transform-origin: calc(var(--WIDTH) / 2) calc(var(--HEIGHT) / 2);
        transform: 
            perspective(calc(var(--distance) * var(--uZ)))
            translateZ(calc(var(--dZ) * var(--DEPTH)))
            translateX(calc(var(--dX) * var(--WIDTH)))
            translateY(calc(var(--dY) * var(--HEIGHT)))
            translate(-50%, -50%)
            ;
    }
}
%%
## javascript support functions. 

our goal with these methods are to use as many semantic features of the element
to control behavior. the tag should describe what it is AND what is does.

    table.html.append(new("script",
```javascript
/**
swap the features on a specific axis by changing css variables.
*/
function swapVar(element) {
    let option = element.selectedOptions[0]
        target = document.getElementById(element.getAttribute(`aria-controls`));
    if (option.value &lt; 0 ) {

    } else {
        target.style.setProperty(`--${element.name}-min`, `var(--${option.value}-min)`);
        target.style.setProperty(`--${element.name}-max`, `var(--${option.value}-max)`);
        target.querySelectorAll("tr").forEach(
            (x) =&gt; {
                x.style.setProperty(`--${element.name}`, `var(--${option.value})`);
            }
        );
    }
}
/**
toggles classes on a target based on a set of options
*/
function swapClass(element) {
    let option = element.selectedOptions[0],
       target = document.getElementById(element.getAttribute(`aria-controls`));
    target.classList.toggle(element.name, option.value &gt; -1);
}
/**
switch the media attribute of a controlled style tag.
*/
function swapStyle(element) {
    let target = document.getElementById(element.getAttribute(`aria-controls`));
    target.setAttribute(`media`, element.checked ?  `none` : `all`);
}
```
    ))

javascript support functions.

our goal with these methods are to use as many semantic features of the element to control behavior. the tag should describe what it is AND what is does.

table.html.append(new("script",
/**
swap the features on a specific axis by changing css variables.
*/
function swapVar(element) {
    let option = element.selectedOptions[0]
        target = document.getElementById(element.getAttribute(`aria-controls`));
    if (option.value < 0 ) {
        
    } else {
        target.style.setProperty(`--${element.name}-min`, `var(--${option.value}-min)`);
        target.style.setProperty(`--${element.name}-max`, `var(--${option.value}-max)`);
        target.querySelectorAll("tr").forEach(
            (x) => {
                x.style.setProperty(`--${element.name}`, `var(--${option.value})`);
            }
        );
    }
}
/**
toggles classes on a target based on a set of options
*/
function swapClass(element) {
    let option = element.selectedOptions[0],
       target = document.getElementById(element.getAttribute(`aria-controls`));
    target.classList.toggle(element.name, option.value > -1);
}
/**
switch the media attribute of a controlled style tag.
*/
function swapStyle(element) {
    let target = document.getElementById(element.getAttribute(`aria-controls`));
    target.setAttribute(`media`, element.checked ?  `none` : `all`);
}
))
%%
## demo

set the initial conditions.

    table.html.table.attrs.update({"class": "X Y"})
    labels = iter(table.html.form.fieldset.select("label"))
    next(labels).select_one("""option[name="x"]""").attrs["selected"] = ""
    (x := next(labels).select_one("""option[name="y"]""")).attrs["selected"] = ""
    table.html.append(new("script", 
```javascript
document.querySelectorAll("select[onchange]").forEach(
    (x) =&gt; {x.dispatchEvent(new Event("change"))}    
)
```
                         ))
    HTML(table.html)
pandas dataframe with 20 rows, 4 columns with 1 index levels and 1 columns levels.
Nonewxyz
0-0.0380.5360.5290.522
1-1.589-0.6761.0200.362
20.7980.5890.008-2.300
30.8230.1220.455-1.204
40.4641.0950.5490.311
5-0.0491.952-1.073-1.259
6-0.485-0.2150.923-0.177
7-1.1980.180-0.4370.263
8-0.0680.4660.5510.607
9-0.0460.641-0.6471.184
10-0.2130.5142.219-1.828
11-0.8550.8840.699-0.625
121.204-1.830-1.193-0.163
13-0.3860.338-0.9590.859
14-0.9870.130-0.423-0.063
15-1.630-0.8420.7610.774
160.572-1.0841.233-1.216
171.5130.1020.8871.370
18-0.9020.2570.1160.279
192.0460.0430.0720.272
min-1.630-1.830-1.193-2.300
max2.0461.9522.2191.370
diff3.6763.7823.4123.669

plotting
visibility

demo

set the initial conditions.

table.html.table.attrs.update({"class": "X Y"})
labels = iter(table.html.form.fieldset.select("label"))
next(labels).select_one("""option[name="x"]""").attrs["selected"] = ""
(x := next(labels).select_one("""option[name="y"]""")).attrs["selected"] = ""
table.html.append(new("script", 
document.querySelectorAll("select[onchange]").forEach(
   (x) => {x.dispatchEvent(new Event("change"))} 
)
                     ))
HTML(table.html)