import { Button, Type } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Field, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast } from '../../../../../fields/Types'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../../Utils'; import { DragManager } from '../../../../util/DragManager'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import { DATA_VIZ_TABLE_ROW_HEIGHT } from '../../../global/globalCssVariables.scss'; import './Chart.scss'; interface TableBoxProps { Document: Doc; layoutDoc: Doc; records: { [key: string]: any }[]; selectAxes: (axes: string[]) => void; axes: string[]; width: number; height: number; margin: { top: number; right: number; bottom: number; left: number; }; docView?: () => DocumentView | undefined; } @observer export class TableBox extends React.Component { _inputChangedDisposer?: IReactionDisposer; _containerRef: HTMLDivElement | null = null; @observable _scrollTop = -1; @observable _tableHeight = 0; @observable _tableContainerHeight = 0; 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.handleScroll(); } componentWillUnmount() { this._inputChangedDisposer?.(); } @computed get _tableDataIds() { return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); } @computed get parentViz() { return DocCast(this.props.Document.dataViz_parentViz); // return LinkManager.Instance.getAllRelatedLinks(this.props.Document) // out of all links // .filter(link => link.link_anchor_1 == this.props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @computed get columns() { return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header != '' && header != undefined) : []; } // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table filterSelectedRowsDown = () => { const selected = NumListCast(this.props.layoutDoc.dataViz_selectedRows); this.props.layoutDoc.dataViz_selectedRows = new List(selected.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data const highlighted = NumListCast(this.props.layoutDoc.dataViz_highlitedRows); this.props.layoutDoc.dataViz_highlitedRows = new List(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data }; @computed get viewScale() { return this.props.docView?.()?.props.ScreenToLocalTransform().Scale || 1; } @computed get rowHeight() { return (this.viewScale * this._tableHeight) / this._tableDataIds.length; } @computed get startID() { return this.rowHeight ? Math.floor(this._scrollTop / this.rowHeight) : 0; } @computed get endID() { return Math.ceil(this.startID + (this._tableContainerHeight * this.viewScale) / (this.rowHeight || 1)); } @action handleScroll = () => { if (!this.props.docView?.()?.ContentDiv?.hidden) { this._scrollTop = this._containerRef?.scrollTop ?? 0; } }; @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); } 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?.()!.Document!; const targetCreator = (annotationOn: Doc | undefined) => { const embedding = Doc.MakeEmbedding(this.props.docView?.()!.Document!); embedding._dataViz = DataVizView.TABLE; embedding._dataViz_axes = new List([col, col]); embedding._dataViz_parentViz = this.props.Document; 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.Document; // 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); }) ); }; render() { if (this._tableData.length > 0) { return (
{ if (this.props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { e.stopPropagation(); this.props.layoutDoc.dataViz_selectedRows = new List(this._tableDataIds); } }}>
{ this._containerRef = r; if (!this.props.docView?.()?.ContentDiv?.hidden && r) { this._tableContainerHeight = r.getBoundingClientRect().height ?? 0; 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(); e.stopPropagation(); }, { passive: false } ); } })}> { if (!this.props.docView?.()?.ContentDiv?.hidden && r) { this._tableHeight = r?.getBoundingClientRect().height ?? 0; } })}>
{this.columns.map(col => ( ))} {this._tableDataIds .filter((rowId, i) => this.startID <= i && i <= this.endID) ?.map(rowId => ( this.tableRowClick(e, rowId)} style={{ background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', }}> {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 ( ); })} ))}
this.columnPointerDown(e, col)}> {col}
{this.props.records[rowId][col]}
); } else return ( // when it is a brushed table and the incoming table doesn't have any rows selected
Selected rows of data from the incoming DataVizBox to display.
); } }