From e3808e50096fe69a77a6cb0e4d3f3ced86176853 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Sat, 22 Feb 2020 13:41:50 +0100 Subject: [PATCH] Implements Grid extractors/visualizer and sorting demo. --- data-extraction/src/CommonDataTypes.ts | 27 +- .../api/default-extractors/GridExtractor.ts | 32 ++ .../registerDefaultDataExtractors.ts | 2 + data-extraction/src/js/helpers/index.ts | 1 + data-extraction/src/js/helpers/markedGrid.ts | 16 + demo/js/src/demo_sorting.ts | 79 ++++ .../ui/src/Visualizers/GridVisualizer.tsx | 376 ++++++++++++++++++ extension/ui/src/Visualizers/index.ts | 4 +- 8 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 data-extraction/src/js/api/default-extractors/GridExtractor.ts create mode 100644 data-extraction/src/js/helpers/markedGrid.ts create mode 100644 demo/js/src/demo_sorting.ts create mode 100644 extension/ui/src/Visualizers/GridVisualizer.tsx diff --git a/data-extraction/src/CommonDataTypes.ts b/data-extraction/src/CommonDataTypes.ts index 857f62d..f2153ff 100644 --- a/data-extraction/src/CommonDataTypes.ts +++ b/data-extraction/src/CommonDataTypes.ts @@ -8,7 +8,8 @@ export type CommonDataType = | CommonDataTypes.Tree | CommonDataTypes.Ast | CommonDataTypes.Graph - | CommonDataTypes.Plotly; + | CommonDataTypes.Plotly + | CommonDataTypes.Grid; export function isCommonDataType( data: ExtractedData, @@ -55,6 +56,30 @@ export module CommonDataTypes { root: TreeNode; } + export interface Grid { + kind: { array: true }; + columnLabels?: { label?: string }[]; + rows: { + label?: string; + columns: { + content?: string; + tag?: string; + color?: string; + }[]; + }[]; + markers?: { + id: string; + + row: number; + column: number; + rows?: number; + columns?: number; + + label?: string; + color?: string; + }[]; + } + export interface Ast extends Tree<{ position: number; diff --git a/data-extraction/src/js/api/default-extractors/GridExtractor.ts b/data-extraction/src/js/api/default-extractors/GridExtractor.ts new file mode 100644 index 0000000..f6ac601 --- /dev/null +++ b/data-extraction/src/js/api/default-extractors/GridExtractor.ts @@ -0,0 +1,32 @@ +import { ExtractedData } from "../../../DataExtractionResult"; +import { + DataExtractor, + ExtractionCollector, + DataExtractorContext, +} from "../.."; +import { CommonDataTypes } from "../../../CommonDataTypes"; + +export class GridExtractor implements DataExtractor { + readonly id = "grid"; + getExtractions( + data: unknown, + extractionCollector: ExtractionCollector, + context: DataExtractorContext + ): void { + if (!Array.isArray(data)) { + return; + } + + extractionCollector.addExtraction({ + id: this.id, + name: "Array As Grid", + priority: 500, + extractData(): CommonDataTypes.Grid { + return { + kind: { array: true }, + rows: [{ columns: data.map(d => ({ tag: d })) }], + }; + }, + }); + } +} diff --git a/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts b/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts index a9c8984..4328877 100644 --- a/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts +++ b/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts @@ -6,6 +6,7 @@ import { ToStringDataExtractor } from "./ToStringExtractor"; import { PlotDataExtractor } from "./PlotlyDataExtractor"; import { ObjectGraphExtractor } from "./ObjectGraphExtractor"; import { getDataExtractorApi } from "../injection"; +import { GridExtractor } from "./GridExtractor"; /** * The default data extractors should be registered by VS Code automatically. @@ -21,6 +22,7 @@ export function registerDefaultExtractors( new ToStringDataExtractor(), new PlotDataExtractor(), new ObjectGraphExtractor(), + new GridExtractor(), ]) { api.registerExtractor(item); } diff --git a/data-extraction/src/js/helpers/index.ts b/data-extraction/src/js/helpers/index.ts index 4f6547f..23af5d4 100644 --- a/data-extraction/src/js/helpers/index.ts +++ b/data-extraction/src/js/helpers/index.ts @@ -1,3 +1,4 @@ export * from "./createGraph"; export * from "./createGraphFromPointers"; export * from "./tryEval"; +export * from "./markedGrid"; diff --git a/data-extraction/src/js/helpers/markedGrid.ts b/data-extraction/src/js/helpers/markedGrid.ts new file mode 100644 index 0000000..047d0b1 --- /dev/null +++ b/data-extraction/src/js/helpers/markedGrid.ts @@ -0,0 +1,16 @@ +import { CommonDataTypes } from "../../CommonDataTypes"; + +export function markedGrid( + arr: any[], + marked: Record +): CommonDataTypes.Grid { + return { + kind: { array: true }, + rows: [{ columns: arr.map(d => ({ tag: d })) }], + markers: Object.entries(marked).map(([key, val]) => ({ + id: key, + row: 0, + column: val, + })), + }; +} diff --git a/demo/js/src/demo_sorting.ts b/demo/js/src/demo_sorting.ts new file mode 100644 index 0000000..38a76f9 --- /dev/null +++ b/demo/js/src/demo_sorting.ts @@ -0,0 +1,79 @@ +import { getDataExtractorApi } from "@hediet/debug-visualizer-data-extraction"; + +getDataExtractorApi().registerDefaultExtractors(); + +// From https://github.com/AvraamMavridis/Algorithms-Data-Structures-in-Typescript/blob/master/algorithms/quickSort.md +const array = [1, 2, 33, 31, 1, 2, 63, 123, 6, 32, 943, 346, 24]; +const sorted = quickSort(array, 0, array.length - 1); +console.log(sorted); + +function swap(array: Array, i: number, j: number) { + [array[i], array[j]] = [array[j], array[i]]; +} + +/** + * Split array and swap values + * + * @param {Array} array + * @param {number} [left=0] + * @param {number} [right=array.length - 1] + * @returns {number} + */ +function partition( + array: Array, + left: number = 0, + right: number = array.length - 1 +) { + const pivot = Math.floor((right + left) / 2); + const pivotVal = array[pivot]; + let i = left; + let j = right; + + while (i <= j) { + while (array[i] < pivotVal) { + i++; + } + + while (array[j] > pivotVal) { + j--; + } + + if (i <= j) { + swap(array, i, j); + i++; + j--; + } + } + + return i; +} + +/** + * Quicksort implementation + * + * @param {Array} array + * @param {number} [left=0] + * @param {number} [right=array.length - 1] + * @returns {Array} + */ +function quickSort( + array: Array, + left: number = 0, + right: number = array.length - 1 +) { + let index; + + if (array.length > 1) { + index = partition(array, left, right); + + if (left < index - 1) { + quickSort(array, left, index - 1); + } + + if (index < right) { + quickSort(array, index, right); + } + } + + return array; +} diff --git a/extension/ui/src/Visualizers/GridVisualizer.tsx b/extension/ui/src/Visualizers/GridVisualizer.tsx new file mode 100644 index 0000000..21ac42e --- /dev/null +++ b/extension/ui/src/Visualizers/GridVisualizer.tsx @@ -0,0 +1,376 @@ +import { + VisualizationProvider, + VisualizationCollector, + asVisualizationId, +} from "./Visualizer"; +import { + ExtractedData, + isCommonDataType, + CommonDataTypes, +} from "@hediet/debug-visualizer-data-extraction"; +import React = require("react"); +import { observer } from "mobx-react"; +import { computed, action, observable } from "mobx"; + +export class GridVisualizer extends VisualizationProvider { + getVisualizations( + data: ExtractedData, + collector: VisualizationCollector + ): void { + if (isCommonDataType(data, { array: true })) { + collector.addVisualization({ + id: asVisualizationId("grid"), + name: "Grid", + priority: 90, + render: () => , + }); + } + } +} + +export class DecoratedGridComponent extends React.Component<{ + data: CommonDataTypes.Grid; +}> { + render() { + const d = this.props.data; + + const map = new Set(); + function getUniqueId(tag: string): string { + let n = 0; + while (true) { + const id = tag + (n === 0 ? "" : `-${n}`); + if (!map.has(id)) { + map.add(id); + return id; + } + n++; + } + } + + const rows: GridComponent["props"]["rows"] = []; + rows.push(); + let rowIdx = 0; + let columnCount = 0; + for (const row of d.rows) { + columnCount = Math.max(columnCount, row.columns.length); + rows.push({ + columns: row.columns.map((c, colIdx) => ({ + content: c.content !== undefined ? c.content : c.tag || "", + id: + c.tag !== undefined + ? getUniqueId(c.tag) + : `${rowIdx}-${colIdx}`, + color: c.color, + kind: "data", + })), + }); + rowIdx++; + } + + rows.unshift({ + columns: [...new Array(columnCount)].map((v, idx) => ({ + id: `column-header-${idx}`, + content: `${idx}`, + kind: "header", + })), + }); + + if (d.markers) { + for (const m of d.markers) { + rows.push({ + columns: [], + }); + const r = rows[rows.length - 1]; + for (let i = 0; i < m.column; i++) { + r.columns.push({ + content: "", + kind: "empty", + id: `marker-spacer-${i}`, + }); + } + + r.columns.push({ + id: m.id, + kind: "data", + content: m.label || m.id, + }); + } + } + + return ( +
+ +
+ ); + } +} + +interface CellData { + content: string; + id: string; + color?: string; + kind: "data" | "header" | "empty"; +} + +@observer +class GridComponent extends React.Component<{ + rows: { + columns: CellData[]; + }[]; + markers: { + id: string; + + row: number; + column: number; + rows?: number; + columns?: number; + + label?: string; + color?: string; + }[]; +}> { + private readonly cells = new Map(); + + @computed get cellsGrid(): { maxWidth: number; grid: CellInfo[][] } { + const unusedIds = new Set(this.cells.keys()); + + const rows = this.props.rows; + + const grid = new Array>(); + + let maxWidth = 0; + for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) { + const row = rows[rowIdx]; + maxWidth = Math.max(maxWidth, row.columns.length); + const cellRow = new Array(); + grid.push(cellRow); + + for (let colIdx = 0; colIdx < row.columns.length; colIdx++) { + const col = row.columns[colIdx]; + const key = col.id; + unusedIds.delete(key); + let c = this.cells.get(key); + if (!c) { + // New cell + c = new CellInfo(col); + this.cells.set(key, c); + } else { + c.updateCellData(col); + } + cellRow.push(c); + } + } + + for (const id in unusedIds.values()) { + this.cells.delete(id); + } + + return { maxWidth, grid }; + } + + @computed + private get layout(): { + cells: { + cell: CellInfo; + top: number; + left: number; + width: number; + height: number; + }[]; + width: number; + height: number; + } { + const { maxWidth, grid } = this.cellsGrid; + + const maxWidths = new Array(); + for (let colIdx = 0; colIdx < maxWidth; colIdx++) { + let maxWidth = 30; + for (const row of grid) { + const col = row[colIdx]; + if (col) { + maxWidth = Math.max(maxWidth, col.contentWidth || 0); + } + } + maxWidths.push(maxWidth); + } + + const result = new Array<{ + cell: CellInfo; + top: number; + left: number; + width: number; + height: number; + }>(); + + let width = 0; + let height = 0; + let top = 0; + let left = 0; + + for (const row of grid) { + let maxHeight = 0; + for (const col of row) { + maxHeight = Math.max(maxHeight, col.contentHeight || 0); + } + + for (let colIdx = 0; colIdx < row.length; colIdx++) { + const maxWidth = maxWidths[colIdx]; + const col = row[colIdx]; + result.push({ + cell: col, + top, + left, + height: maxHeight, + width: maxWidth, + }); + height = Math.max(height, top + maxHeight); + width = Math.max(width, left + maxWidth); + col.lastTop = col.top; + col.lastLeft = col.left; + col.top = top; + col.left = left; + + col.lastContentArea = col.contentArea; + col.contentArea = maxHeight * maxWidth; + left += maxWidth; + } + + left = 0; + top += maxHeight; + } + + result.sort((a, b) => a.cell.id.localeCompare(b.cell.id)); + return { cells: result, height, width }; + } + + render() { + const l = this.layout; + return ( +
+ {l.cells.map(i => ( +
+
+
+
+ {i.cell.content} +
+
+
+
+ ))} +
+ ); + } + + componentDidMount() { + this.updateContentSize(); + } + + componentDidUpdate() { + this.updateContentSize(); + } + + @action + updateContentSize() { + for (const c of this.cells.values()) { + if (c.ref) { + const r = c.ref.getBoundingClientRect(); + c.contentHeight = r.height + 6; + c.contentWidth = r.width + 4; + } + } + } +} + +class CellInfo { + @observable public contentWidth: number | undefined = undefined; + @observable public contentHeight: number | undefined = undefined; + + public contentArea = 0; + public lastContentArea = 0; + + public lastTop = 0; + public top = 0; + public lastLeft = 0; + public left = 0; + + public get distance(): number { + return Math.floor( + Math.pow(this.top - this.lastTop, 2) + + Math.pow(this.left - this.lastLeft, 2) + ); + } + + content!: string; + + ref: HTMLDivElement | null = null; + + public readonly id: string; + public readonly kind: CellData["kind"]; + + constructor(cellData: CellData) { + this.id = cellData.id; + this.kind = cellData.kind; + this.updateCellData(cellData); + } + + public updateCellData(cellData: CellData) { + this.content = cellData.content; + } + + public readonly handleRef = (ref: HTMLDivElement | null) => { + this.ref = ref; + }; +} diff --git a/extension/ui/src/Visualizers/index.ts b/extension/ui/src/Visualizers/index.ts index 5daa932..8e34328 100644 --- a/extension/ui/src/Visualizers/index.ts +++ b/extension/ui/src/Visualizers/index.ts @@ -6,9 +6,10 @@ import { SvgVisualizer } from "./SvgVisualizer"; import { GraphvizDotVisualizer } from "./GraphVisualizer/GraphvizDotVisualizer"; import { TextVisualizer } from "./TextVisualizer/TextVisualizer"; import { MonacoTextVisualizer } from "./TextVisualizer/MonacoTextVisualizer"; -import { AstVisualizer } from "./AstViewer"; +import { AstVisualizer } from "./AstVisualizer"; import { VisJsGraphVisualizer } from "./GraphVisualizer/VisJsGraphVisualizer"; import { PlotlyVisualizer } from "./PlotlyVisualizer"; +import { GridVisualizer } from "./GridVisualizer"; export const knownVisualizations = new ComposedVisualizationProvider([ new TreeVisualizer(), @@ -20,4 +21,5 @@ export const knownVisualizations = new ComposedVisualizationProvider([ new MonacoTextVisualizer(), new AstVisualizer(), new PlotlyVisualizer(), + new GridVisualizer(), ]);