aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/DataVizBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DataVizBox.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx313
1 files changed, 296 insertions, 17 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 3dd568fda..896048ab3 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -4,29 +4,35 @@ import { Colors, Toggle, ToggleType, Type } from 'browndash-components';
import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils';
+import { ClientUtils, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
-import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols';
import { InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
+import { PrefetchProxy } from '../../../../fields/Proxy';
import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { CsvField } from '../../../../fields/URLField';
-import { TraceMobx } from '../../../../fields/util';
+import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
+import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT';
import { DocUtils } from '../../../documents/DocUtils';
import { DocumentType } from '../../../documents/DocumentTypes';
import { Docs } from '../../../documents/Documents';
+import { LinkManager } from '../../../util/LinkManager';
import { UndoManager, undoable } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
import { ViewBoxAnnotatableComponent } from '../../DocComponent';
import { MarqueeAnnotator } from '../../MarqueeAnnotator';
import { PinProps } from '../../PinFuncs';
import { SidebarAnnos } from '../../SidebarAnnos';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { AnchorMenu } from '../../pdf/AnchorMenu';
import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup';
import { DocumentView } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { FocusViewOptions } from '../FocusViewOptions';
import './DataVizBox.scss';
+import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType, TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu';
import { Histogram } from './components/Histogram';
import { LineChart } from './components/LineChart';
import { PieChart } from './components/PieChart';
@@ -41,6 +47,7 @@ export enum DataVizView {
@observer
export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ private _urlError: boolean = false;
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _marqueeref = React.createRef<MarqueeAnnotator>();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -49,7 +56,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
sidebarAddDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined;
crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined;
@observable _marqueeing: number[] | undefined = undefined;
- @observable _savedAnnotations = new ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>();
+ @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @observable _specialHighlightedRow: number | undefined = undefined;
+ @observable GPTSummary: ObservableMap<string, { desc?: string; type?: TemplateFieldType; size?: TemplateFieldSize }> | undefined = undefined;
+ @observable colsInfo: ObservableMap<string, Col> = new ObservableMap();
+ @observable _GPTLoading: boolean = false;
constructor(props: FieldViewProps) {
super(props);
@@ -99,8 +110,14 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// all CSV records in the dataset (that aren't an empty row)
@computed.struct get records() {
- const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
- return records?.filter(record => Object.keys(record).some(key => record[key])) ?? [];
+ try {
+ const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ this._urlError = false;
+ return records?.filter(record => Object.keys(record).some(key => record[key])) ?? [];
+ } catch {
+ this._urlError = true;
+ return [{ error: 'Data not found' }] as { [key: string]: string }[];
+ }
}
// currently chosen visualization type: line, pie, histogram, table
@@ -124,17 +141,75 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.layoutDoc._dataViz_titleCol = titleCol;
};
+ @action setSpecialHighlightedRow = (row: number | undefined) => {
+ this._specialHighlightedRow = row;
+ };
+
+ @action setColumnType = (colTitle: string, type: TemplateFieldType) => {
+ const colInfo = this.colsInfo.get(colTitle);
+ if (colInfo) {
+ colInfo.type = type;
+ } else {
+ this.colsInfo.set(colTitle, { title: colTitle, desc: '', type: type, sizes: [TemplateFieldSize.MEDIUM] });
+ }
+ };
+
+ @action modifyColumnSizes = (colTitle: string, size: TemplateFieldSize, valid: boolean) => {
+ const column = this.colsInfo.get(colTitle);
+ if (column) {
+ if (!valid && column.sizes.includes(size)) {
+ column.sizes.splice(column.sizes.indexOf(size), 1);
+ } else if (valid && !column.sizes.includes(size)) {
+ column.sizes.push(size);
+ }
+ } else {
+ this.colsInfo.set(colTitle, { title: colTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [size] });
+ }
+ };
+
+ @action setColumnTitle = (colTitle: string, newTitle: string) => {
+ const colInfo = this.colsInfo.get(colTitle);
+ if (colInfo) {
+ colInfo.title = newTitle;
+ console.log(colInfo.title);
+ } else {
+ this.colsInfo.set(colTitle, { title: newTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [] });
+ }
+ };
+
+ @action setColumnDesc = (colTitle: string, desc: string) => {
+ const colInfo = this.colsInfo.get(colTitle);
+ if (colInfo) {
+ if (!desc) {
+ colInfo.desc = this.GPTSummary?.get(colTitle)?.desc ?? '';
+ } else {
+ colInfo.desc = desc;
+ }
+ } else {
+ this.colsInfo.set(colTitle, { title: colTitle, desc: desc, type: TemplateFieldType.UNSET, sizes: [] });
+ }
+ };
+
+ @action setColumnDefault = (colTitle: string, cont: string) => {
+ const colInfo = this.colsInfo.get(colTitle);
+ if (colInfo) {
+ colInfo.defaultContent = cont;
+ } else {
+ this.colsInfo.set(colTitle, { title: colTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [], defaultContent: cont });
+ }
+ };
+
@action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
- restoreView = (data: Doc) => {
+ restoreView = (viewData: Doc) => {
// const changedView = data.config_dataViz && this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz);
// const changedAxes = data.config_dataVizAxes && this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes)));
- this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows);
- this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors);
- this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor;
- this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors);
+ this.layoutDoc.dataViz_selectedRows = Field.Copy(viewData.dataViz_selectedRows);
+ this.layoutDoc.dataViz_histogram_barColors = Field.Copy(viewData.dataViz_histogram_barColors);
+ this.layoutDoc.dataViz_histogram_defaultColor = viewData.dataViz_histogram_defaultColor;
+ this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(viewData.dataViz_pie_sliceColors);
Object.keys(this.layoutDoc).forEach(key => {
if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) {
- this.layoutDoc['_' + key] = data[key];
+ this.layoutDoc['_' + key] = viewData[key];
}
});
return true;
@@ -145,6 +220,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// }
// return func() ?? false;
};
+
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const visibleAnchor = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
const anchor = !pinProps
@@ -271,7 +347,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
componentDidMount() {
this._props.setContentViewBox?.(this);
- if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
+ if (!this._urlError) {
+ if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
+ }
this._disposers.datavis = reaction(
() => {
if (this.layoutDoc.dataViz_schemaLive === undefined) this.layoutDoc.dataViz_schemaLive = true;
@@ -332,6 +410,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
{ fireImmediately: true }
);
+ this._disposers.contentSummary = reaction(
+ () => this.records,
+ () => this.updateGPTSummary()
+ );
}
fetchData = () => {
@@ -358,7 +440,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
if (!this.records.length) return 'no data/visualization';
switch (this.dataVizView) {
- case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>;
+ case DataVizView.TABLE: return <TableBox {...sharedProps} specHighlightedRow={this._specialHighlightedRow} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>;
case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />;
case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />;
case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}}
@@ -425,11 +507,18 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.layoutDoc.dataViz_filterSelection = !this.layoutDoc.dataViz_filterSelection;
};
- specificContextMenu = (): void => {
+ openDocCreatorMenu = (x: number, y: number) => {
+ DocCreatorMenu.Instance.toggleDisplay(x, y);
+ DocCreatorMenu.Instance.setDataViz(this);
+ DocCreatorMenu.Instance.setTemplateDocs(this.getPossibleTemplates());
+ };
+
+ specificContextMenu = (e: React.MouseEvent) => {
const cm = ContextMenu.Instance;
const options = cm.findByDescription('Options...');
const optionItems = options?.subitems ?? [];
optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' });
+ optionItems.push({ description: `Create documents`, event: () => this.openDocCreatorMenu(e.pageX, e.pageY), icon: 'table-cells' });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
};
@@ -439,11 +528,201 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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);
- GPTPopup.Instance.setDataJson(JSON.stringify(data));
+ const csvdata = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ GPTPopup.Instance.setDataJson(JSON.stringify(csvdata));
GPTPopup.Instance.generateDataAnalysis();
});
+ getColSummary = (): string => {
+ const possibleIds: number[] = this.records.map((_, index) => index);
+
+ for (let i = possibleIds.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [possibleIds[i], possibleIds[j]] = [possibleIds[j], possibleIds[i]];
+ }
+
+ const rowsToCheck = possibleIds.slice(0, Math.min(10, this.records.length));
+
+ let prompt: string = 'Col titles: ';
+
+ const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined);
+
+ cols.forEach((col, i) => {
+ prompt += `Col #${i}: ${col} ------`;
+ });
+
+ prompt += '----------- Rows: ';
+
+ rowsToCheck.forEach(row => {
+ prompt += `Row #${row}: `;
+ cols.forEach(col => {
+ prompt += `${col}: ${this.records[row][col]} -----`;
+ });
+ });
+
+ return prompt;
+ };
+
+ updateColDefaults = () => {
+ const possibleIds: number[] = this.records.map((_, index) => index);
+
+ for (let i = possibleIds.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [possibleIds[i], possibleIds[j]] = [possibleIds[j], possibleIds[i]];
+ }
+
+ const rowToCheck = possibleIds[0];
+
+ const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined);
+
+ cols.forEach(col => {
+ this.setColumnDefault(col, `${this.records[rowToCheck][col]}`);
+ });
+ };
+
+ updateGPTSummary = async () => {
+ this._GPTLoading = true;
+
+ this.updateColDefaults();
+
+ const prompt = this.getColSummary();
+
+ const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined);
+ cols.forEach(col => {
+ if (!this.colsInfo.get(col)) this.colsInfo.set(col, { title: col, desc: '', sizes: [], type: TemplateFieldType.UNSET });
+ });
+
+ try {
+ const [res1, res2] = await Promise.all([gptAPICall(prompt, GPTCallType.VIZSUM), gptAPICall('Info:' + prompt, GPTCallType.VIZSUM2)]);
+
+ if (res1) {
+ this.GPTSummary = new ObservableMap();
+ const descs: { [col: string]: string } = JSON.parse(res1);
+ for (const [key, val] of Object.entries(descs)) {
+ this.GPTSummary.set(key, { desc: val });
+ if (!this.colsInfo.get(key)?.desc) this.setColumnDesc(key, val);
+ }
+ }
+
+ if (res2) {
+ !this.GPTSummary && (this.GPTSummary = new ObservableMap());
+ const info: { [col: string]: { type: TemplateFieldType; size: TemplateFieldSize } } = JSON.parse(res2);
+ for (const [key, val] of Object.entries(info)) {
+ const colSummary = this.GPTSummary.get(key);
+ if (colSummary) {
+ colSummary.size = val.size;
+ colSummary.type = val.type;
+ this.setColumnType(key, val.type);
+ this.modifyColumnSizes(key, val.size, true);
+ }
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ getPossibleTemplates = (): Doc[] => {
+ const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(this.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)));
+ const linkedCollections: Doc[] = linkedDocs.filter(doc => doc.type === 'config').map(doc => DocCast(doc.annotationOn));
+ const isColumnTitle = (title: string): boolean => {
+ const colTitles: string[] = Object.keys(this.records[0]);
+ for (let i = 0; i < colTitles.length; ++i) {
+ if (colTitles[i] === title) {
+ return true;
+ }
+ }
+ return false;
+ };
+ const isValidTemplate = (collection: Doc) => {
+ const childDocs = DocListCast(collection[Doc.LayoutFieldKey(collection)]);
+ for (let i = 0; i < childDocs.length; ++i) {
+ if (isColumnTitle(String(childDocs[i].title))) return true;
+ }
+ return false;
+ };
+ return linkedCollections.filter(col => isValidTemplate(col));
+ };
+
+ ApplyTemplateTo = (templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) => {
+ if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) {
+ if (target.resolvedDataDoc) {
+ target[targetKey] = new PrefetchProxy(templateDoc);
+ } else {
+ titleTarget && (Doc.GetProto(target).title = titleTarget);
+ const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
+ setDoc[targetKey] = new PrefetchProxy(templateDoc);
+ }
+ }
+ return target;
+ };
+
+ applyLayout = (templateInfo: DataVizTemplateInfo, docs: Doc[]) => {
+ if (templateInfo.layout.type === LayoutType.Stacked) return;
+ const columns: number = templateInfo.columns;
+ const xGap: number = templateInfo.layout.xMargin;
+ const yGap: number = templateInfo.layout.yMargin;
+ // const repeat: number = templateInfo.layout.repeat;
+ const startX: number = templateInfo.referencePos.x;
+ const startY: number = templateInfo.referencePos.y;
+ const templWidth = Number(templateInfo.doc._width);
+ const templHeight = Number(templateInfo.doc._height);
+
+ let i: number = 0;
+ let docsChanged: number = 0;
+ let curX: number = startX;
+ let curY: number = startY;
+
+ while (docsChanged < docs.length) {
+ while (i < columns && docsChanged < docs.length) {
+ docs[docsChanged].x = curX;
+ docs[docsChanged].y = curY;
+ curX += templWidth + xGap;
+ ++docsChanged;
+ ++i;
+ }
+
+ i = 0;
+ curX = startX;
+ curY += templHeight + yGap;
+ }
+ };
+
+ // @action addSavedLayout = (layout: DataVizTemplateLayout) => {
+ // const saved = Cast(this.layoutDoc.dataViz_savedTemplates, listSpec('RefField'));
+
+ // }
+
+ @action
+ createDocsFromTemplate = (templateInfo: DataVizTemplateInfo) => {
+ if (!templateInfo.doc) return;
+ const mainCollection = this.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
+ const fields: string[] = Array.from(Object.keys(this.records[0]));
+ const selectedRows = NumListCast(this.layoutDoc.dataViz_selectedRows);
+ const docs: Doc[] = selectedRows.map(row => {
+ const values: string[] = [];
+ fields.forEach(col => values.push(this.records[row][col]));
+
+ const proto = new Doc();
+ proto.author = ClientUtils.CurrentUserEmail();
+ values.forEach((val, i) => {
+ proto[fields[i]] = val as FieldType;
+ });
+
+ const target = Doc.MakeDelegate(proto);
+ const targetKey = StrCast(templateInfo.doc!.layout_fieldKey, 'layout');
+ const applied = this.ApplyTemplateTo(templateInfo.doc!, target, targetKey, templateInfo.doc!.title + `${row}`);
+ target.layout_fieldKey = targetKey;
+
+ //this.applyImagesTo(target, fields);
+ return applied;
+ });
+
+ docs.forEach(doc => mainCollection.addDocument(doc));
+
+ this.applyLayout(templateInfo, docs);
+ };
+
/**
* creates a new dataviz document filter from this one
* it appears to the right of this document, with the