aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-10-10 12:12:33 -0400
committerbobzel <zzzman@gmail.com>2023-10-10 12:12:33 -0400
commita376f30189e847ac763c08288428de415f1ce081 (patch)
tree7a1c401a0584ae2b11378399a91b5e70990a9c1e
parent9f4c6d895eb6ff495a99463e8150c5d1dff26c5b (diff)
fixes for scrolling large datasets in TableBox
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx80
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx267
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>