import functionPlot, { Chart } from 'function-plot'; import { action, computed, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { Cast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DocUtils } from '../../documents/DocUtils'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { FieldView, FieldViewProps } from './FieldView'; import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; @observer export class FunctionPlotBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FunctionPlotBox, fieldKey); } public static GraphCount = 0; _plot: Chart | undefined; _plotId = ''; _plotEle: HTMLDivElement | null = null; constructor(props: FieldViewProps) { super(props); makeObservable(this); this._plotId = 'graph' + FunctionPlotBox.GraphCount++; } componentDidMount() { this._props.setContentViewBox?.(this); reaction( () => [this.graphFuncs, this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange], () => this.createGraph() ); } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document }); PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.Document); if (this._plot) { anchor.config_xRange = new List(Array.from(this._plot.options.xAxis?.domain ?? [])); anchor.config_yRange = new List(Array.from(this._plot.options.yAxis?.domain ?? [])); } if (addAsAnnotation) this.addDocument(anchor); return anchor; }; @computed get graphFuncs() { const links = Doc.Links(this.Document) .map(d => Doc.getOppositeAnchor(d, this.Document)) .filter(d => d) .map(d => d!); const funcs = links.concat(DocListCast(this.dataDoc[this.fieldKey])).map(doc => StrCast(doc.text, 'x^2') .replace(/\\sqrt/g, 'sqrt') .replace(/\\frac\{(.*)\}\{(.*)\}/g, '($1/$2)') .replace(/\\left/g, '') .replace(/\\right/g, '') .replace(/\{/g, '') .replace(/\}/g, '') ); return funcs; } computeYScale = (width: number, height: number, xScale: number[]) => { const xDiff = xScale[1] - xScale[0]; const yDiff = (height * xDiff) / width; return [-yDiff / 2, yDiff / 2]; }; createGraph = (ele?: HTMLDivElement) => { this._plotEle = ele || this._plotEle; const width = this._props.PanelWidth(); const height = this._props.PanelHeight(); const xrange = Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]); try { this._plotEle?.children.length && this._plotEle.removeChild(this._plotEle.children[0]); this._plot = functionPlot({ target: '#' + this._plotEle?.id, width, height, xAxis: { domain: xrange }, yAxis: { domain: this.computeYScale(width, height, xrange) }, // Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) }, grid: true, data: this.graphFuncs.map(fn => ({ fn, // derivative: { fn: "2 * x", updateOnMouseMove: true } })), }); } catch (e) { console.log(e); } }; @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData?.droppedDocuments.length) { const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => { // const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc); if (res) { const link = DocUtils.MakeLink(doc, this.Document, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' }); link && this._props.addDocument?.(link); } return res; }, true); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place return added; } return false; }; _dropDisposer: DragManager.DragDropDisposer | undefined; protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc); } // if (this.layout_autoHeight) this.tryUpdateScrollHeight(); }; @computed get theGraph() { return (
r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => { e.stopPropagation(); setupMoveUpEvents( this, e, returnFalse, action(() => { if (this._plot?.options.xAxis?.domain) { this.Document.xRange = new List(this._plot.options.xAxis.domain); } if (this._plot?.options.yAxis?.domain) { this.Document.yRange = new List(this._plot.options.yAxis.domain); } }), emptyFunction, false, false ); }} /> ); } render() { TraceMobx(); return (
{this.theGraph}
); } } Docs.Prototypes.TemplateMap.set(DocumentType.FUNCPLOT, { layout: { view: FunctionPlotBox, dataField: 'data' }, options: { acl: '', _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true }, });