diff options
author | bobzel <zzzman@gmail.com> | 2023-10-10 12:12:33 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2023-10-10 12:12:33 -0400 |
commit | a376f30189e847ac763c08288428de415f1ce081 (patch) | |
tree | 7a1c401a0584ae2b11378399a91b5e70990a9c1e | |
parent | 9f4c6d895eb6ff495a99463e8150c5d1dff26c5b (diff) |
fixes for scrolling large datasets in TableBox
-rw-r--r-- | src/client/views/nodes/DataVizBox/DataVizBox.tsx | 80 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/TableBox.tsx | 267 |
2 files changed, 142 insertions, 205 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index b9db5fe15..299494c83 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -36,7 +36,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // all CSV records in the dataset (that aren't an empty row) @computed.struct get records() { var records = DataVizBox.dataset.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); - return records?.filter(record => Object.keys(record).some(key => record[key])); + return records?.filter(record => Object.keys(record).some(key => record[key])) ?? []; } // currently chosen visualization type: line, pie, histogram, table @@ -110,70 +110,38 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // toggles for user to decide which chart type to view the data in renderVizView = () => { - const width = this.props.PanelWidth() * 0.9; - const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; - const margin = { top: 10, right: 25, bottom: 75, left: 45 }; - if (this.records) { - switch (this.dataVizView) { - case DataVizView.TABLE: - return <TableBox layoutDoc={this.layoutDoc} records={this.records} axes={this.axes} height={height} width={width} margin={margin} rootDoc={this.rootDoc} docView={this.props.DocumentView} selectAxes={this.selectAxes} />; - case DataVizView.LINECHART: - return ( - <LineChart - layoutDoc={this.layoutDoc} - ref={r => (this._vizRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - records={this.records} - dataDoc={this.dataDoc} - /> - ); - case DataVizView.HISTOGRAM: - return ( - <Histogram - layoutDoc={this.layoutDoc} - ref={r => (this._vizRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - records={this.records} - dataDoc={this.dataDoc} - /> - ); - case DataVizView.PIECHART: - return ( - <PieChart - layoutDoc={this.layoutDoc} - ref={r => (this._vizRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - records={this.records} - dataDoc={this.dataDoc} - /> - ); - } + const sharedProps = { + rootDoc: this.rootDoc, + layoutDoc: this.layoutDoc, + records: this.records, + axes: this.axes, + height: (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9, + width: this.props.PanelWidth() * 0.9, + margin: { top: 10, right: 25, bottom: 75, left: 45 }, + }; + if (!this.records.length) return 'no data/visualization'; + switch (this.dataVizView) { + case DataVizView.TABLE: + return <TableBox {...sharedProps} docView={this.props.DocumentView} selectAxes={this.selectAxes} />; + case DataVizView.LINECHART: + return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; + case DataVizView.HISTOGRAM: + return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; + case DataVizView.PIECHART: + return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; } - return 'no data/visualization'; }; render() { - return !this.records?.length ? ( + return !this.records.length ? ( // displays how to get data into the DataVizBox if its empty <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command 'ctrl + p' to bring the data table to your canvas.</div> ) : ( <div className="dataViz" + style={{ + pointerEvents: this.props.isContentActive() === true ? 'all' : 'none', + }} onWheel={e => e.stopPropagation()} ref={r => r?.addEventListener( diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index dd13c5749..edf0ea4c7 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,6 +1,7 @@ import { Button, Type } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; +import { actionFieldDecorator } from 'mobx/lib/internal'; import * as React from 'react'; import { Doc, Field, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; @@ -32,13 +33,17 @@ interface TableBoxProps { @observer export class TableBox extends React.Component<TableBoxProps> { @observable startID: number = 0; - @observable endID: number = 15; + @observable endID: number = 0; + _tableContainerRef: HTMLDivElement | null = null; + _tableRef = React.createRef<HTMLTableElement>(); _inputChangedDisposer?: IReactionDisposer; componentDidMount() { // if the tableData changes (ie., when records are selected by the parent (input) visulization), // then we need to remove any selected rows that are no longer part of the visualized dataset. this._inputChangedDisposer = reaction(() => this._tableData.slice(), this.filterSelectedRowsDown, { fireImmediately: true }); + + this._tableContainerRef && this.handleScroll(); } componentWillUnmount() { this._inputChangedDisposer?.(); @@ -70,59 +75,88 @@ export class TableBox extends React.Component<TableBoxProps> { this.props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data }; - @action handleScroll = () => { - var container = document.getElementsByClassName("table-container"); - var eachCell = document.getElementsByClassName("table-row"); - if (eachCell.length==0 || container.length==0) return; + @computed get viewScale() { + return this.props.docView?.()?.props.ScreenToLocalTransform().Scale || 1; + } + @computed get rowHeight() { + const tableHeight = this._tableRef.current?.getBoundingClientRect().height; + return !tableHeight ? 1 : (this.viewScale * tableHeight) / this._tableDataIds.length; + } - var useContainer; - if (container.length==1) useContainer = container[0]; - else { - for (var i=0; i<container.length; i++){ - if (container[i].classList.contains(this.columns[0])) useContainer = container[i]; - } + @action handleScroll = () => { + if (this._tableContainerRef) { + this.startID = Math.floor(this._tableContainerRef.scrollTop / this.rowHeight); + this.endID = this.startID + (this._tableContainerRef.getBoundingClientRect().height * this.viewScale) / this.rowHeight; } - var useCell; - if (eachCell.length==1) useCell = eachCell[0]; - else { - for (var i=0; i<eachCell.length; i++){ - if (eachCell[i].classList.contains(this.columns[0])) useCell = eachCell[i]; - } + }; + @action + tableRowClick = (e: React.MouseEvent, rowId: number) => { + const highlited = Cast(this.props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); + const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + if (e.metaKey) { + // highlighting a row + if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); + else highlited?.push(rowId); + if (!selected?.includes(rowId)) selected?.push(rowId); + } else { + // selecting a row + if (selected?.includes(rowId)) { + if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); + selected.splice(selected.indexOf(rowId), 1); + } else selected?.push(rowId); } - if (useCell && useContainer){ - let atEnd = false; - // top - if (useContainer.scrollTop <= 10){ - atEnd = true; - if (this.startID >= -1){ - this.startID -= .001; - this.endID -= .001; + e.stopPropagation(); + }; + + columnPointerDown = (e: React.PointerEvent, col: string) => { + const downX = e.clientX; + const downY = e.clientY; + setupMoveUpEvents( + {}, + e, + e => { + // dragging off a column to create a brushed DataVizBox + const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; + const targetCreator = (annotationOn: Doc | undefined) => { + const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); + embedding._dataViz = DataVizView.TABLE; + embedding._dataViz_axes = new List<string>([col, col]); + embedding._dataViz_parentViz = this.props.rootDoc; + embedding.annotationOn = annotationOn; + embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors); + embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor; + embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors); + return embedding; + }; + if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { + DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { + dragComplete: e => { + if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { + e.linkDocument.link_displayLine = true; + e.linkDocument.link_matchEmbeddings = true; + e.linkDocument.link_displayArrow = true; + // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; + // e.annoDragData.linkSourceDoc.followLinkZoom = false; + } + }, + }); + return true; } - } - // bottom - else if (useContainer.scrollHeight / (useContainer.scrollTop + useContainer.getBoundingClientRect().height) < 1.1 && this.endID<=this._tableDataIds.length){ - this.startID += .015; - this.endID += .015; - } - // regular scroll - else if (this.endID<=this._tableDataIds.length && !atEnd) { - let newStart = (useContainer.scrollTop / useCell.getBoundingClientRect().height ) ; - if (newStart<this.startID && this.startID>=-1){ - this.startID -= .001; - this.endID -= .001; } - else if (newStart>this.startID) { - this.startID += .001; - this.endID += .001; } - } - } - else { - this.endID = this._tableDataIds.length - 1; - } - } + return false; + }, + emptyFunction, + action(e => { + const newAxes = this.props.axes; + if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); + else if (newAxes.length > 1) newAxes[1] = col; + else newAxes.push(col); + this.props.selectAxes(newAxes); + }) + ); + }; render() { if (this._tableData.length > 0) { - return ( <div className="tableBox" @@ -139,24 +173,24 @@ export class TableBox extends React.Component<TableBoxProps> { </div> <div className={`table-container ${this.columns[0]}`} - style={{ height: this.props.height }} - ref={r => + style={{ height: '100%', overflow: 'auto' }} + onScroll={this.handleScroll} + ref={r => { + this._tableContainerRef = r; r?.addEventListener( 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) (e: WheelEvent) => { if (!r.scrollTop && e.deltaY <= 0) e.preventDefault(); - this.handleScroll(); e.stopPropagation(); }, { passive: false } - ) - }> - <table className="table"> + ); + }}> + <table className="table" ref={this._tableRef}> <thead> - <tr > - {this.columns.map(col => { - if (this.startID>0) return; - return ( + <tr> + {this.columns.map(col => + this.startID > 0 ? null : ( <th key={this.columns.indexOf(col)} style={{ @@ -165,106 +199,41 @@ export class TableBox extends React.Component<TableBoxProps> { fontWeight: 'bolder', border: '3px solid black', }} - onPointerDown={e => { - const downX = e.clientX; - const downY = e.clientY; - setupMoveUpEvents( - {}, - e, - e => { - // dragging off a column to create a brushed DataVizBox - const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; - const targetCreator = (annotationOn: Doc | undefined) => { - const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); - embedding._dataViz = DataVizView.TABLE; - embedding._dataViz_axes = new List<string>([col, col]); - embedding._dataViz_parentViz = this.props.rootDoc; - embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; - embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors); - embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor; - embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors); - return embedding; - }; - if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { - DragManager.StartAnchorAnnoDrag( - e.target instanceof HTMLElement ? [e.target] : [], - new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), - downX, - downY, - { - dragComplete: e => { - if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { - e.linkDocument.link_displayLine = true; - e.linkDocument.link_matchEmbeddings = true; - // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; - // e.annoDragData.linkSourceDoc.followLinkZoom = false; - } - }, - } - ); - return true; - } - return false; - }, - emptyFunction, - action(e => { - const newAxes = this.props.axes; - if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); - else if (newAxes.length > 1) newAxes[1] = col; - else newAxes.push(col); - this.props.selectAxes(newAxes); - }) - ); - }}> + onPointerDown={e => this.columnPointerDown(e, col)}> {col} </th> - )} + ) )} </tr> </thead> <tbody> {this._tableDataIds ?.map(rowId => ({ record: this.props.records[rowId], rowId })) - .map(({ record, rowId }) => { - if (this.startID<=rowId && rowId<=this.endID){ - return ( - <tr - key={rowId} - className={`table-row ${this.columns[0]}`} - onClick={action(e => { - const highlited = Cast(this.props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); - const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); - if (e.metaKey) { - // highlighting a row - if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); - else highlited?.push(rowId); - if (!selected?.includes(rowId)) selected?.push(rowId); - } else { - // selecting a row - if (selected?.includes(rowId)) { - if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); - selected.splice(selected.indexOf(rowId), 1); - } else selected?.push(rowId); - } - e.stopPropagation(); - })} - style={{ - background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', - width: '110%', - }}> - {this.columns.map(col => { - // each cell - const colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false; - return ( - <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> - {record[col]} - </td> - ); - })} - </tr> - ) - } - })} + .map(({ record, rowId }) => + this.startID > rowId || rowId > this.endID ? ( + <tr> + <td /> {/* empty row data for out-of-view items needed to give row the default row height so that scrolling works */} + </tr> + ) : ( + <tr + key={rowId} + className={`table-row ${this.columns[0]}`} + onClick={e => this.tableRowClick(e, rowId)} + style={{ + background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', + width: '110%', + }}> + {this.columns.map(col => { + const colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false; + return ( + <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> + <div style={{ textOverflow: 'ellipsis', width: '100%', whiteSpace: 'pre', maxWidth: 150, overflow: 'hidden' }}>{record[col]}</div> + </td> + ); + })} + </tr> + ) + )} </tbody> </table> </div> |