diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 7 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/DataVizBox.scss | 5 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/DataVizBox.tsx | 67 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/Chart.scss | 14 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/Histogram.tsx | 151 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/LineChart.tsx | 211 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/PieChart.tsx | 136 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/TableBox.tsx | 10 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 25 |
9 files changed, 405 insertions, 221 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 8f58ec364..55667684e 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -24,7 +24,12 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { summary: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' }, edit: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' }, completion: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." }, - data: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please keep your response short and to the point." }, + data: { + model: 'gpt-3.5-turbo', + maxTokens: 256, + temp: 0.5, + prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ", + }, }; let lastCall = ''; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index e9a346fbe..9825d926f 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -30,8 +30,13 @@ } .liveSchema-checkBox { + margin-left: 10px; margin-bottom: -35px; } + .filterData-checkBox { + margin-left: 10px; + margin-bottom: -10px; + } .displaySchemaLive { margin-bottom: 20px; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 9ca63194c..e91ed45c3 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -14,7 +14,6 @@ import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Typ import { CsvField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; import { DocUtils } from '../../../documents/DocUtils'; -import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; @@ -32,6 +31,7 @@ import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; +import { DocumentType } from '../../../documents/DocumentTypes'; export enum DataVizView { TABLE = 'table', @@ -138,12 +138,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.layoutDoc['_' + key] = data[key]; } }); - const func = () => this._vizRenderer?.restoreView(data); - if (changedView || changedAxes) { - setTimeout(func, 100); - return true; - } - return func() ?? false; + return true; + // const func = () => this._vizRenderer?.restoreView(data); + // if (changedView || changedAxes) { + // setTimeout(func, 100); + // return true; + // } + // return func() ?? false; }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const visibleAnchor = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); @@ -352,7 +353,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { axes: this.axes, titleCol: this.titleCol, // width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9, - height: (this._props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9, + height: (this._props.PanelHeight() / scale - 55) /* height of 'change view' button */ * 0.8, width: ((this._props.PanelWidth() - this.sidebarWidth()) / scale) * 0.9, margin: { top: 10, right: 25, bottom: 75, left: 45 }, }; @@ -411,11 +412,20 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { GPTPopup.Instance.addDoc = this.sidebarAddDocument; }; + // represents whether or not a data viz box created from a schema table displays live updates to the canvas @action changeLiveSchemaCheckbox = () => { this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive; }; + // represents whether or not clicking on a peice of data in the visualization + // (i.e. a data point in a linechart, a bar on a histogram, or a slice of a pie chart) + // filters the data onto a new data viz doc created off of this one + @action + changeFilteringCheckbox = () => { + this.layoutDoc.dataViz_filterSelection = !this.layoutDoc.dataViz_filterSelection; + }; + specificContextMenu = (): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); @@ -423,17 +433,43 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); }; + askGPT = action(async () => { GPTPopup.Instance.setSidebarId('data_sidebar'); GPTPopup.Instance.addDoc = this.sidebarAddDocument; + GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc; GPTPopup.Instance.setDataJson(''); GPTPopup.Instance.setMode(GPTPopupMode.DATA); const data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); - const input = JSON.stringify(data); - GPTPopup.Instance.setDataJson(input); + GPTPopup.Instance.setDataJson(JSON.stringify(data)); GPTPopup.Instance.generateDataAnalysis(); }); + /** + * creates a new dataviz document filter from this one + * it appears to the right of this document, with the + * parameters passed in being used to create an initial display + */ + createFilteredDoc = (axes?: any) => { + const embedding = Doc.MakeEmbedding(this.Document!); + embedding._layout_showSidebar = false; + embedding._dataViz = DataVizView.LINECHART; + embedding._dataViz_axes = new List<string>(axes); + embedding._dataViz_parentViz = this.Document; + embedding.histogramBarColors = Field.Copy(this.layoutDoc.histogramBarColors); + embedding.defaultHistogramColor = this.layoutDoc.defaultHistogramColor; + embedding.pieSliceColors = Field.Copy(this.layoutDoc.pieSliceColors); + embedding._layout_showSidebar = false; + embedding.width = NumCast(this.layoutDoc._width) - this.sidebarWidth(); + embedding._layout_sidebarWidthPercent = '0%'; + this._props.addDocument?.(embedding); + embedding._dataViz_axes = new List<string>(axes); + this.layoutDoc.dataViz_selectedRows = new List<number>(this.records.map((rec, i) => i)); + embedding.x = Number(embedding.x) + Number(this.Document.width); + + return true; + }; + render() { const scale = this._props.NativeDimScaling?.() || 1; const toggleBtn = (name: string, type: DataVizView) => ( @@ -480,6 +516,12 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { </div> </div> ) : null} + {this.layoutDoc._dataViz !== DataVizView.TABLE ? ( + <div className="filterData-checkBox"> + <Checkbox color="primary" onChange={this.changeFilteringCheckbox} checked={this.layoutDoc.dataViz_filterSelection as boolean} /> + Select data to filter + </div> + ) : null} {this.renderVizView} @@ -540,3 +582,8 @@ Docs.Prototypes.TemplateMap.set(DocumentType.DATAVIZ, { _layout_nativeDimEditable: true, }, }); + +Docs.Prototypes.TemplateMap.set(DocumentType.DATAVIZ, { + layout: { view: DataVizBox, dataField: 'data' }, + options: { dataViz_title: '', dataViz_line: '', dataViz_pie: '', dataViz_histogram: '', dataViz: 'table', _layout_fitWidth: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true }, +}); diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index cf0007cfd..0eb27b65b 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -15,18 +15,12 @@ font-size: larger; display: flex; flex-direction: row; - margin-top: -20px; - margin-bottom: -20px; + margin-top: -35px; } .asHistogram-checkBox { - align-items: left; - align-self: left; - align-content: left; - justify-content: flex-end; - float: left; - left: 0; - position: relative; - margin-bottom: -35px; + margin-left: 10px; + margin-bottom: -10px; + margin-top: -20px; } .selected-data { align-items: center; diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 79b3e9541..14d7e9bf6 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components'; import * as d3 from 'd3'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaFillDrip } from 'react-icons/fa'; @@ -37,14 +37,14 @@ export interface HistogramProps { @observer export class Histogram extends ObservableReactComponent<HistogramProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; - private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _histogramRef: HTMLDivElement | null = null; private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; private numericalXData: boolean = false; // whether the data is organized by numbers rather than categoreis private numericalYData: boolean = false; // whether the y axis is controlled by provided data rather than frequency private maxBins = 15; // maximum number of bins that is readable on a normal sized doc @observable _currSelected: any | undefined = undefined; // Object of selected bar - private curBarSelected: any = undefined; // histogram bin of selected bar - private selectedData: any = undefined; // Selection of selected bar + private curBarSelected: any = undefined; // histogram bin of selected bar for when just one bar is selected + private selectedData: any[] = []; // array of selected bars private hoverOverData: any = undefined; // Selection of bar being hovered over constructor(props: any) { @@ -103,14 +103,24 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } componentDidMount() { - this._disposers.chartData = reaction( - () => ({ dataSet: this._histogramData, w: this.width, h: this.height }), - ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h), - { fireImmediately: true } - ); + // restore selected bars + const svg = this._histogramSvg; + if (svg) { + const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_histogram_selectedData); + svg.selectAll('rect').attr('class', (d: any) => { + let selected = false; + selectedDataBars.forEach(eachSelectedData => { + if (d[0] === eachSelectedData) selected = true; + }); + if (selected) { + this.selectedData.push(d); + return 'histogram-bar hover'; + } + return 'histogram-bar'; + }); + } } - restoreView = () => {}; // 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({ @@ -130,7 +140,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as any))); const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; return !field ? [] @@ -143,14 +153,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // outlines the bar selected / hovered over highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => { - let sameAsCurrent: boolean; let barCounter = -1; const selected = svg.selectAll('.histogram-bar').filter((d: any) => { barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over - if (barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) { + if (d.length && barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) { let showSelected = this.numericalYData - ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0] - : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0]; + ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0] + : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0]; if (this.numericalXData) { // calculating frequency if (d[0] && d[1] && d[0] !== d[1]) { @@ -159,24 +168,59 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } if (changeSelectedVariables) { // for when a bar is selected - not just hovered over - sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] === this._currSelected![xAxisTitle] && showSelected[yAxisTitle] === this._currSelected![yAxisTitle] : false; - this._currSelected = sameAsCurrent ? undefined : showSelected; - this.selectedData = sameAsCurrent ? undefined : d; + let sameAsAny = false; + const selectedDataBars = Cast(this._props.layoutDoc.dataViz_histogram_selectedData, listSpec('number'), null); + this.selectedData.forEach(eachData => { + if (!sameAsAny) { + let match = true; + Object.keys(d).forEach(key => { + if (d[key] !== eachData[key]) match = false; + }); + if (match) { + sameAsAny = true; + const index = this.selectedData.indexOf(eachData); + this.selectedData.splice(index, 1); + selectedDataBars.splice(index, 1); + this._currSelected = undefined; + } + } + }); + if (!sameAsAny) { + this.selectedData.push(d); + selectedDataBars.push(d[0]); + this._currSelected = this.selectedData.length > 1 ? undefined : showSelected; + } + + // for filtering child dataviz docs + if (this._props.layoutDoc.dataViz_filterSelection) { + const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + this._tableDataIds.forEach(rowID => { + let match = false; + for (let i = 0; i < d.length; i++) { + console.log('Compare: ' + this._props.records[rowID][xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') + ' = ' + d[i]); + if (this._props.records[rowID][xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[i]) match = true; + } + if (match && !selectedRows?.includes(rowID)) + selectedRows?.push(rowID); // adding to filtered rows + else if (match && sameAsAny) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows + }); + } } else this.hoverOverData = d; return true; } return false; }); if (changeSelectedVariables) { - if (sameAsCurrent!) this.curBarSelected = undefined; - else this.curBarSelected = selected; + if (this._currSelected) this.curBarSelected = selected; + else this.curBarSelected = undefined; } }; // draws the histogram drawChart = (dataSet: any, width: number, height: number) => { - d3.select(this._histogramRef.current).select('svg').remove(); - d3.select(this._histogramRef.current).select('.tooltip').remove(); + if (dataSet?.length <= 0) return; + d3.select(this._histogramRef).select('svg').remove(); + d3.select(this._histogramRef).select('.tooltip').remove(); const data = this.data(dataSet); const xAxisTitle = Object.keys(dataSet[0])[0]; @@ -189,7 +233,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; // converts data into Objects - let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); + let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as any))); if (!this.numericalXData) { const histStringDataSet: { [x: string]: unknown }[] = []; if (this.numericalYData) { @@ -201,8 +245,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] }); } for (let i = 0; i < data.length; i++) { - const barData = histStringDataSet.filter(each => each[xAxisTitle] === data[i]); - histStringDataSet.filter(each => each[xAxisTitle] === data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1; + const barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]); + histStringDataSet.filter(each => each[xAxisTitle] == data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1; } } histDataSet = histStringDataSet; @@ -210,7 +254,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // initial graph and binning data for histogram const svg = (this._histogramSvg = d3 - .select(this._histogramRef.current) + .select(this._histogramRef) .append('svg') .attr('class', 'graph') .attr('width', width + this._props.margin.right + this._props.margin.left) @@ -242,7 +286,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { for (let i = 0; i < data.length; i++) { let index = 0; for (let j = 0; j < uniqueArr.length; j++) { - if (uniqueArr[j] === data[i]) { + if (uniqueArr[j] == data[i]) { index = j; } } @@ -315,8 +359,15 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { updateHighlights(); }); const updateHighlights = () => { - const { hoverOverData: hoverOverBar, selectedData } = this; - svg.selectAll('rect').attr('class', (d: any) => ((hoverOverBar && hoverOverBar[0] === d[0]) || (selectedData && selectedData[0] === d[0]) ? 'histogram-bar hover' : 'histogram-bar')); + const hoverOverBar = this.hoverOverData; + const { selectedData } = this; + svg.selectAll('rect').attr('class', (d: any) => { + let selected = false; + selectedData.forEach(eachSelectedData => { + if (d[0] === eachSelectedData[0]) selected = true; + }); + return (hoverOverBar && hoverOverBar[0] == d[0]) || selected ? 'histogram-bar hover' : 'histogram-bar'; + }); }; svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut); @@ -343,9 +394,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { 'transform', this.numericalYData ? d => { - const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]); - const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; - return 'translate(' + x(d.x0!) + ',' + y(length) + ')'; + const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] == d[0]); + const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; + return 'translate(' + x(d.x0!) + ',' + y(Number(length)) + ')'; } : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')' ) @@ -353,20 +404,20 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { 'height', this.numericalYData ? d => { - const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]); - const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; - return height - y(length); + const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] == d[0]); + const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; + return height - y(Number(length)); } : d => height - y(d.length) ) .attr('width', eachRectWidth) - .attr('class', selected ? d => (selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar') + .attr('class', selected ? d => (selected && selected[0] == d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar') .attr('fill', d => { let barColor; const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.forEach(each => { // eslint-disable-next-line prefer-destructuring - if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1]; + if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; else { const range = StrCast(each[0]).split(' to '); // eslint-disable-next-line prefer-destructuring @@ -394,15 +445,17 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); }; - updateBarColors = () => { + // reloads the bar colors and selected bars + updateSavedUI = () => { const svg = this._histogramSvg; - if (svg) + if (svg) { + // bar color svg.selectAll('rect').attr('fill', (d: any) => { let barColor; const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.forEach(each => { // eslint-disable-next-line prefer-destructuring - if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1]; + if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; else { const range = StrCast(each[0]).split(' to '); // eslint-disable-next-line prefer-destructuring @@ -411,10 +464,11 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { }); return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); }); + } }; render() { - this.updateBarColors(); + this.updateSavedUI(); this._histogramData; let curSelectedBarName = ''; let titleAccessor: any = 'dataViz_histogram_title'; @@ -423,6 +477,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { 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<string>(); + if (!this._props.layoutDoc.dataViz_histogram_selectedData) this._props.layoutDoc.dataViz_histogram_selectedData = new List<string>(); let selected = 'none'; if (this._currSelected) { curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); @@ -483,7 +538,12 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { size={Size.XSMALL} /> </div> - <div ref={this._histogramRef} /> + <div + ref={r => { + this._histogramRef = r; + r && this.drawChart(this._histogramData, this.width, this.height); + }} + /> {selected !== 'none' ? ( <div className="selected-data"> Selected: {selected} @@ -503,11 +563,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { size={Size.XSMALL} color="black" type={Type.SEC} - tooltip="Revert to the default bar color" - onClick={undoable( - action(() => this.eraseSelectedColor()), - 'Change Selected Bar Color' - )} + tooltip="Revert to the default bar color" // + onClick={undoable(this.eraseSelectedColor, 'Change Selected Bar Color')} /> </div> ) : null} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index bc35ab8c8..80edf2c36 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -3,7 +3,7 @@ import * as d3 from 'd3'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, NumListCast } from '../../../../../fields/Doc'; +import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; @@ -11,16 +11,12 @@ import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; import {} from '../../../DocComponent'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; -import { PinProps, PinDocView } from '../../../PinFuncs'; +import { PinDocView, PinProps } from '../../../PinFuncs'; +import { DocumentView } from '../../DocumentView'; import { DataVizBox } from '../DataVizBox'; -import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; +import { DataPoint, createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; -import { DocumentView } from '../../DocumentView'; -export interface DataPoint { - x: number; - y: number; -} export interface SelectedDataPoint extends DataPoint { elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; } @@ -48,7 +44,8 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - @observable _currSelected: any | undefined = undefined; + @observable _currSelected: DataPoint | undefined = undefined; + // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates constructor(props: any) { super(props); @@ -71,11 +68,6 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Links(this._props.Document) // out of all links - // .filter(link => { - // return 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 incomingHighlited() { // return selected x and y axes @@ -91,6 +83,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } componentDidMount() { + // draw chart this._disposers.chartData = reaction( () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }), ({ dataSet, w, h }) => { @@ -100,31 +93,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { }, { fireImmediately: true } ); - this._disposers.annos = reaction( - () => DocListCast(this._props.dataDoc[this._props.fieldKey + '_annotations']), - (/* annotations */) => { - // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way - // could be blue colored to make it look like anchor - // this.drawAnnotations() - // loop through annotations and draw them - // annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); - // this.drawAnnotations(annotations.x, annotations.y); - }, - { fireImmediately: true } - ); - this._disposers.highlights = reaction( - () => ({ - selected: this._currSelected, - incomingHighlited: this.incomingHighlited, - }), - ({ selected, incomingHighlited }) => { - // redraw annotations when the chart data has changed, or the local or inherited selection has changed - 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 } - ); + + // coloring the selected point + this.colorSelectedPt(); } // 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 @@ -137,12 +108,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { element.classList.remove('selected'); } }; - // gets called whenever the "data_annotations" fields gets updated + + // draws red annotation on data points when selected 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]; @@ -151,26 +119,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { 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 - restoreView = (data: Doc) => { - const coords = Cast(data.config_dataVizSelection, listSpec('number'), null); - if (coords?.length > 1 && (this._currSelected?.x !== coords[0] || this._currSelected?.y !== coords[1])) { - this.setCurrSelected(coords[0], coords[1]); - return true; - } - if (this._currSelected) { - this.setCurrSelected(); - return true; - } - return false; - }; - // 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({ @@ -182,6 +133,24 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { return anchor; }; + private colorSelectedPt() { + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const x = Number(elements[i].getAttribute('data-x')); + const y = Number(elements[i].getAttribute('data-y')); + const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_lineChart_selectedData); + let selected = false; + selectedDataBars.forEach(eachSelectedData => { + // parse each selected point into x,y + const xy = eachSelectedData.split(','); + if (Number(xy[0]) === x && Number(xy[1]) === y) selected = true; + }); + if (selected) { + this.drawAnnotations(x, y, false); + } + } + } + @computed get height() { return this._props.height - this._props.margin.top - this._props.margin.bottom; } @@ -212,18 +181,54 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .style('font-size', '12px'); } - // TODO: nda - use this everyewhere we update currSelected? @action - setCurrSelected(x?: number, y?: number) { - // 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.records.forEach(record => { - record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true); + setCurrSelected(d: DataPoint) { + let ptWasSelected = false; + const selectedDatapoints = Cast(this._props.layoutDoc.dataViz_lineChart_selectedData, listSpec('string'), null); + selectedDatapoints?.forEach(eachData => { + if (!ptWasSelected) { + const [dx, dy] = eachData.split(','); + if (Number(dx) === d.x && Number(dy) === d.y) { + ptWasSelected = true; + const index = selectedDatapoints.indexOf(eachData); + selectedDatapoints.splice(index, 1); + this._currSelected = undefined; + } + } }); + if (!ptWasSelected) { + selectedDatapoints.push(d.x + ',' + d.y); + this._currSelected = selectedDatapoints.length > 1 ? undefined : d; + } + + // for filtering child dataviz docs + if (this._props.layoutDoc.dataViz_filterSelection) { + const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + this._tableDataIds.forEach(rowID => { + if ( + Number(this._props.records[rowID][this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) === d.x && // + Number(this._props.records[rowID][this._props.axes[1]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) === d.y + ) { + if (!selectedRows?.includes(rowID)) + selectedRows?.push(rowID); // adding to filtered rows + else if (ptWasSelected) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows + } + }); + } + + // coloring the selected point + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const x = Number(elements[i].getAttribute('data-x')); + const y = Number(elements[i].getAttribute('data-y')); + if (x === d.x && y === d.y) { + if (ptWasSelected) elements[i].classList.remove('brushed'); + else elements[i].classList.add('brushed'); + } + } } - drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { + drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>, higlightFocusPt: any, tooltip: any) { if (this._lineChartSvg) { const circleClass = '.circle-' + idx; this._lineChartSvg @@ -235,7 +240,22 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .attr('data-x', d => d.x) - .attr('data-y', d => d.y); + .attr('data-y', d => d.y) + .on('mouseenter', e => { + const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) }; + this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); + higlightFocusPt.style('display', null); + }) + .on('mouseleave', () => { + tooltip?.transition().duration(300).style('opacity', 0); + }) + .on('click', (e: any) => { + const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) }; + // find .circle-d1 with data-x = d0.x and data-y = d0.y + this.setCurrSelected(d0); + this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); + this.colorSelectedPt(); + }); } } @@ -286,9 +306,13 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { xAxisCreator(svg.append('g'), height, xScale); yAxisCreator(svg.append('g'), width, yScale); + const higlightFocusPt = svg.append('g').style('display', 'none'); + higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle'); + const tooltip = this.setupTooltip(); + if (validSecondData) { drawLine(svg.append('path'), validSecondData, lineGen, true); - this.drawDataPoints(validSecondData, 0, xScale, yScale); + this.drawDataPoints(validSecondData, 0, xScale, yScale, higlightFocusPt, tooltip); svg.append('path').attr('stroke', 'red'); // legend @@ -320,45 +344,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { // draw the plot line drawLine(svg.append('path'), validData, lineGen, false); - // draw the datapoint circle - this.drawDataPoints(validData, 0, xScale, yScale); - - const higlightFocusPt = svg.append('g').style('display', 'none'); - higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle'); - const tooltip = this.setupTooltip(); - // add all the tooltipContent to the tooltip - const mousemove = action((e: any) => { - const bisect = d3.bisector((d: DataPoint) => d.x).left; - const xPos = d3.pointer(e)[0]; - const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis - const d0 = data[x0]; - if (d0) this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); - - this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); - }); - - const onPointClick = action((e: any) => { - const bisect = d3.bisector((d: DataPoint) => d.x).left; - const xPos = d3.pointer(e)[0]; - const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis - const d0 = data[x0]; - // find .circle-d1 with data-x = d0.x and data-y = d0.y - svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); - this.setCurrSelected(d0.x, d0.y); - this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); - }); - svg.append('rect') - .attr('class', 'overlay') - .attr('width', width) - .attr('height', this.height + margin.top + margin.bottom) - .attr('fill', 'none') - .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`) - .style('opacity', 0) - .on('mouseover', () => higlightFocusPt.style('display', null)) - .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0)) - .on('mousemove', mousemove) - .on('click', onPointClick); + // draw the datapoint circle + this.drawDataPoints(validData, 0, xScale, yScale, higlightFocusPt, tooltip); // axis titles svg.append('text') @@ -396,14 +384,15 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; + if (!this._props.layoutDoc.dataViz_lineChart_selectedData) this._props.layoutDoc.dataViz_lineChart_selectedData = new List<string>(); const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; let selectedTitle = ''; if (this._currSelected && this._props.titleCol) { selectedTitle += '\n' + this._props.titleCol + ': '; this._tableData.forEach(each => { let mapThisEntry = false; - if (this._currSelected.x === each[this._props.axes[0]] && this._currSelected.y === each[this._props.axes[1]]) mapThisEntry = true; - else if (this._currSelected.y === each[this._props.axes[0]] && this._currSelected.x === each[this._props.axes[1]]) mapThisEntry = true; + if (this._currSelected?.x === each[this._props.axes[0]] && this._currSelected?.y === each[this._props.axes[1]]) mapThisEntry = true; + else if (this._currSelected?.y === each[this._props.axes[0]] && this._currSelected?.x === each[this._props.axes[1]]) mapThisEntry = true; if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ', '; }); selectedTitle = selectedTitle.slice(0, -1).slice(0, -1); diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index ef6d1d412..c82496f1a 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -36,10 +36,10 @@ export interface PieChartProps { @observer export class PieChart extends ObservableReactComponent<PieChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; - private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _piechartRef: HTMLDivElement | null = null; private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - private curSliceSelected: any = undefined; // d3 data of selected slice - private selectedData: any = undefined; // Selection of selected slice + private curSliceSelected: any = undefined; // d3 data of selected slice for when just one slice is selected + private selectedData: any[] = []; // array of selected slices private hoverOverData: any = undefined; // Selection of slice being hovered over @observable _currSelected: any | undefined = undefined; // Object of selected slice @@ -84,24 +84,31 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Links(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 } componentWillUnmount() { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } componentDidMount() { - this._disposers.chartData = reaction( - () => ({ dataSet: this._pieChartData, w: this.width, h: this.height }), - ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h), - { fireImmediately: true } - ); + // restore selected slices + const svg = this._piechartSvg; + if (svg && this._pieChartData[0]) { + const key = Object.keys(this._pieChartData[0])[0]; + const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_pie_selectedData); + svg.selectAll('path').attr('class', (d: any) => { + let selected = false; + selectedDataBars.forEach(eachSelectedData => { + if (d[key] === eachSelectedData) selected = true; + }); + if (selected) { + this.selectedData.push(d); + return 'slice hover'; + } + return 'slice'; + }); + } } - @action - restoreView = (/* data: Doc */) => {}; // 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({ @@ -122,7 +129,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] /* || isNaN(d[key] as any) */)); const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; return !field ? undefined @@ -136,7 +143,6 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // outlines the slice selected / hovered over highlightSelectedSlice = (changeSelectedVariables: boolean, svg: any, arc: any, radius: any, pointer: any, pieDataSet: any) => { let index = -1; - let sameAsCurrent: boolean; const selected = svg.selectAll('.slice').filter((d: any) => { index++; const p1 = [0, 0]; // center of pie @@ -160,31 +166,63 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { if (Math.min(p4[1], p1[1]) <= pointer[1] && pointer[1] <= Math.max(p4[1], p1[1])) { if (pointer[0] <= ((pointer[1] - p4[1]) * (p1[0] - p4[0])) / (p1[1] - p4[1]) + p4[0]) lineCrossCount++; } - if (lineCrossCount % 2 !== 0) { + if (lineCrossCount % 2 !== 0 || d.startAngle % (2 * Math.PI) === d.endAngle % (2 * Math.PI)) { // inside the slice of it crosses an odd number of edges const showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; + let key = 'data'; // key that represents slice + // eslint-disable-next-line prefer-destructuring + if (Object.keys(showSelected)[0] === 'frequency') key = Object.keys(showSelected)[1]; if (changeSelectedVariables) { - // for when a bar is selected - not just hovered over - sameAsCurrent = this._currSelected - ? showSelected[Object.keys(showSelected)[0]] === this._currSelected![Object.keys(showSelected)[0]] && showSelected[Object.keys(showSelected)[1]] === this._currSelected![Object.keys(showSelected)[1]] - : this._currSelected === showSelected; - this._currSelected = sameAsCurrent ? undefined : showSelected; - this.selectedData = sameAsCurrent ? undefined : d; + let sameAsAny = false; + const selectedDataSlices = Cast(this._props.layoutDoc.dataViz_pie_selectedData, listSpec('number'), null); + this.selectedData.forEach(eachData => { + if (!sameAsAny) { + let match = true; + Object.keys(d).forEach(objKey => { + if (d[objKey] !== eachData[objKey]) match = false; + }); + if (match) { + sameAsAny = true; + const selIndex = this.selectedData.indexOf(eachData); + this.selectedData.splice(selIndex, 1); + selectedDataSlices.splice(selIndex, 1); + this._currSelected = undefined; + } + } + }); + if (!sameAsAny) { + this.selectedData.push(d); + selectedDataSlices.push(d[key]); + this._currSelected = this.selectedData.length > 1 ? undefined : showSelected; + } + + // for filtering child dataviz docs + if (this._props.layoutDoc.dataViz_filterSelection) { + const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + this._tableDataIds.forEach(rowID => { + let match = false; + if (this._props.records[rowID][this._props.axes[0]] == d[key]) match = true; + if (match && !selectedRows?.includes(rowID)) + selectedRows?.push(rowID); // adding to filtered rows + else if (match && sameAsAny) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows + }); + } } else this.hoverOverData = d; return true; } return false; }); if (changeSelectedVariables) { - if (sameAsCurrent!) this.curSliceSelected = undefined; - else this.curSliceSelected = selected; + if (this._currSelected) this.curSliceSelected = selected; + else this.curSliceSelected = undefined; } }; // draws the pie chart drawChart = (dataSet: any, width: number, height: number) => { - d3.select(this._piechartRef.current).select('svg').remove(); - d3.select(this._piechartRef.current).select('.tooltip').remove(); + if (!dataSet?.length) return; + d3.select(this._piechartRef).select('svg').remove(); + d3.select(this._piechartRef).select('.tooltip').remove(); let percentField = Object.keys(dataSet[0])[0]; let descriptionField = Object.keys(dataSet[0])[1]!; @@ -192,7 +230,8 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // converts data into Objects let data = this.data(dataSet); - let pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); + let pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key])); + if (!pieDataSet.length) return; if (this.byCategory) { const uniqueCategories = [...new Set(data)]; const pieStringDataSet: { frequency: number }[] = []; @@ -201,10 +240,11 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } for (let i = 0; i < data.length; i++) { // eslint-disable-next-line no-loop-func - const sliceData = pieStringDataSet.filter((each: any) => each[percentField] === data[i]); + const sliceData = pieStringDataSet.filter((each: any) => each[percentField] == data[i]); sliceData[0].frequency += 1; } pieDataSet = pieStringDataSet; + if (!pieDataSet.length) return; [percentField, descriptionField] = Object.keys(pieDataSet[0]); data = this.data(pieStringDataSet); } @@ -215,7 +255,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // initial chart const svg = (this._piechartSvg = d3 - .select(this._piechartRef.current) + .select(this._piechartRef) .append('svg') .attr('class', 'graph') .attr('width', width + this._props.margin.right + this._props.margin.left) @@ -228,10 +268,15 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { const updateHighlights = () => { const hoverOverSlice = this.hoverOverData; const { selectedData } = this; - svg.selectAll('path').attr('class', (d: any) => - (selectedData && d.startAngle === selectedData.startAngle && d.endAngle === selectedData.endAngle) || (hoverOverSlice && d.startAngle === hoverOverSlice.startAngle && d.endAngle === hoverOverSlice.endAngle) ? 'slice hover' : 'slice' - ); + svg.selectAll('path').attr('class', (d: any) => { + let selected = false; + selectedData.forEach((eachSelectedData: any) => { + if (d.startAngle === eachSelectedData.startAngle) selected = true; + }); + return selected || (hoverOverSlice && d.startAngle === hoverOverSlice.startAngle && d.endAngle === hoverOverSlice.endAngle) ? 'slice hover' : 'slice'; + }); }; + // click/hover const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet)); const onHover = action((e: any) => { @@ -242,7 +287,6 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { this.hoverOverData = undefined; updateHighlights(); }); - // drawing the slices const selected = this.selectedData; const arcs = g.selectAll('arc').data(pie(data)).enter().append('g'); @@ -259,8 +303,15 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { possibleDataPointVals.push(dataPointVal); }); const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + + // to make sure all important slice information is on 'd' object + let addKey: any = false; + if (pieDataSet.length && Object.keys(pieDataSet[0])[0] === 'frequency') { + // eslint-disable-next-line prefer-destructuring + addKey = Object.keys(pieDataSet[0])[1]; + } arcs.append('path') - .attr('fill', (d, i) => { + .attr('fill', (d: any, i) => { let dataPoint; const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); if (possibleDataPoints.length === 1) [dataPoint] = possibleDataPoints; @@ -270,6 +321,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } let sliceColor; if (dataPoint) { + if (addKey) d[addKey] = dataPoint[addKey]; // adding all slice information to d const sliceTitle = dataPoint[this._props.axes[0]]; const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle; sliceColors.forEach(each => { @@ -279,7 +331,13 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length]; }) - .attr('class', selected ? d => (selected && d.startAngle === selected.startAngle && d.endAngle === selected.endAngle ? 'slice hover' : 'slice') : () => 'slice') + .attr('class', d => { + let selectThisData = false; + selected.forEach((eachSelectedData: any) => { + if (d.startAngle === eachSelectedData.startAngle) selectThisData = true; + }); + return selectThisData ? 'slice hover' : 'slice'; + }) // @ts-ignore .attr('d', arc) .on('click', onPointClick) @@ -335,6 +393,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this._props.layoutDoc.dataViz_pie_sliceColors) this._props.layoutDoc.dataViz_pie_sliceColors = new List<string>(); + if (!this._props.layoutDoc.dataViz_pie_selectedData) this._props.layoutDoc.dataViz_pie_selectedData = new List<string>(); let selected: string; let curSelectedSliceName = ''; if (this._currSelected) { @@ -388,7 +447,12 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { Organize data as histogram </div> ) : null} - <div ref={this._piechartRef} /> + <div + ref={r => { + this._piechartRef = r; + this.drawChart(this._pieChartData, this.width, this.height); + }} + /> {selected !== 'none' ? ( <div className="selected-data"> Selected: {selected} diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 5cd77e274..bcd8e54f2 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,7 +1,7 @@ /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ /* eslint-disable jsx-a11y/no-static-element-interactions */ import { Button, Type } from 'browndash-components'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, setupMoveUpEvents } from '../../../../../ClientUtils'; @@ -64,7 +64,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { // 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 }); const selected = NumListCast(this._props.layoutDoc.dataViz_selectedRows); - if (selected.length > 0) this.hasRowsToFilter = true; + if (selected.length > 0) runInAction(() => (this.hasRowsToFilter = true)); this.handleScroll(); } componentWillUnmount() { @@ -141,7 +141,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { const targetCreator = (annotationOn: Doc | undefined) => { const embedding = Doc.MakeEmbedding(this._props.docView?.()!.Document!); embedding._dataViz = DataVizView.TABLE; - embedding._dataViz_axes = new List<string>([col, col]); + embedding._dataViz_axes = new List<string>([col]); embedding._dataViz_parentViz = this._props.Document; embedding.annotationOn = annotationOn; embedding.histogramBarColors = Field.Copy(this._props.layoutDoc.histogramBarColors); @@ -188,8 +188,8 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { let start: any; let end: any; if (this.filteringType === 'Range') { - start = (this.filteringVal[0] as Number) ? Number(this.filteringVal[0]) : this.filteringVal[0]; - end = (this.filteringVal[1] as Number) ? Number(this.filteringVal[1]) : this.filteringVal[0]; + start = Number.isNaN(Number(this.filteringVal[0])) ? this.filteringVal[0] : Number(this.filteringVal[0]); + end = Number.isNaN(Number(this.filteringVal[1])) ? this.filteringVal[1] : Number(this.filteringVal[1]); } this._tableDataIds.forEach(rowID => { diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index c1bfdf176..2680644ac 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -32,6 +32,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { // eslint-disable-next-line no-use-before-define static Instance: GPTPopup; @observable private chatMode: boolean = false; + private correlatedColumns: string[] = []; @observable public visible: boolean = false; @@ -122,6 +123,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { }; public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false; + public createFilteredDoc: (axes?: any) => boolean = () => false; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; /** @@ -151,6 +153,10 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { return undefined; }; + /** + * Completes an API call to generate a summary of + * this.selectedText in the popup. + */ generateSummary = async () => { GPTPopup.Instance.setVisible(true); GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY); @@ -165,12 +171,21 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { GPTPopup.Instance.setLoading(false); }; + /** + * Completes an API call to generate an analysis of + * this.dataJson in the popup. + */ generateDataAnalysis = async () => { GPTPopup.Instance.setVisible(true); GPTPopup.Instance.setLoading(true); try { const res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt); - GPTPopup.Instance.setText(res || 'Something went wrong.'); + const json = JSON.parse(res! as string); + const keys = Object.keys(json); + this.correlatedColumns = []; + this.correlatedColumns.push(json[keys[0]]); + this.correlatedColumns.push(json[keys[1]]); + GPTPopup.Instance.setText(json[keys[2]] || 'Something went wrong.'); } catch (err) { console.error(err); } @@ -197,6 +212,13 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { }; /** + * Creates a histogram to show the correlation relationship that was found + */ + private createVisualization = () => { + this.createFilteredDoc(this.correlatedColumns); + }; + + /** * Transfers the image urls to actual image docs */ private transferToImage = (source: string) => { @@ -357,6 +379,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { ) : ( <> <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> + <Button tooltip="Create a graph to visualize the correlation results" text="Visualize" onClick={this.createVisualization} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> </> ) |