import { Colors, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Field, Opt, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { DocUtils, Docs } from '../../../documents/Documents'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; import './DataVizBox.scss'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { SidebarAnnos } from '../../SidebarAnnos'; import { PDFBox } from '../PDFBox'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { emptyFunction, setupMoveUpEvents } from '../../../../Utils'; import { LinkManager } from '../../../util/LinkManager'; export enum DataVizView { TABLE = 'table', LINECHART = 'lineChart', HISTOGRAM = 'histogram', PIECHART = 'pieChart', } @observer export class DataVizBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(DataVizBox, fieldStr); } // all datasets that have been retrieved from the server stored as a map from the dataset url to an array of records static dataset = new ObservableMap(); private _vizRenderer: LineChart | Histogram | PieChart | undefined; private _sidebarRef = React.createRef(); // 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])) ?? []; } // currently chosen visualization type: line, pie, histogram, table @computed get dataVizView(): DataVizView { return StrCast(this.layoutDoc._dataViz, 'table') as DataVizView; } @computed get dataUrl() { return Cast(this.dataDoc[this.fieldKey], CsvField); } @computed.struct get axes() { return StrListCast(this.layoutDoc.dataViz_axes); } selectAxes = (axes: string[]) => (this.layoutDoc.dataViz_axes = new List(axes)); @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { const changedView = this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz); const changedAxes = this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List(StrListCast(data.config_dataVizAxes))); this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows); this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors); this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor; this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors); Object.keys(this.layoutDoc).map(key => { if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { this.layoutDoc['_' + key] = data[key]; } }); const func = () => this._vizRenderer?.restoreView(data); if (changedView || changedAxes) { setTimeout(func, 100); return true; } return func() ?? false; }; getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { const anchor = !pinProps ? this.rootDoc : this._vizRenderer?.getAnchor(pinProps) ?? Docs.Create.ConfigDocument({ // when we clear selection -> we should have it so chartBox getAnchor returns undefined // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) /*put in some options*/ }); anchor.config_dataViz = this.dataVizView; anchor.config_dataVizAxes = this.axes.length ? new List(this.axes) : undefined; anchor.dataViz_selectedRows = Field.Copy(this.layoutDoc.dataViz_selectedRows); anchor.dataViz_histogram_barColors = Field.Copy(this.layoutDoc.dataViz_histogram_barColors); anchor.dataViz_histogram_defaultColor = this.layoutDoc.dataViz_histogram_defaultColor; anchor.dataViz_pie_sliceColors = Field.Copy(this.layoutDoc.dataViz_pie_sliceColors); Object.keys(this.layoutDoc).map(key => { if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { anchor[key] = this.layoutDoc[key]; } }); this.addDocument(anchor); return anchor; }; createNoteAnnotation = () => { const createFunc = undoable( action(() => { const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']); // if (note && this.selectedPin) { // note.latitude = this.selectedPin.latitude; // note.longitude = this.selectedPin.longitude; // note.map = this.selectedPin.map; // } }), 'create note annotation' ); if (!this.layoutDoc.layout_showSidebar) { this.toggleSidebar(); setTimeout(createFunc); } else createFunc(); }; @observable _showSidebar = false; @observable _previewNativeWidth: Opt = undefined; @observable _previewWidth: Opt = undefined; @action toggleSidebar = () => { const prevWidth = this.sidebarWidth(); this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); }; @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } @computed get sidebarHandle() { return (
); } /** * Toggle sidebar onclick the tiny comment button on the top right corner * @param e */ sidebarBtnDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, e, (e, down, delta) => runInAction(() => { const localDelta = this.props .ScreenToLocalTransform() .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const fullWidth = this.props.width; const mapWidth = fullWidth! - this.sidebarWidth(); if (this.sidebarWidth() + localDelta[0] > 0) { this.layoutDoc._layout_showSidebar = true; this.layoutDoc._width = fullWidth! + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth! + localDelta[0])).toString() + '%'; } else { this.layoutDoc._layout_showSidebar = false; this.layoutDoc._width = mapWidth; this.layoutDoc._layout_sidebarWidthPercent = '0%'; } return false; }), emptyFunction, () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') ); }; @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); } sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { console.log('sideBarAddDoc') if (!this.SidebarShown) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); // if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); // const docs = doc instanceof Doc ? [doc] : doc; // docs.forEach(doc => { // let existingPin = Docs.Create.TextDocument("test"); // //let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin; // // if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { // // existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); // // } // if (existingPin) { // setTimeout(() => { // // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet // if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) { // // const anchor = this.getAnchor(true, undefined, existingPin); // const anchor = this.getAnchor(true, undefined); // anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to datapoint' }); // // doc.latitude = existingPin?.latitude; // // doc.longitude = existingPin?.longitude; // } // }); // } // }); //add to annotation list // return this.addDocument(doc, sidebarKey); // add to sidebar list }; sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeDocument(doc, sidebarKey); componentDidMount() { this.props.setContentView?.(this); if (!DataVizBox.dataset.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) this.fetchData(); } fetchData = () => { DataVizBox.dataset.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests fetch('/csvData?uri=' + this.dataUrl?.url.href) // .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res)))); }; // toggles for user to decide which chart type to view the data in renderVizView = () => { 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.SidebarShown? this.props.PanelWidth()*.9/1.2: 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 ; case DataVizView.LINECHART: return (this._vizRenderer = r ?? undefined)} />; case DataVizView.HISTOGRAM: return (this._vizRenderer = r ?? undefined)} />; case DataVizView.PIECHART: return (this._vizRenderer = r ?? undefined)} />; } }; render() { return !this.records.length ? ( // displays how to get data into the DataVizBox if its empty
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.
) : (
e.stopPropagation()} ref={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(); e.stopPropagation(); }, { passive: false } ) }>
(this.layoutDoc._dataViz = DataVizView.TABLE)} toggleStatus={this.layoutDoc._dataViz == DataVizView.TABLE} /> (this.layoutDoc._dataViz = DataVizView.LINECHART)} toggleStatus={this.layoutDoc._dataViz == DataVizView.LINECHART} /> (this.layoutDoc._dataViz = DataVizView.HISTOGRAM)} toggleStatus={this.layoutDoc._dataViz == DataVizView.HISTOGRAM} /> (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == DataVizView.PIECHART} />
{this.renderVizView()}
{this.sidebarHandle}
); } }