diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 4 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/DataVizBox.scss | 5 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/DataVizBox.tsx | 50 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/Chart.scss | 14 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/Histogram.tsx | 101 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/LineChart.tsx | 125 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/PieChart.tsx | 98 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/TableBox.tsx | 9 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 27 |
9 files changed, 314 insertions, 119 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 30194f9f8..8747a00a6 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -24,7 +24,9 @@ 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 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. " }, + }; /** 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 60c5fdba2..01258a996 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -29,6 +29,7 @@ import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; import { Checkbox } from '@mui/material'; import { ContextMenu } from '../../ContextMenu'; +import { DragManager } from '../../../util/DragManager'; export enum DataVizView { TABLE = 'table', @@ -346,7 +347,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im 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 }, }; @@ -400,11 +401,20 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im 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 = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); @@ -417,6 +427,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im 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); let data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); @@ -425,6 +436,31 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im 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, type?: DataVizView) => { + const embedding = Doc.MakeEmbedding(this.Document!); + embedding._layout_showSidebar = false; + embedding._dataViz = type? type : 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; return !this.records.length ? ( @@ -454,11 +490,17 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im {(this.layoutDoc && this.layoutDoc.dataViz_asSchema)?( <div className={'displaySchemaLive'}> <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}> - <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> - Display Live Updates to Canvas + <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> + Display Live Updates to Canvas + </div> </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} 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 6672603f3..110626923 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -43,8 +43,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { 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) { @@ -104,11 +104,29 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } componentDidMount() { + // draw histogram 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 + var 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' + } + else return 'histogram-bar'; + }); + } } @action @@ -162,16 +180,49 @@ 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; + let 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++){ + 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; } }; @@ -321,7 +372,11 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const hoverOverBar = this.hoverOverData; const selectedData = this.selectedData; svg.selectAll('rect').attr('class', function (d: any) { - return (hoverOverBar && hoverOverBar[0] == d[0]) || (selectedData && selectedData[0] == d[0]) ? 'histogram-bar hover' : 'histogram-bar'; + 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); @@ -352,8 +407,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const eachData = histDataSet.filter((data: { [x: string]: number }) => { return data[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 length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + return 'translate(' + x(d.x0!) + ',' + y(Number(length)) + ')'; } : function (d) { return 'translate(' + x(d.x0!) + ',' + y(d.length) + ')'; @@ -366,8 +421,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const eachData = histDataSet.filter((data: { [x: string]: number }) => { return data[xAxisTitle] == d[0]; }); - const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; - return height - y(length); + const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + return height - y(Number(length)); } : function (d) { return height - y(d.length); @@ -376,13 +431,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .attr('width', eachRectWidth) .attr( 'class', - selected - ? function (d) { - return selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar'; - } - : function (d) { - return 'histogram-bar'; - } + function (d) { + let selectThisData = false; + selected.forEach(eachSelectedData => { + if (d[0]==eachSelectedData[0]) selectThisData = true; + }) + return selectThisData ? 'histogram-bar hover' : 'histogram-bar'; + } ) .attr('fill', d => { var barColor; @@ -415,9 +470,11 @@ 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 = () => { var svg = this._histogramSvg; - if (svg) + if (svg) { + // bar color svg.selectAll('rect').attr('fill', (d: any) => { var barColor; const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); @@ -430,10 +487,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; var curSelectedBarName = ''; var titleAccessor: any = 'dataViz_histogram_title'; @@ -442,6 +500,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>(); var selected = 'none'; if (this._currSelected) { curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index e093ec648..e79f4cde5 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -48,6 +48,8 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; @observable _currSelected: any | undefined = undefined; + private selectedData: any[] = []; // array of selected data points + // 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 +73,6 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(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 +88,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 +98,23 @@ 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 + 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 + let xy = eachSelectedData.split(','); + if (Number(xy[0])===x && Number(xy[1])===y) selected = true; + }) + if (selected) { + this.drawAnnotations(x, y, false); + } + } } // 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 +127,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,25 +138,11 @@ 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; - }; + restoreView = (data: Doc) => {}; // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { @@ -211,13 +184,49 @@ 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 sameAsAny = false; + const selectedDatapoints = Cast(this._props.layoutDoc.dataViz_lineChart_selectedData, listSpec('string'), null); + this.selectedData.forEach(eachData => { + if (!sameAsAny){ + if (eachData.x==d.x && eachData.y==d.y) { + sameAsAny = true; + let index = this.selectedData.indexOf(eachData) + this.selectedData.splice(index, 1); + selectedDatapoints.splice(index, 1); + this._currSelected = undefined; + } + } + }) + if(!sameAsAny) { + this.selectedData.push(d); + selectedDatapoints.push(d.x + "," + d.y); + this._currSelected = this.selectedData.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 (this._props.records[rowID][this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')==d.x + && 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 (sameAsAny) 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 (sameAsAny) 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>) { @@ -345,8 +354,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { 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 - const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); - this.setCurrSelected(d0.x, d0.y); + this.setCurrSelected(d0); this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); }); @@ -398,6 +406,7 @@ 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 = 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'; var selectedTitle = ""; if (this._currSelected && this._props.titleCol){ diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index fc23f47de..5c341e0b4 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -38,8 +38,8 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef(); 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,20 +84,35 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links - // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link - // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } componentWillUnmount() { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } componentDidMount() { + // draw chart 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 + var svg = this._piechartSvg; + if (svg && this._pieChartData[0]) { + let 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' + } + else return 'slice'; + }); + } } @action @@ -163,21 +178,54 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { if (lineCrossCount % 2 != 0) { // inside the slice of it crosses an odd number of edges var showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; + let key = 'data' // key that represents slice + 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(key => { + if (d[key] != eachData[key]) match = false; + }) + if (match) { + sameAsAny = true; + let index = this.selectedData.indexOf(eachData) + this.selectedData.splice(index, 1); + selectedDataSlices.splice(index, 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][key] == 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; } }; @@ -237,9 +285,11 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { const hoverOverSlice = this.hoverOverData; const selectedData = this.selectedData; svg.selectAll('path').attr('class', function (d: any) { - return (selectedData && d.startAngle == selectedData.startAngle && d.endAngle == selectedData.endAngle) || (hoverOverSlice && d.startAngle == hoverOverSlice.startAngle && d.endAngle == hoverOverSlice.endAngle) - ? 'slice hover' - : 'slice'; + 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'; }); }; @@ -257,8 +307,14 @@ 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 (Object.keys(pieDataSet[0])[0]=='frequency'){ + addKey = Object.keys(pieDataSet[0])[1] + } arcs.append('path') - .attr('fill', (d, i) => { + .attr('fill', (d: any, i) => { var dataPoint; const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0]; @@ -268,6 +324,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } var 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 => each[0] == accessByName && (sliceColor = each[1])); @@ -276,13 +333,13 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { }) .attr( 'class', - selected - ? function (d) { - return selected && d.startAngle == selected.startAngle && d.endAngle == selected.endAngle ? 'slice hover' : 'slice'; - } - : function (d) { - return 'slice'; - } + function (d: any) { + let selectThisData = false; + selected.forEach((eachSelectedData: any) => { + if (d.startAngle==eachSelectedData.startAngle) selectThisData = true; + }) + return selectThisData ? 'slice hover' : 'slice'; + } ) // @ts-ignore .attr('d', arc) @@ -337,6 +394,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { else if (this._props.axes.length > 0) titleAccessor = 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>(); var selected: string; var curSelectedSliceName = ''; if (this._currSelected) { diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 67e1c67bd..7ad5a0e6b 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -53,6 +53,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { makeObservable(this); } + @action 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. @@ -137,7 +138,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); @@ -185,9 +186,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { filter = undoable((e: any) => { var start: any; var 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]; + if (this.filteringType === 'Range') { + 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 29b1ca365..40946cd36 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -15,6 +15,7 @@ import { DocUtils, Docs } from '../../../documents/Documents'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { AnchorMenu } from '../AnchorMenu'; import './GPTPopup.scss'; +import { DataVizView } from '../../nodes/DataVizBox/DataVizBox'; export enum GPTPopupMode { SUMMARY, @@ -29,6 +30,7 @@ interface GPTPopupProps {} export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { static Instance: GPTPopup; @observable private chatMode: boolean = false; + private correlatedColumns: string[] = [] @observable public visible: boolean = false; @@ -121,6 +123,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { }; public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false; + public createFilteredDoc: (axes?: any, type?: DataVizView) => boolean = () => false; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; /** @@ -149,6 +152,10 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { this.setLoading(false); }; + /** + * 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); @@ -163,12 +170,22 @@ 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 { let res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt); - GPTPopup.Instance.setText(res || 'Something went wrong.'); + console.log(res) + let 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); } @@ -195,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) => { @@ -362,6 +386,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} /> </> ) : ( |