function interval(range = [], options = {}) {
const [min = 0, max = 1] = range;
const {
step = .001,
label = null,
value = [min, max],
format = ([start, end]) => `${start} … ${end}`,
color,
width,
theme,
__ns__ = randomScope(),
} = options;
const css = `
#${__ns__} {
//font: 13px/1.2 var(--sans-serif);
display: flex;
align-items: baseline;
flex-wrap: wrap;
max-width: 100%;
width: auto;
flex-direction: column;
}
@media only screen and (min-width: 30em) {
#${__ns__} {
flex-wrap: nowrap;
width: 360px;
}
}
#${__ns__} .label {
//width: 120px;
//padding: 5px 0 4px 0;
//padding: 5px 0 0 0;
//margin-right: 6.5px;
flex-shrink: 0;
}
#${__ns__} .form {
display: flex;
width: 100%;
}
#${__ns__} .range {
flex-shrink: 1;
width: 100%;
}
#${__ns__} .range-slider {
width: 100%;
margin-bottom: .3em;
margin-top: .3em;
}
`;
const $range = rangeInput({min, max, value: [value[0], value[1]], step, color, width, theme});
const $output = html`<output>`;
const $view = html`<div id=${__ns__}>
${label == null ? '' : html`<div class="label">${label}`}
<div class=form>
<div class=range>
${$range}<div class=range-output>${$output}</div>
</div>
</div>
${html`<style>${css}`}
`;
const update = () => {
const content = format([$range.value[0], $range.value[1]]);
if(typeof content === 'string') $output.value = content;
else {
while($output.lastChild) $output.lastChild.remove();
$output.appendChild(content);
}
};
$range.oninput = update;
update();
return Object.defineProperty($view, 'value', {
get: () => $range.value,
set: ([a, b]) => {
$range.value = [a, b];
update();
},
});
}
function rangeInput(options = {}) {
const {
min = 0,
max = 100,
step = 'any',
value: defaultValue = [min, max],
color,
width,
theme = theme_Flat,
} = options;
const controls = {};
const scope = randomScope();
const clamp = (a, b, v) => v < a ? a : v > b ? b : v;
// Will be used to sanitize values while avoiding floating point issues.
const input = html`<input type=range ${{min, max, step}}>`;
const dom = html`<div class=${`${scope} range-slider`} style=${{
color,
width: cssLength(width),
}}>
${controls.track = html`<div class="range-track">
${controls.zone = html`<div class="range-track-zone">
${controls.range = html`<div class="range-select" tabindex=0>
${controls.min = html`<div class="thumb thumb-min" tabindex=0>`}
${controls.max = html`<div class="thumb thumb-max" tabindex=0>`}
`}
`}
`}
${html`<style>${theme.replace(/:scope\b/g, '.'+scope)}`}
</div>`;
let value = [], changed = false;
Object.defineProperty(dom, 'value', {
get: () => [...value],
set: ([a, b]) => {
value = sanitize(a, b);
updateRange();
},
});
const sanitize = (a, b) => {
a = isNaN(a) ? min : ((input.value = a), input.valueAsNumber);
b = isNaN(b) ? max : ((input.value = b), input.valueAsNumber);
return [Math.min(a, b), Math.max(a, b)];
}
const updateRange = () => {
const ratio = v => (v - min) / (max - min);
dom.style.setProperty('--range-min', `${ratio(value[0]) * 100}%`);
dom.style.setProperty('--range-max', `${ratio(value[1]) * 100}%`);
};
const dispatch = name => {
dom.dispatchEvent(new Event(name, {bubbles: true}));
};
const setValue = (vmin, vmax) => {
const [pmin, pmax] = value;
value = sanitize(vmin, vmax);
updateRange();
// Only dispatch if values have changed.
if(pmin === value[0] && pmax === value[1]) return;
dispatch('input');
changed = true;
};
setValue(...defaultValue);
// Mousemove handlers.
const handlers = new Map([
[controls.min, (dt, ov) => {
const v = clamp(min, ov[1], ov[0] + dt * (max - min));
setValue(v, ov[1]);
}],
[controls.max, (dt, ov) => {
const v = clamp(ov[0], max, ov[1] + dt * (max - min));
setValue(ov[0], v);
}],
[controls.range, (dt, ov) => {
const d = ov[1] - ov[0];
const v = clamp(min, max - d, ov[0] + dt * (max - min));
setValue(v, v + d);
}],
]);
// Returns client offset object.
const pointer = e => e.touches ? e.touches[0] : e;
// Note: Chrome defaults "passive" for touch events to true.
const on = (e, fn) => e.split(' ').map(e => document.addEventListener(e, fn, {passive: false}));
const off = (e, fn) => e.split(' ').map(e => document.removeEventListener(e, fn, {passive: false}));
let initialX, initialV, target, dragging = false;
function handleDrag(e) {
// Gracefully handle exit and reentry of the viewport.
if(!e.buttons && !e.touches) {
handleDragStop();
return;
}
dragging = true;
const w = controls.zone.getBoundingClientRect().width;
e.preventDefault();
handlers.get(target)((pointer(e).clientX - initialX) / w, initialV);
}
function handleDragStop(e) {
off('mousemove touchmove', handleDrag);
off('mouseup touchend', handleDragStop);
if(changed) dispatch('change');
}
invalidation.then(handleDragStop);
dom.ontouchstart = dom.onmousedown = e => {
dragging = false;
changed = false;
if(!handlers.has(e.target)) return;
on('mousemove touchmove', handleDrag);
on('mouseup touchend', handleDragStop);
e.preventDefault();
e.stopPropagation();
target = e.target;
initialX = pointer(e).clientX;
initialV = value.slice();
};
controls.track.onclick = e => {
if(dragging) return;
changed = false;
const r = controls.zone.getBoundingClientRect();
const t = clamp(0, 1, (pointer(e).clientX - r.left) / r.width);
const v = min + t * (max - min);
const [vmin, vmax] = value, d = vmax - vmin;
if(v < vmin) setValue(v, v + d);
else if(v > vmax) setValue(v - d, v);
if(changed) dispatch('change');
};
return dom;
}
function randomScope(prefix = 'scope-') {
return prefix + (performance.now() + Math.random()).toString(32).replace('.', '-');
}
cssLength = v => v == null ? null : typeof v === 'number' ? `${v}px` : `${v}`
html = htl.html
theme_Flat = `
/* Options */
:scope {
color: #3b99fc;
width: 240px;
}
:scope {
position: relative;
display: inline-block;
--thumb-size: 15px;
--thumb-radius: calc(var(--thumb-size) / 2);
//padding: var(--thumb-radius) 0;
margin: 2px;
vertical-align: middle;
}
:scope .range-track {
box-sizing: border-box;
position: relative;
height: 7px;
background-color: hsl(0, 0%, 80%);
overflow: visible;
border-radius: 4px;
padding: 0 var(--thumb-radius);
}
:scope .range-track-zone {
box-sizing: border-box;
position: relative;
}
:scope .range-select {
box-sizing: border-box;
position: relative;
left: var(--range-min);
width: calc(var(--range-max) - var(--range-min));
cursor: ew-resize;
background: currentColor;
height: 7px;
border: inherit;
}
/* Expands the hotspot area. */
:scope .range-select:before {
content: "";
position: absolute;
width: 100%;
height: var(--thumb-size);
left: 0;
top: calc(2px - var(--thumb-radius));
}
:scope .range-select:focus,
:scope .thumb:focus {
outline: none;
}
:scope .thumb {
box-sizing: border-box;
position: absolute;
width: var(--thumb-size);
height: var(--thumb-size);
background: #fcfcfc;
top: -4px;
border-radius: 100%;
border: 1px solid hsl(0,0%,55%);
cursor: default;
margin: 0;
}
:scope .thumb:active {
box-shadow: inset 0 var(--thumb-size) #0002;
}
:scope .thumb-min {
left: calc(-1px - var(--thumb-radius));
}
:scope .thumb-max {
right: calc(-1px - var(--thumb-radius));
}
`tol = ({
QualBright: ['#4477AA', '#EE6677', '#228833', '#CCBB44', '#66CCEE','#AA3377'],
QualHighContrast: ['#004488', '#DDAA33', '#BB5566'],
QualVibrant: ['#EE7733', '#0077BB', '#33BBEE', '#EE3377', '#CC3311', '#009988'],
QualMuted: ['#CC6677', '#332288', '#DDCC77', '#117733', '#88CCEE','#882255', '#44AA99', '#999933', '#AA4499'],
QualLight: ['#77AADD', '#EE8866', '#EEDD88', '#FFAABB', '#99DDFF', '#44BB99', '#BBCC33', '#AAAA00'],
Sunset: ['#364B9A', '#4A7BB7', '#6EA6CD', '#98CAE1', '#C2E4EF', '#EAECCC', '#FEDA8B', '#FDB366', '#F67E4B', '#DD3D2D', '#A50026'],
BuRd: ['#2166AC', '#4393C3', '#92C5DE', '#D1E5F0', '#F7F7F7', '#FDDBC7', '#F4A582', '#D6604D', '#B2182B'],
PRGn: ['#762A83', '#9970AB', '#C2A5CF', '#E7D4E8', '#F7F7F7', '#D9F0D3', '#ACD39E', '#5AAE61', '#1B7837'],
YlOrBr: ['#FFFFE5', '#FFF7BC', '#FEE391', '#FEC44F', '#FB9A29',
'#EC7014', '#CC4C02', '#993404', '#662506'],
Iridescent: ['#FEFBE9', '#FCF7D5', '#F5F3C1', '#EAF0B5', '#DDECBF',
'#D0E7CA', '#C2E3D2', '#B5DDD8', '#A8D8DC', '#9BD2E1',
'#8DCBE4', '#81C4E7', '#7BBCE7', '#7EB2E4', '#88A5DD',
'#9398D2', '#9B8AC4', '#9D7DB2', '#9A709E', '#906388',
'#805770', '#684957', '#46353A'],
RainbowPuRd: ['#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6',
'#4E96BC', '#549EB3', '#59A5A9', '#60AB9E', '#69B190',
'#77B77D', '#8CBC68', '#A6BE54', '#BEBC48', '#D1B541',
'#DDAA3C', '#E49C39', '#E78C35', '#E67932', '#E4632D',
'#DF4828', '#DA2222'],
RainbowPuBr: ['#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6',
'#4E96BC', '#549EB3', '#59A5A9', '#60AB9E', '#69B190',
'#77B77D', '#8CBC68', '#A6BE54', '#BEBC48', '#D1B541',
'#DDAA3C', '#E49C39', '#E78C35', '#E67932', '#E4632D',
'#DF4828', '#DA2222', '#B8221E', '#95211B', '#721E17',
'#521A13'],
RainbowWhRd: ['#E8ECFB', '#DDD8EF', '#D1C1E1', '#C3A8D1', '#B58FC2',
'#A778B4', '#9B62A7', '#8C4E99', '#6F4C9B', '#6059A9',
'#5568B8', '#4E79C5', '#4D8AC6', '#4E96BC', '#549EB3',
'#59A5A9', '#60AB9E', '#69B190', '#77B77D', '#8CBC68',
'#A6BE54', '#BEBC48', '#D1B541', '#DDAA3C', '#E49C39',
'#E78C35', '#E67932', '#E4632D', '#DF4828', '#DA2222'],
RainbowDiscrete: ['#E8ECFB', '#D9CCE3', '#D1BBD7', '#CAACCB', '#BA8DB4',
'#AE76A3', '#AA6F9E', '#994F88', '#882E72', '#1965B0',
'#437DBF', '#5289C7', '#6195CF', '#7BAFDE', '#4EB265',
'#90C987', '#CAE0AB', '#F7F056', '#F7CB45', '#F6C141',
'#F4A736', '#F1932D', '#EE8026', '#E8601C', '#E65518',
'#DC050C', '#A5170E', '#72190E', '#42150A']
})ns = Inputs.text().classList[0]
// custom css to override some ojs defaults for inputs
html`<style>
.${ns} {
--label-width: 80px;
}
form.${ns} {
flex-wrap: wrap;
}
.plot-inputs form.${ns} {
flex-direction: column;
}
.${ns} div label {
background-color: #f4f4f4;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
margin-right: 0.25rem;
margin-bottom: 0.25rem;
width: auto;
}
.${ns} div label:hover,
.${ns} div label:active,
.${ns} div label:focus {
background-color: #fbe4b4;
}
.${ns} input[type="checkbox"] {
accent-color: black;
margin-bottom: 0;
}
.${ns} div input[type="number"] {
background-color: #f4f4f4;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
flex-shrink: 3;
border: none;
}
.${ns} select {
background-color: #f4f4f4;
border: none;
border-radius: 0.5rem;
padding: 0.25rem 0.5rem;
//width: auto;
}
.${ns} .hist {
width: 100%;
display: flex;
flex-direction: column;
row-gap: 0em;
}
</style>`task_info = ({
"hf": "Hearts & Flowers",
"sds": "Same & Different",
"mg": "Memory",
"math": "Math",
"matrix": "Pattern Matching",
"mrot": "Shape Rotation",
"trog": "Sentence Understanding",
"vocab": "Vocabulary",
"tom": "Stories",
"pa": "Language Sounds",
"sre": "Sentence Reading",
"swr": "Word Reading",
})
prep_scores = (sc) => {
return sc.map(obj => {
return tag_vars.reduce((acc, key) => ({
...acc,
[key]: acc[key] === undefined || acc[key] === null ? "NA" : acc[key]
}), {
...obj,
adaptive: obj.adaptive ? "adaptive" : "non-adaptive",
task_label: task_info[obj.task_code]
})
}).sort((a, b) => {
const keysOrder = Object.keys(task_info);
return keysOrder.indexOf(a.task_code) - keysOrder.indexOf(b.task_code);
})
}
mutable scores = prep_scores(initial_scores)vi = ({
"age": { label: "Age (years)", filter: true, tip: true, range: [4, 12] },
"age_gap": { label: "Age gap (months)", filter: true, tip: false, range: [0, 24] },
"score": { label: "Score", filter: false, tip: true, range: null },
"score_se": { label: "Score SE", filter: false, tip: true, range: null },
"num_attempted": { label: "num_attempted", filter: false, tip: true, range: null }
})
// info for categorical variables
// color = include in color dropdown?, tip = include in tooltip?
// note: filter property not actually used programmatically
ti = ({
"site": { label: "Site", filter: true, color: true, tip: true },
"dataset": { label: "Dataset", filter: true, color: true, tip: true },
"task_label": { label: "Task", filter: true, color: false, tip: false },
"language": { label: "Language", filter: true, color: true, tip: false },
"adaptive": { label: "Adaptive", filter: true, color: true, tip: false },
"score_type": { label: "Score type", filter: true, color: true, tip: false },
"scoring_model": { label: "Scoring model", filter: true, color: true, tip: false },
"run_id": { label: "run_id", filter: false, color: false, tip: true },
})interval_format = ([start, end]) => `[${Plot.formatNumber("en-US")(start)}; ${Plot.formatNumber("en-US")(end)}]`
// integer-rounded min and max of an array
// range = (x) => [Math.floor(d3.min(x)), Math.ceil(d3.max(x))]
// interval input for a given numeric variable
var_interval = (v) => {
return interval(vi[v].range, {
step: 1, label: vi[v].label, format: interval_format, width: "95%", color: "#f7ca69"
})
}cat_checkbox = (tags, t) => Inputs.checkbox(tags[t], {value: tags[t]})
viewof age_range = var_interval("age")
viewof age_gap_range = var_interval("age_gap")
viewof site_vals = cat_checkbox(tags, "site")
viewof dataset_vals = cat_checkbox(tags, "dataset")
viewof task_label_vals = cat_checkbox(tags, "task_label")
viewof language_vals = cat_checkbox(tags, "language")
viewof adaptive_vals = cat_checkbox(tags, "adaptive")
viewof score_type_vals = cat_checkbox(tags, "score_type")
viewof scoring_model_vals = cat_checkbox(tags, "scoring_model")
// populates datasource selector -- first click should open redivis auth pop up
viewof auth_button = Inputs.button("Populate data sources", {
reduce: async () => {
const ds = await getDatasets()
mutable dn = allowedDatasetNames(ds)
}
})
mutable dn = [] // available datasource names
viewof datasource = Inputs.select(dn)
// replaces current scores with scores from selected datasource
viewof switch_datasource_button = Inputs.button("Switch data source", {
reduce: async () => {
const ds_scores = await getScores(datasource);
mutable scores = prep_scores(ds_scores);
}
})
range_filter = (v, v_range) => {
return (d) => d[v] === undefined || (d[v] >= v_range[0] & d[v] <= v_range[1])
}
// generate overlap filter function for a given categorical variable and input values
overlap_filter = (t, t_vals) => {
return (d) => t_vals.some(v => d[t].includes(v))
}
// generate overlap filter function as above but that defaults to including everything if input values empty
overlap_null_filter = (t, t_vals) => {
return (d) => t_vals.length ? t_vals.some(v => d[t].includes(v)) : true
}
// specify range-based inputs (as variable, values)
range_inputs = ([
{ v: "age", vals: age_range },
])
// specify overlap-based inputs (as variable, values)
overlap_inputs = ([
{ v: "site", vals: site_vals },
{ v: "dataset", vals: dataset_vals },
{ v: "task_label", vals: task_label_vals },
{ v: "language", vals: language_vals },
{ v: "adaptive", vals: adaptive_vals },
{ v: "score_type", vals: score_type_vals },
{ v: "scoring_model", vals: scoring_model_vals },
])
//apply_filters = (df, ri, oi) => {
// const overlap_filters = oi.map(({ v, vals }) => overlap_filter(v, vals))
// const range_filters = ri.map(({ v, vals }) => range_filter(v, vals))
// const all_filters = [...range_filters, ...overlap_filters]
// return all_filters.reduce((d, fun) => d.filter(fun), df)
//}
//sf = apply_filters(scores, range_inputs, overlap_inputs)
// generate filter function for each input and combine them
range_filters = range_inputs.map(({ v, vals }) => range_filter(v, vals))
overlap_filters = overlap_inputs.map(({ v, vals }) => overlap_filter(v, vals))
all_filters = [...range_filters, ...overlap_filters]
// apply filters to score data
sf = all_filters.reduce((sc, fun) => sc.filter(fun), scores)
// apply age gap filter
ag = ls.filter(d => d.age_gap === NaN || (d.age_gap >= age_gap_range[0] & d.age_gap <= age_gap_range[1]))
ag_runs = ag.map(d => d.runs).flat()
scoresf = sf.filter(d => ag_runs.includes(d.run_id))
// plot type selector
viewof plot_type = Inputs.radio(
new Map([["Cross-sectional", "dot"], ["Longitudinal", "line"]]),
{value: "dot", label: "Data view"}
)
cat_array = Object.keys(ti).filter((t) => ti[t].color).map((t) => [ti[t].label, t])
cat_vars = new Map([["None", null], ...cat_array])
viewof color_var = Inputs.select(cat_vars, {value: "site", label: "Color"})
x_var = "age" // x
y_var = "score" // y
r_var = "score_se" // dot radius
z_var = "user_id" // line group
f_var = "task_label" // facets
//
// plot colors
// set up to keep color mapping consistent regardless of filters but include
// only present values in legend
//
scheme = tol.QualMuted // color scheme
default_color = scheme[0] // default color when there's no color variable
unk_color = "lightgrey" // color for missing values
plt_color = color_var || default_color // plot color variable
// color variable values -- null or all possible values for variable except NA
color_vals = color_var === null ? [default_color] : tags[color_var].filter((v) => v !== "NA")
// domain: values + NA
color_domain = [...color_vals, "NA"]
// range: corresponding values from scheme + missing color
color_range = [...scheme.slice(0, color_vals.length), unk_color]
// map from domain to range
color_map = color_domain.map((v, i) => [v, color_range[i]])
// drop elements from map if values not in data
colors = Object.fromEntries(color_map.filter(([k, v]) => scoresf.map((d) => d[color_var]).includes(k)))
// create facet specification to emulate facet wrap
// given data, column name to facet by, and number of columns
facet_wrap = (df, col, ncol) => {
const keys = Array.from(d3.union(df.map((d) => d[col])));
const index = new Map(keys.map((key, i) => [key, i]));
const fx = (key) => index.get(key) % ncol;
const fy = (key) => Math.floor(index.get(key) / ncol);
return ({
fx: (d) => fx(d[col]),
fy: (d) => fy(d[col]),
text: Plot.text(keys, {fx, fy, dy: -12, frameAnchor: "top", fontWeight: "bold"})
//text: Plot.text(keys, {fx, fy, frameAnchor: "top", fontWeight: "bold"})
})
}
all_vars = [...Object.entries(ti), ...Object.entries(vi)] // all variables
tip_vars = all_vars.filter(([k, v]) => v.tip) // variables with "tip" property
dot_channels = Object.fromEntries(tip_vars.map(([k, v]) => [k, k]))
// marks for dots
dot_marks = Plot.marks(
// facet labels
dot_facet.text,
// points
Plot.dot(scoresf, {
x: x_var,
y: y_var,
r: r_var,
stroke: plt_color,
strokeOpacity: 0.5,
fx: dot_facet.fx,
fy: dot_facet.fy
}),
// regression lines
Plot.linearRegressionY(scoresf, {
x: x_var,
y: y_var,
stroke: "black",
fx: dot_facet.fx,
fy: dot_facet.fy
}),
// tooltips
Plot.tip(scoresf, Plot.pointer({
x: x_var,
y: y_var,
stroke: plt_color,
fx: dot_facet.fx,
fy: dot_facet.fy,
channels: dot_channels,
format: { fx: null, fy: null }
}))
)
ls = Array.from(
d3.group(sf, d => d[f_var], d => d[z_var]),
([f_val, z_map]) => Array.from(
z_map,
([z_val, points]) => ({
[f_var]: f_val,
[z_var]: z_val,
[color_var]: points[0][color_var],
[x_var]: d3.mean(points, d => d[x_var]),
[y_var]: d3.mean(points, d => d[y_var]),
n_scores: points.length,
age_gap: 12*(d3.max(points, d => d[x_var]) - d3.min(points, d => d[x_var])),
runs: points.map(d => d.run_id)
// [`${x_var}_mean`]: d3.mean(points, d => d[x_var]),
// [`${y_var}_mean`]: d3.mean(points, d => d[y_var]),
// [`${x_var}_range`]: d3.extent(points, d => d[x_var]),
// [`${y_var}_range`]: d3.extent(points, d => d[y_var]),
})
)
).flat()
// filter line segment summary to users with more than one score
line_summary = ls.filter(d => d.n_scores > 1)
// filter scores to runs filtered line summary
long_runs = line_summary.map(d => d.runs).flat()
scoresl = scoresf.filter(d => long_runs.includes(d.run_id))
// channels for line tooltips (everything in summary other than facet variable)
//line_vars = Object.keys(line_summary[0]).filter(d => d !== f_var)
line_vars = [color_var, z_var, "n_scores", "age_gap"]
line_channels = Object.fromEntries(line_vars.map(k => [k, k]))
// marks for lines
line_marks = Plot.marks(
// facet labels
line_facet.text,
// spaghetti lines
Plot.line(scoresl, {
x: x_var,
y: y_var,
z: z_var,
stroke: plt_color,
fx: line_facet.fx,
fy: line_facet.fy
}),
// regression lines
Plot.linearRegressionY(scoresl, {
x: x_var,
y: y_var,
stroke: "black",
fx: line_facet.fx,
fy: line_facet.fy
}),
// tooltips
Plot.tip(line_summary, Plot.pointer({
x: x_var,
y: y_var,
stroke: plt_color,
fx: line_summary_facet.fx,
fy: line_summary_facet.fy,
channels: line_channels,
format: { x: null, y: null, fx: null, fy: null }
}))
)
dot_facet = facet_wrap(scoresf, f_var, 6) // facetting for dots
line_facet = facet_wrap(scoresl, f_var, 6) // facetting for lines
line_summary_facet = facet_wrap(line_summary, f_var, 6) // facetting for line summary
// map plot_type values to marks
marks = ({ dot: dot_marks, line: line_marks })
// values for x axis ticks -- age range with step size 1
//ages = d3.range(age_range[0], age_range[1] + 1, 1)
span = (x) => [Math.floor(d3.min(x)), Math.ceil(d3.max(x))]
age_span = span(scoresf.map(d => d.age))
ages = d3.range(age_span[0], age_span[1] + 1, 1)
// plot scores!
scorePlot = Plot.plot({
style: { fontFamily: "var(--sans-serif)", fontSize: 12 },
width: 1000,
grid: true,
x: { line: true, domain: age_span, ticks: ages, tickFormat: d3.format(".0f") },
y: { line: true },
r: { transform: (s) => s ? 1/s : 1, range: [0, 4] }, // radius inversely proportional
color: {
domain: Object.keys(colors),
range: Object.values(colors),
legend: color_var !== null,
className: "color-legend"
},
facet: { axis: null }, // hide facet labels
marks: marks[plot_type] // use marks according to plot type (dot/line)
})Filter by…
NoteSite
NoteDataset
NoteTask
NoteLanguage
NoteAdaptive
NoteScore type
NoteScoring model
Change data source…
Note: To change data sources, you need a Redivis account. Click on the button below to log into your Redivis account and list the data sources that you have access to.
redivis = require("redivis")
// get scores table for a given dataset
async function getScores(dataset) {
console.log('getting scores')
const rows = await redivis
.organization('LEVANTE')
.dataset(dataset)
.table('scores')
.listRows({ max_results: 3 })
console.log('got scores')
return rows;
}
// get references to all LEVANTE datasets (with "properties" populated)
async function getDatasets() {
const ds = await redivis
.organization('LEVANTE')
.listDatasets()
return await Promise.all(ds.map(d => d.get()))
}
// filter datasets by authed user's access + get names of "processed" datasets
allowedDatasetNames = (ds) => {
const accessLevels = ["data", "edit"]
return ds.filter(d => accessLevels.includes(d.properties.accessLevel))
.map(d => d.name)
.filter(n => n.match("processed"))
.sort()
}