Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

olli

Package Overview
Dependencies
Maintainers
2
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

olli - npm Package Compare versions

Comparing version 1.0.3 to 1.0.4

dist/adapters.js

9

package.json
{
"name": "olli",
"version": "1.0.3",
"version": "1.0.4",
"description": "The core of Olli handling the creation of the Accessibility Tree and rendering the accessible output to the DOM",

@@ -9,3 +9,5 @@ "main": "./dist/olli.js",

"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
"start": "webpack --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
"deploy-docs": "cp ./dist/olli.js ../../docs/olli"
},

@@ -31,4 +33,5 @@ "repository": {

"css-loader": "^6.7.1",
"style-loader": "^3.3.1"
"style-loader": "^3.3.1",
"webpack-merge": "^5.8.0"
}
}

@@ -0,0 +0,0 @@ # Directory Structure

@@ -1,8 +0,10 @@

import { OlliVisSpec } from "olli-adapters/src/Types"
import { OlliVisSpec } from "./Types"
import { renderTable } from "./Render/Table"
import { Tree } from "./Render/TreeView/Tree"
import { renderTree } from "./Render/TreeView"
import { TreeLinks } from "./Render/TreeView/TreeLink"
import { olliVisSpecToTree } from "./Structure"
import { AccessibilityTreeNode } from "./Structure/Types"
import { AccessibilityTree } from "./Structure/Types"
export * from './Types';
/**

@@ -12,4 +14,3 @@ * The configuration object outlining how an accessible visualization should be rendered based on a {@link OlliVisSpec}.

type OlliConfigOptions = {
renderType?: 'tree' | 'table',
ariaLabel?: string
renderType?: 'tree' | 'table'
}

@@ -22,3 +23,3 @@

export function olli(olliVisSpec: OlliVisSpec, config?: OlliConfigOptions): HTMLElement {
let chartEncodingTree: AccessibilityTreeNode = olliVisSpecToTree(olliVisSpec);
const tree: AccessibilityTree = olliVisSpecToTree(olliVisSpec);

@@ -29,4 +30,3 @@ const htmlRendering: HTMLElement = document.createElement("div");

config = {
renderType: config?.renderType ?? 'tree',
ariaLabel: config?.ariaLabel ?? undefined
renderType: config?.renderType || 'tree'
}

@@ -36,26 +36,19 @@

case ("table"):
htmlRendering.appendChild(renderTable(chartEncodingTree));
htmlRendering.appendChild(renderTable(tree.root.selected, tree.fieldsUsed));
break;
case ('tree'):
default:
const ul = renderTree(chartEncodingTree);
const ul = renderTree(tree);
htmlRendering.appendChild(ul);
new TreeLinks(ul).init();
const t = new Tree(ul);
t.init();
document.addEventListener('keypress', (e) => {
if (e.key === 't') {
t.setFocusToItem(t.rootTreeItem);
}
})
break;
}
if (config.ariaLabel) {
htmlRendering.setAttribute("aria-label", config.ariaLabel);
}
document.addEventListener('keypress', (keyStroke) => {
if (keyStroke.key.toLowerCase() === 't') {
const treeview = document.getElementById('treeView');
if (treeview !== null) {
(treeview as any).firstChild!.focus()
}
}
})
return htmlRendering;
}

@@ -1,2 +0,3 @@

import { AccessibilityTreeNode } from "../../Structure/Types";
import { OlliDataset, OlliDatum } from "../../Types";
import { fmtValue } from "../../utils";

@@ -8,21 +9,26 @@ /**

*/
export function renderTable(tree: AccessibilityTreeNode): HTMLElement {
export function renderTable(data: OlliDataset, fieldsUsed: string[]): HTMLElement {
const table = document.createElement("table");
const thead = document.createElement("thead");
const theadtr = document.createElement("tr");
fieldsUsed.forEach((field: string) => {
const th = document.createElement("th");
th.setAttribute('scope', 'col');
th.innerText = field;
theadtr.appendChild(th);
});
thead.appendChild(theadtr);
table.appendChild(thead);
const tableBody = document.createElement("tbody");
const tableHeaders = document.createElement("tr");
tree.fieldsUsed.forEach((field: string) => {
const header = document.createElement("th");
header.innerText = field;
tableHeaders.appendChild(header);
})
tableBody.appendChild(tableHeaders)
tree.selected.forEach((data: any) => {
data.forEach((data: OlliDatum) => {
const dataRow = document.createElement("tr")
tree.fieldsUsed.forEach((field: string) => {
const tableData = document.createElement("td")
tableData.innerText = data[field];
dataRow.appendChild(tableData);
fieldsUsed.forEach((field: string) => {
const td = document.createElement("td")
td.innerText = fmtValue(data[field]);
dataRow.appendChild(td);
})
tableBody.appendChild(dataRow);

@@ -33,2 +39,2 @@ })

return table
}
}

@@ -1,2 +0,5 @@

import { AccessibilityTreeNode } from "../../Structure/Types";
// Adapted from: https://w3c.github.io/aria-practices/examples/treeview/treeview-1/treeview-1b.html
import { AccessibilityTree, AccessibilityTreeNode } from "../../Structure/Types";
import { fmtValue } from "../../utils";
import "./TreeStyle.css";

@@ -6,70 +9,119 @@

*
* @param tree A {@link AccessibilityTreeNode} to generate a navigable tree view from
* @param node A {@link AccessibilityTreeNode} to generate a navigable tree view from
* @returns An {@link HTMLElement} ARIA TreeView of the navigable tree view for a visualization
*/
export function renderTree(tree: AccessibilityTreeNode): HTMLElement {
const nodeToAppend: HTMLElement = document.createElement("li")
nodeToAppend.setAttribute("role", "treeitem");
nodeToAppend.setAttribute("aria-expanded", "false");
export function renderTree(tree: AccessibilityTree): HTMLElement {
const namespace = (Math.random() + 1).toString(36).substring(7);
const nestedChildElements: HTMLElement = document.createElement("ul")
const node = tree.root;
const nodeDescription: HTMLElement = document.createElement("span");
nodeDescription.appendChild(document.createTextNode(tree.description));
const root = document.createElement('ul');
const labelId = `${namespace}-${node.type}-label`;
const treeChildren: AccessibilityTreeNode[] = tree.children;
const dataChildren: AccessibilityTreeNode[] = treeChildren.filter((child: AccessibilityTreeNode) => child.type === "data")
if (dataChildren.length > 0) {
const table: HTMLElement = document.createElement("table");
root.setAttribute('role', 'tree');
root.setAttribute('aria-labelledby', labelId);
const tableBody = document.createElement("tbody");
const rowHeaders = document.createElement("tr");
tree.fieldsUsed.forEach((key: string) => {
const header = document.createElement("th")
header.setAttribute("class", "tableInformation");
header.innerText = key
rowHeaders.appendChild(header);
})
tableBody.appendChild(rowHeaders)
// const childContainer = document.createElement('ul');
// childContainer.setAttribute('role', 'group');
dataChildren.forEach((dataPoint: AccessibilityTreeNode) => {
const dataRow = document.createElement("tr")
tree.fieldsUsed.forEach((key: string) => {
const headerData = document.createElement("td")
headerData.setAttribute("class", "tableInformation");
const value = dataPoint.selected[0][key];
if (!isNaN(value) && value % 1 != 0) {
headerData.innerText = Number(value).toFixed(2);
} else {
headerData.innerText = dataPoint.selected[0][key]
}
dataRow.appendChild(headerData);
})
tableBody.appendChild(dataRow)
})
// childContainer.appendChild(_renderTree(node, namespace, 1, 1, 1));
table.appendChild(tableBody);
// root.appendChild(childContainer);
nestedChildElements.appendChild(table);
// childContainer.querySelector('span')?.setAttribute('id', labelId);
root.appendChild(_renderTree(node, namespace, 1, 1, 1));
root.querySelector('span')?.setAttribute('id', labelId);
return root;
function _renderTree(node: AccessibilityTreeNode, namespace: string, level: number, posinset: number, setsize: number): HTMLElement {
const item = document.createElement('li');
item.setAttribute('role', 'treeitem');
item.setAttribute('aria-level', String(level));
item.setAttribute('aria-setsize', String(setsize));
item.setAttribute('aria-posinset', String(posinset));
item.setAttribute('aria-expanded', 'false');
item.setAttribute('data-nodetype', node.type);
if (node.gridIndex) {
item.setAttribute('data-i', String(node.gridIndex.i));
item.setAttribute('data-j', String(node.gridIndex.j));
}
nodeToAppend.appendChild(nodeDescription);
const label = document.createElement('span');
label.textContent = node.description;
item.appendChild(label);
if (treeChildren.length > 0) {
treeChildren.filter((child: AccessibilityTreeNode) => child.type !== `data`).forEach((child: AccessibilityTreeNode) => {
nestedChildElements.appendChild(renderTree(child));
if (node.children.length) {
const dataChildren = node.children.filter(n => n.type === 'data');
const treeChildren = node.children.filter(n => n.type !== 'data');
const childContainer = document.createElement('ul');
childContainer.setAttribute('role', 'group');
if (dataChildren.length) {
childContainer.appendChild(createDataTable(dataChildren, level + 1));
}
else {
treeChildren.forEach((n, index, array) => {
childContainer.appendChild(_renderTree(n, namespace, level + 1, index + 1, array.length));
})
nodeToAppend.appendChild(nestedChildElements);
}
item.appendChild(childContainer);
}
const treeDom = document.createElement("ul");
treeDom.appendChild(nodeToAppend);
return treeDom;
}
return item;
}
// function appendStyle() {
// const style = document.createElement('style')
// style.innerHTML = treeStyle;
// document.head.appendChild(style)
function createDataTable(dataNodes: AccessibilityTreeNode[], level: number) {
const table = document.createElement("table");
table.setAttribute('aria-label', `Table with ${dataNodes.length} rows`);
table.setAttribute('aria-level', String(level));
table.setAttribute('aria-posinset', '1');
table.setAttribute('aria-setsize', '1');
// }
const thead = document.createElement("thead");
const theadtr = document.createElement("tr");
dataNodes[0].tableKeys?.forEach((key: string) => {
const th = document.createElement("th");
th.setAttribute('scope', 'col');
th.innerText = key
theadtr.appendChild(th);
});
thead.appendChild(theadtr);
table.appendChild(thead);
//
const tableBody = document.createElement("tbody");
dataNodes.forEach((node) => {
const dataRow = document.createElement("tr")
node.tableKeys?.forEach((key: string) => {
const td = document.createElement("td")
const value = fmtValue(node.selected[0][key]);
td.innerText = value;
dataRow.appendChild(td);
})
tableBody.appendChild(dataRow);
});
table.appendChild(tableBody);
const item = document.createElement('li');
item.setAttribute('role', 'treeitem');
item.setAttribute('aria-level', String(level));
item.setAttribute('aria-setsize', '1');
item.setAttribute('aria-posinset', '1');
item.setAttribute('aria-expanded', 'false');
item.appendChild(table);
return item;
}
}

@@ -1,4 +0,9 @@

import { Guide, Chart, OlliVisSpec, Mark, FacetedChart } from "olli-adapters/src/Types";
import { AccessibilityTreeNode, NodeType } from "./Types";
import { Guide, Chart, OlliVisSpec, FacetedChart, Axis, Legend, OlliDatum, OlliDataset } from "../Types";
import { fmtValue } from "../utils";
import { AccessibilityTree, AccessibilityTreeNode, NodeType } from "./Types";
type EncodingFilterValue = string | [number | Date, number | Date];
type GridFilterValue = [EncodingFilterValue, EncodingFilterValue];
type FilterValue = EncodingFilterValue | GridFilterValue;
/**

@@ -9,207 +14,67 @@ * Constructs an {@link AccessibilityTreeNode} based off of a generalized visualization

*/
export function olliVisSpecToTree(olliVisSpec: OlliVisSpec): AccessibilityTreeNode {
let node: AccessibilityTreeNode;
if (olliVisSpec.type === "facetedChart") {
let facets: FacetedChart = olliVisSpec as FacetedChart
facets.charts.forEach((chart: Chart, k: string) => {
chart.data = chart.data.filter((val: any) => val[facets.facetedField] === k)
const updateNestedData = ((g: Guide) => g.data = JSON.parse(JSON.stringify(chart.data)))
chart.axes.forEach(updateNestedData)
chart.legends.forEach(updateNestedData)
})
node = informationToNode(olliVisSpec.description, null, olliVisSpec.data, "multiView", olliVisSpec);
node.description += ` with ${node.children.length} nested charts`
} else {
const axesString: string = olliVisSpec.axes.length > 0 ?
olliVisSpec.axes.length == 2 ?
` ${olliVisSpec.axes.length} axes` :
` ${olliVisSpec.axes[0].orient} axis` :
'';
const legendsString: string = olliVisSpec.legends.length === 1 ? ` and ${olliVisSpec.legends.length} legend` : ''
node = informationToNode(olliVisSpec.description, null, olliVisSpec.data, "chart", olliVisSpec);
node.description += ` with ${axesString} ${legendsString}`
export function olliVisSpecToTree(olliVisSpec: OlliVisSpec): AccessibilityTree {
const fieldsUsed = getFieldsUsedForChart(olliVisSpec);
switch (olliVisSpec.type) {
case "facetedChart":
return {
root: olliVisSpecToNode("multiView", olliVisSpec.data, null, olliVisSpec, fieldsUsed),
fieldsUsed
}
case "chart":
return {
root: olliVisSpecToNode("chart", olliVisSpec.data, null, olliVisSpec, fieldsUsed),
fieldsUsed
}
default:
throw `olliVisSpec.type ${(olliVisSpec as any).type} not handled in olliVisSpecToTree`;
}
return node
}
/**
* Generates children tree nodes for the given parent node.
* @param parent The root faceted chart to be the parent of each nested chart
* @param multiViewChart The {@link FacetedChart} of the abstracted visualization
* @returns an array of {@link AccessibilityTreeNode} to be the given parent's children
*/
function generateMultiViewChildren(parent: AccessibilityTreeNode, multiViewChart: FacetedChart): AccessibilityTreeNode[] {
multiViewChart.type === "facetedChart"
let charts: AccessibilityTreeNode[] = []
multiViewChart.charts.forEach((c: Chart, k: string, m: Map<any, Chart>) => {
charts.push(informationToNode(
`A facet titled ${k}, ${charts.length + 1} of ${m.size}`,
parent,
multiViewChart.data,
"chart",
c))
})
return charts;
function getFieldsUsedForChart(olliVisSpec: OlliVisSpec): string[] {
switch (olliVisSpec.type) {
case "facetedChart":
return [...new Set([olliVisSpec.facetedField, ...[...olliVisSpec.charts.values()].flatMap((chart: Chart) => getFieldsUsedForChart(chart))])];
case "chart":
return [...new Set((olliVisSpec.axes as Guide[]).concat(olliVisSpec.legends).reduce((fields: string[], guide: Guide) => fields.concat(guide.field), []))];
default:
throw `olliVisSpec.type ${(olliVisSpec as any).type} not handled in getFieldsUsedForChart`;
}
}
/**
* Recursively generates children nodes of a chart's structured elements for the provided parent
* @param childrenNodes the array of children nodes to eventually return to the parent
* @param parent The root chart to be the parent of each nested chart
* @param axes The {@link Guide}s of axes to be transformed into {@link AccessibilityTreeNode}s
* @param legends The {@link Guide}s of legends to be transformed into {@link AccessibilityTreeNode}s
* @param grids The {@link Guide}s of axes with grid lines to be transformed into {@link AccessibilityTreeNode}s
* @returns an array of {@link AccessibilityTreeNode} to be the given parent's children
*/
function generateChartChildren(childrenNodes: AccessibilityTreeNode[], parent: AccessibilityTreeNode,
axes: Guide[], legends: Guide[], grids: Guide[]): AccessibilityTreeNode[] {
if (axes.length > 0) {
const axis: Guide = axes.pop()!;
const scaleStr: string = axis.scaleType ? `for a ${axis.scaleType} scale ` : "";
let axisField: string = Array.isArray(axis.field) ? axis.field[1] : (axis.field as string);
let defaultRange: number | string = axis.data[0][axisField]
// TODO: Re-used code from line 143. Make utility function and add try/catch since the data should not be undefined!
if (defaultRange === undefined) {
let updatedField = Object.keys(axis.data[0]).find((k: string) => k.includes(axisField) || axisField.includes(k))
if (updatedField) {
axisField = updatedField
defaultRange = axis.data[0][axisField];
const filterInterval = (selection: OlliDataset, field: string, lowerBound: number | Date, upperBound: number | Date): OlliDataset => {
return selection.filter((datum: any) => {
let value = datum[field];
if (value instanceof Date) {
const lowerBoundStr = String(lowerBound);
const upperBoundStr = String(upperBound);
if (lowerBoundStr.length === 4 && upperBoundStr.length === 4) {
value = value.getFullYear();
}
}
return value >= lowerBound && value < upperBound;
})
}
let minValue: number | string = axis.data.reduce((min: any, val: any) => {
if (val[axisField] !== null && val[axisField] < min) return val[axisField]
return min
}, axis.data[0][axisField])
function axisValuesToIntervals(values: string[] | number[]): ([number, number] | [Date, Date])[] {
let maxValue: number | string = axis.data.reduce((max: any, val: any) => {
if (val[axisField] !== null && val[axisField] > max) return val[axisField]
return max
}, axis.data[0][axisField])
if (axisField.toLowerCase().includes("date")) {
minValue = new Date(minValue).toLocaleString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
maxValue = new Date(maxValue).toLocaleString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
const ensureAxisValuesNumeric = (values: any[]): {values: number[], isDate?: boolean} => {
const isStringArr = values.every(v => typeof v === 'string' || v instanceof String);
if (isStringArr) {
return {
values: values.map(s => Number(s.replaceAll(',', '')))
};
}
const description = `${axis.title} ${scaleStr}with values from ${minValue} to ${maxValue}`;
childrenNodes.push(informationToNode(description, parent, axis.data, axis.title.includes("Y-Axis") ? "yAxis" : "xAxis", axis));
return generateChartChildren(childrenNodes, parent, axes, legends, grids);
} else if (legends.length > 0) {
const legend: Guide = legends.pop()!;
const scaleType = legend.scaleType ? `for ${legend.scaleType} scale ` : "";
let node: AccessibilityTreeNode = informationToNode(legend.title, parent, legend.data, "legend", legend)
node.description = `Legend titled '${node.description}' ${scaleType}with ${node.children.length} values`;
childrenNodes.push(node);
return generateChartChildren(childrenNodes, parent, axes, legends, grids);
} else if (grids.length > 0 && grids.length === 2) {
const grid: Guide[] = [grids.pop()!, grids.pop()!];
childrenNodes.push(informationToNode("Grid view of the data", parent, grid[0].data, "grid", grid))
return generateChartChildren(childrenNodes, parent, axes, legends, grids);
} else {
return childrenNodes;
}
}
/**
* Generates the incremental children for each structured element of a visualization
* @param parent The structured element whose data is being incrmeented
* @param field The data field used to compare idividual data points
* @param values The groupings or increments of values for the structured element (ex: for axes these are the array of ticks)
* @param data The array of data used in the visualization
* @param markUsed {@link Mark} of the visualization
* @returns an array of {@link AccessibilityTreeNode} to be the given parent's children
*/
function generateStructuredNodeChildren(parent: AccessibilityTreeNode, field: string, values: string[] | number[], data: any[], markUsed: Mark): AccessibilityTreeNode[] {
const lowerCaseDesc: string = parent.description.toLowerCase();
if (isStringArray(values) && !field.includes("date") || parent.type === "legend") {
return values.map((grouping: any) => {
return informationToNode(`${[[grouping]]}`, parent, data.filter((node: any) => node[field] === grouping), "filteredData", data.filter((node: any) => node[field] === grouping))
})
} else {
const ticks: number[] = values as number[]
const filterData = (lowerBound: number, upperBound: number): any[] => {
return data.filter((val: any) => {
if ((lowerCaseDesc.includes("date") || lowerCaseDesc.includes("temporal")) && upperBound.toString().length === 4) {
const d = new Date(val[field])
return d.getFullYear() >= lowerBound && d.getFullYear() < upperBound;
} else if (val[field] === undefined) {
let updatedField = Object.keys(val).find((k: string) => k.includes(field) || field.includes(k))
if (updatedField) return val[updatedField] >= lowerBound && val[updatedField] < upperBound;
}
return val[field] >= lowerBound && val[field] < upperBound;
})
const isDateArr = values.every(v => v instanceof Date);
if (isDateArr) {
return {
values: values.map(d => d.getTime()),
isDate: true
};
}
let valueIncrements: any[];
if (markUsed !== 'bar') {
valueIncrements = ticks.reduce(getEncodingValueIncrements, []);
} else {
if (lowerCaseDesc.includes("date") || field.includes("date")) {
valueIncrements = ticks.reduce(getEncodingValueIncrements, []);
} else {
valueIncrements = ticks.map((val: number) => [val, val]);
}
}
return valueIncrements.map((range: number[]) => {
let desc = ``
if ((lowerCaseDesc.includes("date") || field.includes("date") || parent.description.includes("temporal")) && range[0].toString().length > 4) {
range.forEach((val: number) => desc += `${new Date(val).toLocaleString("en-US", { year: 'numeric', month: 'short', day: 'numeric' })}, `)
} else {
desc = `${range},`
}
return informationToNode(desc, parent, filterData(range[0], range[1]), "filteredData", filterData(range[0], range[1]));
});
return {
values
};
}
}
/**
* Generates the incremental children for a pair of axes forming an explorable grid
* @param parent The structured element whose data is being incrmeented
* @param field The data fields used to compare idividual data points
* @param firstValues Array of tick values for the first axis
* @param secondValues Array of tick values for the second axis
* @param data The array of data used in the visualization
* @returns an array of {@link AccessibilityTreeNode} to be the given parent's children
*/
function generateGridChildren(parent: AccessibilityTreeNode, fields: string[], firstValues: number[], secondValues: number[], data: any[]): AccessibilityTreeNode[] {
let childNodes: AccessibilityTreeNode[] = []
const filterData = (xLowerBound: number | string, yLowerBound: number | string, xUpperBound?: number | string, yUpperBound?: number | string): any[] => {
return data.filter((val: any) => {
const inRange = (field: string, r1: number | string, r2?: number | string): boolean => {
if (r2) {
return val[field] >= r1 && val[field] < r2
} else {
return val[field] === r1
}
}
return inRange(fields[1], xLowerBound, xUpperBound) && inRange(fields[0], yLowerBound, yUpperBound);
});
}
const yIncrements: number[][] | string[][] = firstValues.reduce(getEncodingValueIncrements, []);
const xIncrements: number[][] | string[][] = secondValues.reduce(getEncodingValueIncrements, []);
yIncrements.forEach((yIncrement: number[] | string[]) => {
xIncrements.forEach((xIncrement: number[] | string[]) => {
const filteredSelection: any[] = filterData(xIncrement[0], yIncrement[0], xIncrement[1], yIncrement[1]);
childNodes.push(informationToNode(`${[yIncrement, xIncrement]}`, parent, filteredSelection, "filteredData", filteredSelection));
})
})
return childNodes;
}
function isStringArray(data: any[]): data is string[] {
return data.every((pnt: string | number) => typeof pnt === "string")
}
function getEncodingValueIncrements(incrementArray: any[][], currentValue: any, index: number, array: number[] | string[]): any[][] {
if (isStringArray(array)) {
incrementArray.push([currentValue])
return incrementArray
} else {
const getEncodingValueIncrements = (incrementArray: [number, number][], currentValue: number, index: number, array: number[]): [number, number][] => {
let bounds: [number, number]

@@ -224,8 +89,3 @@ let reducedIndex = index - 1;

const incrementDifference: number = currentValue - (array[index - 1] as number)
let finalIncrement;
if (currentValue instanceof Date) {
finalIncrement = currentValue.getTime() + incrementDifference;
} else {
finalIncrement = currentValue + incrementDifference;
}
const finalIncrement = currentValue + incrementDifference;
incrementArray.push([array[reducedIndex] as number, currentValue])

@@ -240,53 +100,12 @@ bounds = [currentValue, finalIncrement];

}
}
/**
* Recursively generates a child node for each data point in the provided range
* @param childrenNodes The array {@link AccessibilityTreeNode} to eventually return
* @param filteredSelection The data points to transform into {@link AccessibilityTreeNode} nodes
* @param parent The parent whose children are being generated
* @returns
*/
function generateFilteredDataChildren(childrenNodes: AccessibilityTreeNode[], filteredSelection: any[], parent: AccessibilityTreeNode): AccessibilityTreeNode[] {
if (filteredSelection.length > 0) {
// const dataPoint: any = filteredSelection.pop();
const dataPoint: any = filteredSelection.pop();
let objCopy: any = {};
Object.keys(dataPoint).forEach((key: string) => {
if (key.toLowerCase().includes("date")) {
objCopy[key] = new Date(dataPoint[key]).toLocaleString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
} else {
objCopy[key] = dataPoint[key]
}
})
childrenNodes.push(informationToNode(nodeToDesc(dataPoint), parent, [objCopy], "data"))
generateFilteredDataChildren(childrenNodes, filteredSelection, parent)
const res = ensureAxisValuesNumeric(values);
const increments = res.values.reduce(getEncodingValueIncrements, []);
if (res.isDate) {
return increments.map(value => [new Date(value[0]), new Date(value[1])])
}
return childrenNodes
return increments;
}
/**
* Creates specific children nodes based on a provided {@link NodeType}
* @param type The {@link NodeType} of the parent
* @param parent The parent {@link AccessibilityTreeNode} whose children need to be generated
* @param generationInformation A changing variable that assists in generating children nodes at all levels
* @returns an array of {@link AccessibilityTreeNode}
*/
function generateChildNodes(type: NodeType, parent: AccessibilityTreeNode, generationInformation: any): AccessibilityTreeNode[] {
if (type === "multiView") {
return generateMultiViewChildren(parent, generationInformation);
} else if (type === "chart") {
return generateChartChildren([], parent, generationInformation.axes, generationInformation.legends, generationInformation.gridNodes);
} else if (type === "xAxis" || type === "yAxis" || type === "legend") {
return generateStructuredNodeChildren(parent, generationInformation.field, generationInformation.values, generationInformation.data, generationInformation.markUsed);
} else if (type === "filteredData") {
return generateFilteredDataChildren([], generationInformation.map((val: any) => Object.assign({}, val)), parent);
} else if (type === "grid") {
return generateGridChildren(parent, [generationInformation[0].field, generationInformation[1].field], generationInformation[0].values, generationInformation[1].values, generationInformation[0].data)
} else {
return [];
}
}
/**
* Creates a {@link AccessibilityTreeNode} of the given parameters

@@ -300,15 +119,181 @@ * @param desc The string that will be used when rendering this node

*/
function informationToNode(desc: string, parent: AccessibilityTreeNode | null, selected: any[], type: NodeType, childrenInformation?: any): AccessibilityTreeNode {
function olliVisSpecToNode(type: NodeType, selected: any[], parent: AccessibilityTreeNode | null, olliVisSpec: OlliVisSpec, fieldsUsed: string[], facetValue?: string, filterValue?: FilterValue, guide?: Guide, index?: number, length?: number, gridIndex?: {i: number, j: number}): AccessibilityTreeNode {
let node: AccessibilityTreeNode = {
description: desc,
type: type,
parent: parent,
selected: selected,
gridIndex,
//
description: type,
children: [],
selected: selected,
type: type,
fieldsUsed: parent !== null ? parent.fieldsUsed : childrenInformation.dataFieldsUsed
}
if (childrenInformation) node.children = generateChildNodes(type, node, childrenInformation);
node.description = nodeToDesc(node);
return node
const facetedChart = olliVisSpec as FacetedChart;
const chart = olliVisSpec as Chart;
switch (type) {
case "multiView":
node.children = [...facetedChart.charts.entries()].map(([facetValue, chart]: [string, Chart], index, array) => {
return olliVisSpecToNode(
"chart",
selected.filter((datum: any) => String(datum[facetedChart.facetedField]) === facetValue),
node,
chart,
fieldsUsed,
facetValue,
undefined,
undefined,
index,
array.length
);
});
break;
case "chart":
// remove some axes depending on mark type
const filteredAxes = chart.axes.filter(axis => {
if (chart.mark === 'bar' && axis.type === 'continuous') {
// don't show continuous axis for bar charts
return false;
}
return true;
});
node.children = [
...filteredAxes.map(axis => {
return olliVisSpecToNode(
axis.axisType === 'x' ? 'xAxis' : 'yAxis',
selected,
node,
chart,
fieldsUsed,
facetValue,
undefined,
axis);
}),
...chart.legends.map(legend => {
return olliVisSpecToNode(
'legend',
selected,
node,
chart,
fieldsUsed,
facetValue,
undefined,
legend);
}),
...(chart.mark === 'point' && filteredAxes.length === 2 && filteredAxes.every(axis => axis.type === 'continuous') ? [
olliVisSpecToNode('grid', selected, node, chart, fieldsUsed, facetValue)
] : [])
]
break;
case "xAxis":
case "yAxis":
const axis = guide as Axis;
switch (axis.type) {
case "discrete":
node.children = axis.values.map(value => {
return olliVisSpecToNode(
'filteredData',
selected.filter(d => String(d[axis.field]) === String(value)),
node,
chart,
fieldsUsed,
facetValue,
String(value),
axis);
});
break;
case "continuous":
const intervals = axisValuesToIntervals(axis.values);
node.children = intervals.map(([a, b]) => {
return olliVisSpecToNode(
'filteredData',
filterInterval(selected, axis.field, a, b),
node,
chart,
fieldsUsed,
facetValue,
[a, b],
axis);
});
break;
}
break;
case "legend":
const legend = guide as Legend;
switch (legend.type) {
case "discrete":
node.children = legend.values.map(value => {
return olliVisSpecToNode(
'filteredData',
selected.filter(d => String(d[legend.field]) === String(value)),
node,
chart,
fieldsUsed,
facetValue,
String(value),
legend);
});
break;
case "continuous":
// TODO currently unsupported
break;
}
break;
case "grid":
const xAxis = chart.axes.find(axis => axis.axisType === 'x')!;
const yAxis = chart.axes.find(axis => axis.axisType === 'y')!;
const xIntervals = axisValuesToIntervals(xAxis.values);
const yIntervals = axisValuesToIntervals(yAxis.values);
const cartesian = (...a: any[][]) => a.reduce((a: any[], b: any[]) => a.flatMap((d: any) => b.map((e: any) => [d, e].flat())));
node.children = cartesian(xIntervals, yIntervals).map(([x1, x2, y1, y2], index) => {
return olliVisSpecToNode(
'filteredData',
filterInterval(
filterInterval(selected, xAxis.field, x1, x2),
yAxis.field,
y1,
y2),
node,
chart,
fieldsUsed,
facetValue,
[[x1, x2], [y1, y2]],
undefined, undefined, undefined, {i: Math.floor(index / yIntervals.length), j: index % yIntervals.length});
});
break;
case "filteredData":
node.children = selected.map((datum, index, array) => {
return olliVisSpecToNode(
'data',
[datum],
node,
chart,
fieldsUsed,
facetValue,
undefined,
guide,
index,
array.length
)
})
break;
case "data":
// set the ordering of the fields for rendering to a table
// put the filter values last, since user already knows the value
node.tableKeys = fieldsUsed;
if (guide) {
node.tableKeys = node.tableKeys.filter(f => f !== guide.field).concat([guide.field]);
}
if (facetValue) {
const facetedField = fieldsUsed[0];
node.tableKeys = node.tableKeys.filter(f => f !== facetedField).concat([facetedField]);
}
break;
default:
throw `Node type ${type} not handled in olliVisSpecToNode`;
}
node.description = nodeToDesc(node, olliVisSpec, facetValue, filterValue, guide, index, length);
return node;
}

@@ -321,17 +306,83 @@

*/
function nodeToDesc(node: AccessibilityTreeNode): string {
if (node.type === "multiView" || node.type === "chart") {
return node.description
} else if (node.type === "xAxis" || node.type === "yAxis") {
return node.description
} else if (node.type === `legend`) {
return node.description
} else if (node.type === "filteredData") {
return `Range ${node.description} ${node.selected.length} ${node.selected.length === 1 ? 'value' : 'values'} in the interval`
} else if (node.type === `grid`) {
return node.description
} else if (node.type === 'data') {
return node.fieldsUsed.reduce((desc: string, currentKey: string) => `${desc} ${currentKey}: ${node.selected[0][currentKey]}`, "");
function nodeToDesc(node: AccessibilityTreeNode, olliVisSpec: OlliVisSpec, facetValue?: string, filterValue?: FilterValue, guide?: Guide, index?: number, length?: number): string {
return _nodeToDesc(node, olliVisSpec, facetValue, filterValue, guide, index, length).replace(/\s+/g, ' ').trim();
function _nodeToDesc(node: AccessibilityTreeNode, olliVisSpec: OlliVisSpec, facetValue?: string, filterValue?: FilterValue, guide?: Guide, index?: number, length?: number): string {
const chartType = (chart: Chart) => {
if (chart.mark === 'point') {
if (chart.axes.every(axis => axis.type === 'continuous')) {
return 'scatterplot';
}
else {
return 'dot plot';
}
}
return chart.mark ? `${chart.mark} chart` : '';
}
const chartTitle = (chart: OlliVisSpec) => (chart.title || facetValue) ? `titled "${chart.title || facetValue}"` : '';
const listAxes = (chart: Chart) => chart.axes.length === 1 ? `with axis "${chart.axes[0].title || chart.axes[0].field}"` : `with axes ${chart.axes.map(axis => `"${axis.title || axis.field}"`).join(' and ')}`;
const guideTitle = (guide: Guide) => `titled "${guide.title || guide.field}"`;
const axisScaleType = (axis: Axis) => `for a ${axis.scaleType || axis.type} scale`;
const legendChannel = (legend: Legend) => legend.channel ? `for ${legend.channel}` : '';
const pluralize = (count: number, noun: string, suffix = 's') => `${count} ${noun}${count !== 1 ? suffix : ''}`;
const guideValues = (guide: Guide) => guide.type === 'discrete' ?
(
guide.values.length === 2 ?
`with 2 values: "${fmtValue(guide.values[0])}" and "${fmtValue(guide.values[1])}"` :
`with ${pluralize(guide.values.length, 'value')} starting with "${guide.values[0]}" and ending with "${guide.values[guide.values.length - 1]}"`
) :
`with values from "${fmtValue(guide.values[0])}" to "${fmtValue(guide.values[guide.values.length - 1])}"`;
const facetValueStr = (facetValue?: string) => facetValue ? `"${facetValue}".` : '';
const filteredValues = (guideFilterValues?: EncodingFilterValue) => {
if (!guideFilterValues) return '';
else if (Array.isArray(guideFilterValues)) {
return `${guideFilterValues.map(v => fmtValue(v)).join(' to ')}`
}
else {
return `"${fmtValue(guideFilterValues)}"`;
}
}
const filteredValuesGrid = (gridFilterValues?: GridFilterValue) => {
if (!gridFilterValues) return '';
return `in ${filteredValues(gridFilterValues[0])} and ${filteredValues(gridFilterValues[1])}`;
}
const indexStr = (index?: number, length?: number) => index !== undefined && length !== undefined ? `${index + 1} of ${length}.` : '';
const datum = (datum: OlliDatum, node: AccessibilityTreeNode) => {
return node.tableKeys?.map(field => {
const value = fmtValue(datum[field]);
return `"${field}": "${value}"`;
}).join(', ');
}
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
const chart = olliVisSpec as Chart;
const axis = guide as Axis;
const legend = guide as Legend;
switch (node.type) {
case 'multiView':
return `A faceted chart ${chartTitle(olliVisSpec)} with ${node.children.length} views.`;
case 'chart':
return `${indexStr(index, length)} A ${chartType(chart)} ${chartTitle(chart)} ${listAxes(chart)}.`;
case 'xAxis':
case 'yAxis':
return `${axis.axisType.toUpperCase()}-axis ${guideTitle(axis)} ${axisScaleType(axis)} ${guideValues(axis)}. ${facetValueStr(facetValue)}`;
case 'legend':
return `Legend ${guideTitle(legend)} ${legendChannel(legend)} ${guideValues(axis)}. ${facetValueStr(facetValue)}`;
case 'grid':
return `Grid view of ${chartType(chart)}. ${facetValueStr(facetValue)}`
case 'filteredData':
if (node.parent?.type === 'grid') {
return `${pluralize(node.children.length, 'value')} ${filteredValuesGrid(filterValue as GridFilterValue)}. ${facetValueStr(facetValue)}`;
}
else {
return `${capitalize(filteredValues(filterValue as EncodingFilterValue))}. ${pluralize(node.children.length, 'value')}. ${facetValueStr(facetValue)}`;
}
case 'data':
// note: the datum description is not used by the table renderer
return `${indexStr(index, length)} ${datum(node.selected[0], node)}`;
default:
throw `Node type ${node.type} not handled in nodeToDesc`;
}
}
return "";
}
}

@@ -0,0 +0,0 @@ # Generating a Tree

@@ -9,43 +9,33 @@ /**

* a high-level overview of the visualization to structured elements (ex: axes and legends) to eventually specific data points.
*
*
* description: A verbose description of the node used when rendering
*
*
* parent: The parent node of the tree, null if this node is the root
*
*
* children: The children tree nodes that this element has
*
*
* selected: The array of data points that are contained in this node and all children nodes
*
*
* type: The {@link NodeType} of this element
*
*
* fieldsUsed: The data fields used (assists with rendering data tables)
*/
export type BaseAccessibilityTreeNode = {
description: string,
export type AccessibilityTreeNode = {
type: NodeType,
parent: AccessibilityTreeNode | null,
children: AccessibilityTreeNode[],
selected: any[],
type: NodeType,
fieldsUsed: string[],
description: string,
children: AccessibilityTreeNode[]
tableKeys?: string[],
gridIndex?: {
i: number,
j: number
}
}
/**
* Node for visual elements such as marks, or anything that relates to the visual aspects of a chart
*/
export interface VisualEncodingNode extends BaseAccessibilityTreeNode {
chartType: string,
export type AccessibilityTree = {
root: AccessibilityTreeNode,
fieldsUsed: string[]
}
/**
* Node for structured elements such as axes and legends
*/
export interface StructuralTreeNode extends BaseAccessibilityTreeNode {
field: string,
}
/**
* Union type of different tree nodes that all share {@link BaseAccessibilityTreeNode} attributes
*/
export type AccessibilityTreeNode = BaseAccessibilityTreeNode | VisualEncodingNode | StructuralTreeNode;
/*

@@ -52,0 +42,0 @@ TODO:

@@ -31,8 +31,3 @@ {

"src"
],
"references": [
{
"path": "../adapters"
}
],
]
}

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc