From 4aeffc02334fb873341d93f300f3e153499854fd Mon Sep 17 00:00:00 2001 From: srichman333 Date: Thu, 17 Aug 2023 14:12:22 -0400 Subject: layout doc fields organized --- .../nodes/DataVizBox/components/Histogram.tsx | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/Histogram.tsx') diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index b3bdccbbb..1eb57a323 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -107,6 +107,7 @@ export class Histogram extends React.Component { }, { fireImmediately: true } ); + if (!this.props.layoutDoc.dataViz_histogram) this.props.layoutDoc.dataViz_histogram = ''; }; @action @@ -397,7 +398,7 @@ export class Histogram extends React.Component { ) .attr('fill', d => { var barColor; - var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::')); + var barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.map(each => { if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; else { @@ -405,7 +406,7 @@ export class Histogram extends React.Component { if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); - return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.defaultHistogramColor); + return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); }); }; @@ -413,7 +414,7 @@ export class Histogram extends React.Component { this.curBarSelected.attr('fill', color); var barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { if (each.split('::')[0] == barName) barColors.splice(barColors.indexOf(each), 1); }); @@ -421,10 +422,10 @@ export class Histogram extends React.Component { }; @action eraseSelectedColor = () => { - this.curBarSelected.attr('fill', this.props.layoutDoc.defaultHistogramColor); + this.curBarSelected.attr('fill', this.props.layoutDoc.dataViz_histogram_defaultColor); var barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { if (each.split('::')[0] == barName) barColors.splice(barColors.indexOf(each), 1); }); @@ -434,11 +435,11 @@ export class Histogram extends React.Component { this._histogramData; var curSelectedBarName = ''; var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_title_histogram_' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_title_histogram_' + this.props.axes[0]; + if (this.props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0] + '-' + this.props.axes[1]; + else if (this.props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0]; if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this.props.layoutDoc.defaultHistogramColor) this.props.layoutDoc.defaultHistogramColor = '#69b3a2'; - if (!this.props.layoutDoc.histogramBarColors) this.props.layoutDoc.histogramBarColors = new List(); + if (!this.props.layoutDoc.dataViz_histogram_defaultColor) this.props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; + if (!this.props.layoutDoc.dataViz_histogram_barColors) this.props.layoutDoc.dataViz_histogram_barColors = new List(); var selected: string; if (this._currSelected) { curSelectedBarName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { selected += ' }'; } else selected = 'none'; var selectedBarColor; - var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::')); + var barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.map(each => { if (each[0] == curSelectedBarName!) selectedBarColor = each[1]; }); @@ -476,9 +477,9 @@ export class Histogram extends React.Component { tooltip={'Change Default Bar Color'} type={Type.SEC} icon={} - selectedColor={StrCast(this.props.layoutDoc.defaultHistogramColor)} - setFinalColor={undoable(color => (this.props.layoutDoc.defaultHistogramColor = color), 'Change Default Bar Color')} - setSelectedColor={undoable(color => (this.props.layoutDoc.defaultHistogramColor = color), 'Change Default Bar Color')} + selectedColor={StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor)} + setFinalColor={undoable(color => (this.props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} + setSelectedColor={undoable(color => (this.props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} size={Size.XSMALL} /> -- cgit v1.2.3-70-g09d2 From 7335619a157c9bac01d43122a623f1edc1d326da Mon Sep 17 00:00:00 2001 From: srichman333 Date: Fri, 25 Aug 2023 22:30:00 -0400 Subject: merge --- src/client/documents/Documents.ts | 2 +- .../CollectionFreeFormLinksView.tsx | 5 +- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 171 ++++++------- .../nodes/DataVizBox/components/Histogram.tsx | 193 +++++++-------- .../nodes/DataVizBox/components/LineChart.tsx | 100 +++++--- .../views/nodes/DataVizBox/components/PieChart.tsx | 137 +++++------ .../views/nodes/DataVizBox/components/TableBox.tsx | 270 +++++++++++---------- 7 files changed, 449 insertions(+), 429 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/Histogram.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 85119a0be..54b6bac79 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -680,7 +680,7 @@ export namespace Docs { DocumentType.DATAVIZ, { layout: { view: DataVizBox, dataField: defaultDataKey }, - options: { _layout_fitWidth: true, nativeDimModifiable: true }, + options: { dataViz_title: '', dataViz: 'table', _layout_fitWidth: true, nativeDimModifiable: true }, }, ], [ diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 7c869af24..420e6a318 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -12,10 +12,7 @@ export class CollectionFreeFormLinksView extends React.Component { @computed get uniqueConnections() { return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)) .filter(c => !LightboxView.LightboxDoc || (LightboxView.IsLightboxDocView(c.a.docViewPath) && LightboxView.IsLightboxDocView(c.b.docViewPath))) - .map(c => { - console.log("got a connectoin", c) - return ; - }); + .map(c => ); } render() { diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index f936971a6..b98f65054 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,3 +1,4 @@ +import { Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, ObservableMap } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -9,12 +10,11 @@ import { 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 { Histogram } from './components/Histogram'; -import { PieChart } from './components/PieChart'; -import { Toggle, ToggleType, Type } from 'browndash-components'; export enum DataVizView { TABLE = 'table', @@ -29,19 +29,29 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { return FieldView.LayoutString(DataVizBox, fieldStr); } - // all data - static pairSet = new ObservableMap(); - @computed.struct get pairs() { - return DataVizBox.pairSet.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); - } + // 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 _chartRenderer: LineChart | Histogram | PieChart | undefined; + // all CSV records in the dataset + @computed.struct get records() { + return DataVizBox.dataset.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); + } - // current displayed chart type + // 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); @@ -55,7 +65,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { this.layoutDoc['_' + key] = data[key]; } }); - const func = () => this._chartRenderer?.restoreView(data); + const func = () => this._vizRenderer?.restoreView(data); if (changedView || changedAxes) { setTimeout(func, 100); return true; @@ -65,7 +75,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { const anchor = !pinProps ? this.rootDoc - : this._chartRenderer?.getAnchor(pinProps) ?? + : 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) @@ -86,87 +96,78 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { return anchor; }; - @computed.struct get axes() { - return StrListCast(this.layoutDoc.dataViz_axes); + componentDidMount() { + this.props.setContentView?.(this); + if (!DataVizBox.dataset.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) this.fetchData(); } - selectAxes = (axes: string[]) => (this.layoutDoc.dataViz_axes = new List(axes)); + + 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 - @computed get selectView() { + 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.pairs) return 'no data'; - switch (this.dataVizView) { - case DataVizView.TABLE: - return ; - case DataVizView.LINECHART: - return ( - (this._chartRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - pairs={this.pairs} - dataDoc={this.dataDoc} - /> - ); - case DataVizView.HISTOGRAM: - return ( - (this._chartRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - pairs={this.pairs} - dataDoc={this.dataDoc} - /> - ); - case DataVizView.PIECHART: - return ( - (this._chartRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - pairs={this.pairs} - dataDoc={this.dataDoc} - /> - ); + if (this.records) { + switch (this.dataVizView) { + case DataVizView.TABLE: + return ; + case DataVizView.LINECHART: + return ( + (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 ( + (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 ( + (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} + /> + ); + } } - } - - @computed get dataUrl() { - return Cast(this.dataDoc[this.fieldKey], CsvField); - } - - componentDidMount() { - this.props.setContentView?.(this); - this.fetchData(); - if (!this.layoutDoc._dataViz) this.layoutDoc._dataViz = this.dataVizView; - } - - fetchData() { - if (DataVizBox.pairSet.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) return; - DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []); - fetch('/csvData?uri=' + this.dataUrl?.url.href) // - .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res)))); - } + return 'no data/visualization'; + }; render() { - return !this.pairs?.length ? ( + 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.
) : ( @@ -189,7 +190,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { (this.layoutDoc._dataViz = DataVizView.HISTOGRAM)} toggleStatus={this.layoutDoc._dataViz == DataVizView.HISTOGRAM} /> (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == DataVizView.PIECHART} /> - {this.selectView} + {this.renderVizView()} ); } diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 1eb57a323..edb1665f2 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -1,26 +1,26 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components'; +import * as d3 from 'd3'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, StrListCast } from '../../../../../fields/Doc'; import * as React from 'react'; -import * as d3 from 'd3'; -import { IReactionDisposer, action, computed, observable, reaction } from 'mobx'; -import { LinkManager } from '../../../../util/LinkManager'; -import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; -import { PinProps, PresBox } from '../../trails'; -import { Docs } from '../../../../documents/Documents'; -import { List } from '../../../../../fields/List'; -import './Chart.scss'; -import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components'; import { FaFillDrip } from 'react-icons/fa'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; +import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; +import { Docs } from '../../../../documents/Documents'; +import { LinkManager } from '../../../../util/LinkManager'; +import { undoable } from '../../../../util/UndoManager'; +import { PinProps, PresBox } from '../../trails'; import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils'; -import { undoBatch, undoable } from '../../../../util/UndoManager'; +import './Chart.scss'; export interface HistogramProps { rootDoc: Doc; layoutDoc: Doc; axes: string[]; - pairs: { [key: string]: any }[]; + records: { [key: string]: any }[]; width: number; height: number; dataDoc: Doc; @@ -46,44 +46,47 @@ export class Histogram extends React.Component { private selectedData: any = undefined; // Selection of selected bar private hoverOverData: any = undefined; // Selection of bar being hovered over + @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]); + } // filters all data to just display selected data if brushed (created from an incoming link) @computed get _histogramData() { - var guids = StrListCast(this.props.layoutDoc.dataViz_rowGuids); if (this.props.axes.length < 1) return []; if (this.props.axes.length < 2) { var ax0 = this.props.axes[0]; - if (/\d/.test(this.props.pairs[0][ax0])) { + if (/\d/.test(this.props.records[0][ax0])) { this.numericalXData = true; } - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ [ax0]: pair[this.props.axes[0]] })); + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); } var ax0 = this.props.axes[0]; var ax1 = this.props.axes[1]; - if (/\d/.test(this.props.pairs[0][ax0])) { + if (/\d/.test(this.props.records[0][ax0])) { this.numericalXData = true; } - if (/\d/.test(this.props.pairs[0][ax1])) { + if (/\d/.test(this.props.records[0][ax1])) { this.numericalYData = true; } - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ [ax0]: pair[this.props.axes[0]], [ax1]: pair[this.props.axes[1]] })); + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] })); } @computed get defaultGraphTitle() { var ax0 = this.props.axes[0]; var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !ax1 || !/\d/.test(this.props.pairs[0][ax1]) || !this.numericalYData) { + if (this.props.axes.length < 2 || !ax1 || !/\d/.test(this.props.records[0][ax1]) || !this.numericalYData) { return ax0 + ' Histogram'; } else return ax0 + ' by ' + ax1 + ' Histogram'; } - @computed get incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => link.link_anchor_1 == this.props.rootDoc.draggedFrom) // 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 parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => link.link_anchor_1 == this.props.rootDoc.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 rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { @@ -100,11 +103,7 @@ export class Histogram extends React.Component { componentDidMount = () => { this._disposers.chartData = reaction( () => ({ dataSet: this._histogramData, w: this.width, h: this.height }), - ({ dataSet, w, h }) => { - if (dataSet!.length > 0) { - this.drawChart(dataSet, w, h); - } - }, + ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h), { fireImmediately: true } ); if (!this.props.layoutDoc.dataViz_histogram) this.props.layoutDoc.dataViz_histogram = ''; @@ -115,7 +114,6 @@ export class Histogram extends React.Component { // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ - // title: 'histogram doc selection' + this._currSelected, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc); @@ -132,21 +130,15 @@ export class Histogram extends React.Component { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - var validData = dataSet.filter((d: { [x: string]: unknown }) => { - var valid = true; - Object.keys(dataSet[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; - }); - return valid; - }); - var field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; - const data = validData.map((d: { [x: string]: any }) => { - if (this.numericalXData) { - return +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\ !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; + return !field + ? [] + : validData.map((d: { [x: string]: any }) => + !this.numericalXData // + ? d[field] + : +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { d3.select(this._histogramRef.current).select('svg').remove(); d3.select(this._histogramRef.current).select('.tooltip').remove(); - var data = this.data(dataSet); - var xAxisTitle = Object.keys(dataSet[0])[0]; - var yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; - let uniqueArr: unknown[] = [...new Set(data)]; + const data = this.data(dataSet); + const xAxisTitle = Object.keys(dataSet[0])[0]; + const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; + const uniqueArr: unknown[] = [...new Set(data)]; var numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; var translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; if (numBins > this.maxBins) numBins = this.maxBins; - var startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; - var endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; + const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; + const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; // converts data into Objects - var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => { - var valid = true; - Object.keys(dataSet[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; - }); - return valid; - }); + var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); if (!this.numericalXData) { var histStringDataSet: { [x: string]: unknown }[] = []; if (this.numericalYData) { @@ -322,7 +308,7 @@ export class Histogram extends React.Component { // click/hover const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); const onHover = action((e: any) => { - const selected = this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); + this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); updateHighlights(); }); const mouseOut = action((e: any) => { @@ -361,10 +347,10 @@ export class Histogram extends React.Component { 'transform', this.numericalYData ? function (d) { - var eachData = histDataSet.filter((data: { [x: string]: number }) => { + const eachData = histDataSet.filter((data: { [x: string]: number }) => { return data[xAxisTitle] == d[0]; }); - var length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { 'height', this.numericalYData ? function (d) { - var eachData = histDataSet.filter((data: { [x: string]: number }) => { + const eachData = histDataSet.filter((data: { [x: string]: number }) => { return data[xAxisTitle] == d[0]; }); - var length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { 'class', selected ? function (d) { - return selected && selected[0] == d[0] ? 'histogram-bar hover' : 'histogram-bar'; + return selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar'; } : function (d) { return 'histogram-bar'; @@ -398,11 +384,11 @@ export class Histogram extends React.Component { ) .attr('fill', d => { var barColor; - var barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); - barColors.map(each => { + const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + barColors.forEach(each => { if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; else { - var range = StrCast(each[0]).split(' to '); + const range = StrCast(each[0]).split(' to '); if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); @@ -412,26 +398,40 @@ export class Histogram extends React.Component { @action changeSelectedColor = (color: string) => { this.curBarSelected.attr('fill', color); - var barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { - if (each.split('::')[0] == barName) barColors.splice(barColors.indexOf(each), 1); - }); + barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); barColors.push(StrCast(barName + '::' + color)); }; @action eraseSelectedColor = () => { this.curBarSelected.attr('fill', this.props.layoutDoc.dataViz_histogram_defaultColor); - var barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { - if (each.split('::')[0] == barName) barColors.splice(barColors.indexOf(each), 1); - }); + barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); }; + updateBarColors = () => { + var svg = this._histogramSvg; + if (svg) svg.selectAll('rect') + .attr('fill', (d: any) => { + var barColor; + const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + barColors.forEach(each => { + if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; + else { + const range = StrCast(each[0]).split(' to '); + if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; + } + }); + return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); + }); + } + render() { + this.updateBarColors(); this._histogramData; var curSelectedBarName = ''; var titleAccessor: any = ''; @@ -440,25 +440,22 @@ export class Histogram extends React.Component { if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this.props.layoutDoc.dataViz_histogram_defaultColor) this.props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; if (!this.props.layoutDoc.dataViz_histogram_barColors) this.props.layoutDoc.dataViz_histogram_barColors = new List(); - var selected: string; + var selected = 'none'; if (this._currSelected) { curSelectedBarName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { - key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; - }); - selected = selected.substring(0, selected.length - 2); - selected += ' }'; - } else selected = 'none'; + Object.keys(this._currSelected).forEach(key => + key // + ? (selected += key + ': ' + this._currSelected[key] + ', ') + : '' + ); + selected = selected.substring(0, selected.length - 2) + ' }'; + } var selectedBarColor; - var barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); - barColors.map(each => { - if (each[0] == curSelectedBarName!) selectedBarColor = each[1]; - }); - - this.componentDidMount(); + var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::')); + barColors.forEach(each => each[0] === curSelectedBarName && (selectedBarColor = each[1])); - if (this._histogramData.length > 0 || (!this.incomingLinks || this.incomingLinks.length==0)) { + if (this._histogramData.length > 0 || !this.parentViz) { return this.props.axes.length >= 1 ? (
@@ -515,10 +512,8 @@ export class Histogram extends React.Component { ) : ( {'first use table view to select a column to graph'} ); - } 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.
- ); + } + // when it is a brushed table and the incoming table doesn't have any rows selected + return
Selected rows of data from the incoming DataVizBox to display.
; } -} \ No newline at end of file +} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 2b2a5326a..206169c13 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx' import { observer } from 'mobx-react'; import * as React from 'react'; import * as d3 from 'd3'; -import { Doc, DocListCast, StrListCast } from '../../../../../fields/Doc'; +import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc'; import { Id } from '../../../../../fields/FieldSymbols'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; @@ -28,7 +28,7 @@ export interface LineChartProps { rootDoc: Doc; layoutDoc: Doc; axes: string[]; - pairs: { [key: string]: any }[]; + records: { [key: string]: any }[]; width: number; height: number; dataDoc: Doc; @@ -49,32 +49,35 @@ export class LineChart extends React.Component { @observable _currSelected: SelectedDataPoint | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + @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 _lineChartData() { - var guids = StrListCast(this.props.layoutDoc.dataViz_rowGuids); + var guids = StrListCast(this.props.layoutDoc.dataViz_rowIds); if (this.props.axes.length <= 1) return []; - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })) - .sort((a, b) => (a.x < b.x ? -1 : 1)); + return this._tableData.map(record => ({ x: Number(record[this.props.axes[0]]), y: Number(record[this.props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); } @computed get graphTitle() { return this.props.axes[1] + ' vs. ' + this.props.axes[0] + ' Line Chart'; } - @computed get incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => { - return link.link_anchor_1 == this.props.rootDoc.draggedFrom; - }) // 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 parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => { + // return link.link_anchor_1 == this.props.rootDoc.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 incomingSelected() { + @computed get incomingHighlited() { // return selected x and y axes // otherwise, use the selection of whatever is linked to us - return this.incomingLinks // all links that are pointing to this node - .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes - .filter(dvb => dvb) - .map(dvb => dvb.pairs?.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor - .lastElement(); + const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; + const highlitedRowIds = (incomingVizBox && incomingVizBox.rootDoc)? NumListCast(incomingVizBox.rootDoc.dataViz_highlitedRows) : []; + return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { return minMaxRange([this._lineChartData]); @@ -107,23 +110,46 @@ export class LineChart extends React.Component { this._disposers.highlights = reaction( () => ({ selected: this._currSelected, - incomingSelected: this.incomingSelected, + incomingHighlited: this.incomingHighlited, }), - ({ selected, incomingSelected }) => { + ({ selected, incomingHighlited }) => { // redraw annotations when the chart data has changed, or the local or inherited selection has changed - if (selected){ - const elements = document.querySelectorAll('.datapoint'); - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - const x = element.getAttribute('data-x'); - const y = element.getAttribute('data-y'); - if (Number(x) === Number(selected.x) && Number(y) === Number(selected.y)) element.classList.add('selected'); - } - } + this.clearAnnotations(); + selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true); + incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this.props.axes[0]]), Number(record[this.props.axes[1]]))); }, { fireImmediately: true } ); - if (!this.props.layoutDoc.dataViz_lineChart) this.props.layoutDoc.dataViz_lineChart = ''; + }; + + // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that + + clearAnnotations = () => { + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + element.classList.remove('brushed'); + element.classList.remove('selected'); + } + }; + // gets called whenever the "data_annotations" fields gets updated + drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => { + // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements + // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY + // if it exists, then highlight it + // if it doesn't exist, then remove the highlight + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const x = element.getAttribute('data-x'); + const y = element.getAttribute('data-y'); + if (x === dataX.toString() && y === dataY.toString()) { + element.classList.add(selected ? 'selected' : 'brushed'); + } + // TODO: nda - this remove highlight code should go where we remove the links + // } else { + // } + } }; @action @@ -162,7 +188,7 @@ export class LineChart extends React.Component { @computed get defaultGraphTitle() { var ax0 = this.props.axes[0]; var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !/\d/.test(this.props.pairs[0][ax0]) || !ax1) { + if (this.props.axes.length < 2 || !/\d/.test(this.props.records[0][ax0]) || !ax1) { return ax0 + ' Line Chart'; } else return ax1 + ' by ' + ax0 + ' Line Chart'; } @@ -186,7 +212,7 @@ export class LineChart extends React.Component { // TODO: nda - get rid of svg element in the list? if (this._currSelected && this._currSelected.x == x && this._currSelected.y == y) this._currSelected = undefined; else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; - this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true)); + this.props.records.forEach(record => record[this.props.axes[0]] === x && record[this.props.axes[1]] === y && (record.selected = true)); } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear, yScale: d3.ScaleLinear) { @@ -327,10 +353,10 @@ export class LineChart extends React.Component { else if (this.props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0]; if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; const selectedPt = this._currSelected ? `{ ${this.props.axes[0]}: ${this._currSelected.x} ${this.props.axes[1]}: ${this._currSelected.y} }` : 'none'; - if (this._lineChartData.length>0 || (!this.incomingLinks || this.incomingLinks.length==0)){ - return this.props.axes.length>=2 && /\d/.test(this.props.pairs[0][this.props.axes[0]]) && /\d/.test(this.props.pairs[0][this.props.axes[1]]) ? ( -
-
+ if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { + return this.props.axes.length >= 2 && /\d/.test(this.props.records[0][this.props.axes[0]]) && /\d/.test(this.props.records[0][this.props.axes[1]]) ? ( +
+
{ private _disposers: { [key: string]: IReactionDisposer } = {}; private _piechartRef: React.RefObject = React.createRef(); private _piechartSvg: d3.Selection | undefined; - private byCategory: boolean = true; // whether the data is organized by category or by specified number percentages/ratios - @observable _currSelected: any | undefined = undefined; // Object of selected slice private curSliceSelected: any = undefined; // d3 data of selected slice private selectedData: any = undefined; // Selection of selected slice private hoverOverData: any = undefined; // Selection of slice being hovered over + @observable _currSelected: any | undefined = undefined; // Object of selected slice + @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]); + } + + // organized by specified number percentages/ratios if one column is selected and it contains numbers + // otherwise, assume data is organized by categories + @computed get byCategory() { + if (this.props.axes.length === 1) { + return !/\d/.test(this.props.records[0][this.props.axes[0]]); + } + return true; + } // filters all data to just display selected data if brushed (created from an incoming link) @computed get _piechartData() { - var guids = StrListCast(this.props.layoutDoc.dataViz_rowGuids); if (this.props.axes.length < 1) return []; + + const ax0 = this.props.axes[0]; if (this.props.axes.length < 2) { - var ax0 = this.props.axes[0]; - if (/\d/.test(this.props.pairs[0][ax0])) { - this.byCategory = false; - } - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ [ax0]: pair[this.props.axes[0]] })); + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); } - var ax0 = this.props.axes[0]; - var ax1 = this.props.axes[1]; - if (/\d/.test(this.props.pairs[0][ax0])) { - this.byCategory = false; - } - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ [ax0]: pair[this.props.axes[0]], [ax1]: pair[this.props.axes[1]] })); + const ax1 = this.props.axes[1]; + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] })); } @computed get defaultGraphTitle() { var ax0 = this.props.axes[0]; var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !/\d/.test(this.props.pairs[0][ax0]) || !ax1) { + if (this.props.axes.length < 2 || !/\d/.test(this.props.records[0][ax0]) || !ax1) { return ax0 + ' Pie Chart'; - } else return ax1 + ' by ' + ax0 + ' Pie Chart'; + } + return ax1 + ' by ' + ax0 + ' Pie Chart'; } - @computed get incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => link.link_anchor_1 == this.props.rootDoc.draggedFrom) // 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 parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => link.link_anchor_1 == this.props.rootDoc.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 } componentWillUnmount() { @@ -117,21 +123,15 @@ export class PieChart extends React.Component { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - var validData = dataSet.filter((d: { [x: string]: unknown }) => { - var valid = true; - Object.keys(dataSet[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; - }); - return valid; - }); - var field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; - const data = validData.map((d: { [x: string]: any }) => { - if (!this.byCategory) { - return +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\ !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; + return !field + ? undefined + : validData.map((d: { [x: string]: any }) => + this.byCategory + ? d[field] // + : +d[field].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { // converts data into Objects var data = this.data(dataSet); - var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => { - var valid = true; - Object.keys(dataSet[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; - }); - return valid; - }); + var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); if (this.byCategory) { let uniqueCategories = [...new Set(data)]; var pieStringDataSet: { frequency: number }[] = []; @@ -233,7 +227,7 @@ export class PieChart extends React.Component { // click/hover const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet)); const onHover = action((e: any) => { - const selected = this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet); + this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet); updateHighlights(); }); const mouseOut = action((e: any) => { @@ -253,16 +247,19 @@ export class PieChart extends React.Component { // drawing the slices var selected = this.selectedData; var arcs = g.selectAll('arc').data(pie(data)).enter().append('g'); + const sliceColors = StrListCast(this.props.layoutDoc.pieSliceColors).map(each => each.split('::')); + const possibleDataPointVals = pieDataSet.map((each: { [x: string]: any | { valueOf(): number } }) => { + try { + each[percentField] = Number(each[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { - var possibleDataPoints = pieDataSet.filter((each: { [x: string]: any | { valueOf(): number } }) => { - try { - return Number(each[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\ pval[percentField] === Number(d.data)); if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0]; else { dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]]; @@ -270,11 +267,9 @@ export class PieChart extends React.Component { } var sliceColor; if (dataPoint) { - var accessByName = dataPoint[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ each.split('::')); - sliceColors.map(each => { - if (each[0] == StrCast(accessByName)) sliceColor = each[1]; - }); + sliceColors.forEach(each => each[0] == StrCast(accessByName) && (sliceColor = each[1])); } return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length]; }) @@ -317,7 +312,6 @@ export class PieChart extends React.Component { dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]]; trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; } - if (dataPoint[percentField] && dataPoint[percentField]==0) return ''; return dataPoint ? dataPoint[percentField]! + (!descriptionField ? '' : ' - ' + dataPoint[descriptionField])! : ''; }); }; @@ -334,7 +328,6 @@ export class PieChart extends React.Component { }; render() { - this.componentDidMount(); var titleAccessor: any = ''; if (this.props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this.props.axes[0] + '-' + this.props.axes[1]; else if (this.props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + this.props.axes[0]; @@ -357,7 +350,7 @@ export class PieChart extends React.Component { if (each[0] == curSelectedSliceName!) selectedSliceColor = each[1]; }); - if (this._piechartData.length>0 || (!this.incomingLinks || this.incomingLinks.length==0)){ + if (this._piechartData.length > 0 || !this.parentViz) { return this.props.axes.length >= 1 ? (
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index d6e33d976..067dff07a 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,21 +1,21 @@ -import { action, computed } from 'mobx'; +import { action, computed, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, Field, StrListCast } from '../../../../../fields/Doc'; +import { Doc, Field, NumListCast, StrListCast } 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 { LinkManager } from '../../../../util/LinkManager'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; -import { LinkManager } from '../../../../util/LinkManager'; -import { Cast, DocCast } from '../../../../../fields/Types'; import './Chart.scss'; -import { listSpec } from '../../../../../fields/Schema'; interface TableBoxProps { rootDoc: Doc; layoutDoc: Doc; - pairs: { [key: string]: any }[]; + records: { [key: string]: any }[]; selectAxes: (axes: string[]) => void; axes: string[]; width: number; @@ -31,151 +31,159 @@ interface TableBoxProps { @observer export class TableBox extends React.Component { - // filters all data to just display selected data if brushed (created from an incoming link) + _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 }); + } + 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() { - if (this.incomingLinks.length! <= 0) return this.props.pairs; - var guids = StrListCast(this.props.layoutDoc.dataViz_rowGuids); - return this.props.pairs?.filter(pair => this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)])); + return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); } - @computed get incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => { - return link.link_anchor_1 == this.props.rootDoc.draggedFrom; - }) // 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 parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => link.link_anchor_1 == this.props.rootDoc.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() { - if (!this.props.layoutDoc.dataViz_rowGuids) this.props.layoutDoc.dataViz_rowGuids = new List(); - const guids = Cast(this.props.layoutDoc.dataViz_rowGuids, listSpec('string'), null); - if (guids.length == 0) this.props.pairs.map(row => guids.push(Utils.GenerateGuid())); return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header != '' && header != undefined) : []; } - // updates the 'selected' field to no longer include rows that aren't in the table - filterSelectedRowsDown() { - if (!this.props.layoutDoc.dataViz_selectedRows) this.props.layoutDoc.dataViz_selectedRows = new List(); - const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('string'), null); - const incomingSelected = this.incomingLinks.length ? StrListCast(this.incomingLinks[0].dataViz_selectedRows) : undefined; - if (incomingSelected) { - selected.map(guid => { - if (!incomingSelected.includes(guid)) selected.splice(selected.indexOf(guid), 1); - }); // filters through selected to remove guids that were removed in the incoming data - } - } + // 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 selected to remove guids that were removed in the incoming data + }; render() { - this.filterSelectedRowsDown(); if (this._tableData.length > 0) { return ( -
+
+ 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.columns - .filter(col => !col.startsWith('select')) - .map(col => { - const header = React.createRef(); - return ( - - ); - })} + {this.columns.map(col => ( + + ))} - {this._tableData?.map((p, i) => { - var containsData = false; - var guid = StrListCast(this.props.layoutDoc.dataViz_rowGuids)![this.props.pairs.indexOf(p)]; - this.columns.map(col => { - if (p[col] != '' && p[col] != null && p[col] != undefined) containsData = true; - }); - if (containsData) { - return ( - { + {this._tableDataIds + ?.map(rowId => ({ record: this.props.records[rowId], rowId })) + .map(({ record, rowId }) => ( + { + 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 - const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('string'), null); - if (selected.includes(guid)) selected.splice(selected.indexOf(guid), 1); - else { - selected.push(guid); - } - })} - style={{ background: StrListCast(this.props.layoutDoc.dataViz_selectedRows).includes(guid) ? 'lightgrey' : '', width: '110%' }}> - {this.columns.map(col => { - // each cell - var 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 ( - - ); - })} - - ); - } - })} + 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 ( + + ); + })} + + ))}
{ - 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([col, col]); - embedding._draggedFrom = this.props.docView?.()!.rootDoc!; - embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; - embedding.dataViz_histogram_barColors = Field.Copy(this.props.layoutDoc.dataViz_histogram_barColors); - embedding.dataViz_histogram_defaultColor = this.props.layoutDoc.dataViz_histogram_defaultColor; - embedding.dataViz_pie_sliceColors = Field.Copy(this.props.layoutDoc.dataViz_pie_sliceColors); - return embedding; - }; - if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { - DragManager.StartAnchorAnnoDrag([header.current!], 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); - }) - ); - }}> - {col} - { + 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([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); + }) + ); + }}> + {col} +
- {p[col]} -
+ {record[col]} +
-- cgit v1.2.3-70-g09d2 From 0332a80329328e39c244623d5090331b83df339c Mon Sep 17 00:00:00 2001 From: bobzel Date: Sun, 27 Aug 2023 13:31:46 -0400 Subject: minor cleanup of componentDidMount stuff for dataViz --- src/client/documents/Documents.ts | 2 +- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 8 ++------ src/client/views/nodes/DataVizBox/components/Histogram.tsx | 1 - src/client/views/nodes/DataVizBox/components/LineChart.tsx | 3 +-- src/client/views/nodes/DataVizBox/components/PieChart.tsx | 1 - 5 files changed, 4 insertions(+), 11 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/Histogram.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5b68c2d84..7f3309ea8 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -689,7 +689,7 @@ export namespace Docs { DocumentType.DATAVIZ, { layout: { view: DataVizBox, dataField: defaultDataKey }, - options: { dataViz_title: '', dataViz: 'table', _layout_fitWidth: true, nativeDimModifiable: true }, + options: { dataViz_title: '', dataViz_line: '', dataViz_pie: '', dataViz_histogram: '', dataViz: 'table', _layout_fitWidth: true, nativeDimModifiable: true }, }, ], [ diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index b14525993..b9db5fe15 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -35,12 +35,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { // 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) - var nonEmptyRecords = records?.filter(record => { - var notEmpty = false; - Object.keys(record).forEach(key => { if (record[key]!=undefined && record[key]!="") notEmpty = true}); - return notEmpty}) - return nonEmptyRecords; + 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 diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 1d8ab8f2a..e67e2bf31 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -106,7 +106,6 @@ export class Histogram extends React.Component { ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h), { fireImmediately: true } ); - if (!this.props.layoutDoc.dataViz_histogram) this.props.layoutDoc.dataViz_histogram = ''; }; @action diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 8967f27b8..a69d309dc 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -76,7 +76,7 @@ export class LineChart extends React.Component { // return selected x and y axes // otherwise, use the selection of whatever is linked to us const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; - const highlitedRowIds = incomingVizBox && incomingVizBox.rootDoc ? NumListCast(incomingVizBox.rootDoc.dataViz_highlitedRows) : []; + const highlitedRowIds = NumListCast(incomingVizBox?.rootDoc?.dataViz_highlitedRows); return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { @@ -347,7 +347,6 @@ export class LineChart extends React.Component { } render() { - this.componentDidMount(); var titleAccessor: any = ''; if (this.props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0] + '-' + this.props.axes[1]; else if (this.props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0]; diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index 6ad9e0d7e..4e23a114a 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -94,7 +94,6 @@ export class PieChart extends React.Component { }, { fireImmediately: true } ); - if (!this.props.layoutDoc.dataViz_pie) this.props.layoutDoc.dataViz_pie = ''; }; @action -- cgit v1.2.3-70-g09d2