aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/gpt/GPT.ts7
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss5
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx67
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss14
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx151
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx211
-rw-r--r--src/client/views/nodes/DataVizBox/components/PieChart.tsx136
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx10
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx25
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} />
</>
)