diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DataVizBox.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/DataVizBox.tsx | 305 |
1 files changed, 295 insertions, 10 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index df6e74d85..12196f290 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -5,14 +5,14 @@ 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 { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; -import { TraceMobx } from '../../../../fields/util'; +import { GetEffectiveAcl, TraceMobx, inheritParentAcls } from '../../../../fields/util'; import { DocUtils } from '../../../documents/DocUtils'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; @@ -32,6 +32,18 @@ import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; +import { LinkManager } from '../../../util/LinkManager'; +import { Col, DataVizTemplateInfo, DataVizTemplateLayout, DocCreatorMenu, TemplateFieldSize, LayoutType, TemplateFieldType } from './DocCreatorMenu'; +import { CollectionFreeFormView, MarqueeView } from '../../collections/collectionFreeForm'; +import { PrefetchProxy } from '../../../../fields/Proxy'; +import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols'; +import { template } from 'lodash'; +import { data } from 'jquery'; +import { listSpec } from '../../../../fields/Schema'; +import { ObjectField } from '../../../../fields/ObjectField'; +import { Id } from '../../../../fields/FieldSymbols'; +import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT'; +import { TbSortDescendingShapes } from 'react-icons/tb'; export enum DataVizView { TABLE = 'table', @@ -42,6 +54,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(); @@ -50,7 +63,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); @@ -100,8 +117,17 @@ 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 (e){ + this._urlError = true; + const data: { [key: string]: string; }[] = [ + { error: "Data not found"}, + ]; + return data; + } } // currently chosen visualization type: line, pie, histogram, table @@ -125,6 +151,61 @@ 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) => { // const changedView = data.config_dataViz && this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz); @@ -146,6 +227,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 @@ -272,7 +354,7 @@ 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; @@ -333,6 +415,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, { fireImmediately: true } ); + this._disposers.contentSummary = reaction( + () => this.records, + () => this.updateGPTSummary() + ); } fetchData = () => { @@ -359,7 +445,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;}} @@ -426,11 +512,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 = (x: number, y: number): void => { 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(x, y), icon: 'table-cells' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); }; @@ -445,6 +538,198 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { GPTPopup.Instance.generateDataAnalysis(); }); + getColSummary = (): string => { + let 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, i) => { + prompt += `Row #${row}: ` + cols.forEach(col => { + prompt += `${col}: ${this.records[row][col]} -----` + }) + }) + + return prompt; + } + + updateColDefaults = () => { + let 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 @@ -498,7 +783,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { transform: `scale(${scale})`, position: 'absolute', }} - onContextMenu={this.specificContextMenu} + onContextMenu={(e) => this.specificContextMenu(e.pageX, e.pageY)} onWheel={e => e.stopPropagation()} ref={this._mainCont}> <div className="datatype-button"> |