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.container.dialog.dialog.form.details.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.container.dialog.dialog.form.details.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.container.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.container.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.container.dialog.dialog.form.details.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.container.dialog.dialog.form.details.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.container.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.container.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.table.attrs.update({"class": "X Y"})
    labels = iter(table.container.dialog.dialog.form.details.fieldset.select("label"))
    next(labels).select_one("""option[name="x"]""").attrs["selected"] = ""
    (x := next(labels).select_one("""option[name="y"]""")).attrs["selected"] = ""
    table.container.append(new("script", 
```javascript
document.querySelectorAll("select[onchange]").forEach(
    (x) =&gt; {x.dispatchEvent(new Event("change"))}    
)
```
                         ))
    HTML(table.container)
wxyz
0-0.0275-0.571-0.3829-1.5975
1-0.9933-1.6393-0.3096-0.9563
22.09770.7554-0.0806-1.1483
30.22730.259-0.63660.7647
4-0.3330.0562-0.3734-0.4969
5-0.13170.4818-0.145-0.3931
60.85690.08680.1815-0.8421
70.5641-1.02221.04561.3537
80.7279-0.22931.63480.0816
90.911-0.26620.7502-0.2676
101.27081.0367-0.17940.287
110.04980.55190.2086-2.0149
120.5281-0.8856-0.67811.4749
13-1.9251-0.5337-0.5558-0.9417
14-0.23660.6751.5749-0.7348
151.26581.31-0.485-1.539
160.9286-0.64170.63470.0904
170.4812-0.3979-1.14241.0719
180.17280.1939-0.5224-0.935
190.39831.1888-1.42061.2021
min-1.9251-1.6393-1.4206-2.0149
max2.09771.311.63481.4749
controls

plotting
visibility
context

demo

set the initial conditions.

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