import React from "react"; import { observable, reaction, makeObservable, runInAction, autorun, IReactionDisposer, toJS } from "mobx"; import { registerClass, makeDerivedClassInfo, PropertyType, RectObject, ProjectType, EezObject, ClassInfo, getParent, MessageType, IMessage } from "project-editor/core/object"; import { makeDataPropertyInfo, makeExpressionProperty, makeStylePropertyInfo, migrateStyleProperty, Widget } from "project-editor/flow/component"; import { IFlowContext } from "project-editor/flow/flow-interfaces"; import { observer } from "mobx-react"; import type * as PlotlyModule from "plotly.js-dist-min"; import classNames from "classnames"; import { specificGroup } from "project-editor/ui-components/PropertyGrid/groups"; import { evalProperty, getAnyValue, getNumberValue } from "project-editor/flow/helper"; import { getChildOfObject, Message, Section } from "project-editor/store"; import { buildExpression, checkExpression, evalConstantExpression } from "project-editor/flow/expression"; import { humanize } from "eez-studio-shared/string"; import { Assets, DataBuffer } from "project-editor/build/assets"; import { makeEndInstruction } from "project-editor/flow/expression/instructions"; import type { IDashboardComponentContext, ValueType } from "eez-studio-types"; import { GAUGE_ICON, LINE_CHART_ICON, PLOTLY_ICON } from "project-editor/ui-components/icons"; import type { Style } from "project-editor/features/style/style"; import { getThemedColor } from "project-editor/features/style/theme"; //////////////////////////////////////////////////////////////////////////////// let plotlyModule: typeof PlotlyModule; function Plotly() { if (!plotlyModule) { plotlyModule = require("plotly.js-dist-min/plotly.min.js") as typeof PlotlyModule; } return plotlyModule; } function openLink(url: string) { const { shell } = require("electron"); shell.openExternal(url); } //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // Plotly Widget export class PlotlyExecutionState { getInstrumentItemData?: () => { itemType: string; message: { data: any; layout: any; config: any; }; }; } const PlotlyElement = observer( class PlotlyeElement extends React.Component<{ widget: PlotlyWidget; flowContext: IFlowContext; width: number; height: number; }> { ref = React.createRef(); plotly: PlotlyModule.PlotlyHTMLElement | undefined; plotlyEl: HTMLDivElement | undefined; plotlyWidth: number; plotlyHeight: number; updateClientSizeTimeoutId: any; clientWidth = 0; clientHeight = 0; createChartState: "idle" | "create" | "cancel" | "stop" = "idle"; constructor(props: any) { super(props); makeObservable(this, { clientWidth: observable, clientHeight: observable }); } get data() { let data = evalProperty( this.props.flowContext, this.props.widget, "plotlyData" ); if (!data) { return undefined; } data = toJS(data); return data; } get layout() { let layout = evalProperty( this.props.flowContext, this.props.widget, "layout" ); if (!layout) { return undefined; } layout = toJS(layout); layout.width = this.clientWidth; layout.height = this.clientHeight; return layout; } get config() { let config = evalProperty( this.props.flowContext, this.props.widget, "config" ); if (!config) { return undefined; } config = toJS(config); return config; } async createChart(el: HTMLDivElement) { if (!this.data) { return; } if (this.createChartState != "idle") { if (this.createChartState == "create") { this.createChartState = "cancel"; } return; } this.createChartState = "create"; if (this.plotlyEl) { Plotly().purge(this.plotlyEl); } this.plotly = await Plotly().newPlot( el, this.data, this.layout, this.config ); if (this.createChartState != "create") { if (this.createChartState == "cancel") { this.createChartState = "idle"; this.createChart(el); } return; } this.createChartState = "idle"; this.plotlyEl = el; this.plotlyWidth = this.clientWidth; this.plotlyHeight = this.clientHeight; const executionState = this.props.flowContext.flowState?.getComponentExecutionState( this.props.widget ); if (executionState) { executionState.getInstrumentItemData = () => { return { itemType: "instrument/plotly", message: { data: this.data, layout: this.layout, config: this.config } }; }; } } updateClientSize = () => { if (this.ref.current) { const parentElement = this.ref.current.parentElement; if (parentElement) { const clientWidth = parentElement.clientWidth; const clientHeight = parentElement.clientHeight; if (clientWidth == 0 && clientHeight == 0) { this.updateClientSizeTimeoutId = setTimeout(() => { this.updateClientSizeTimeoutId = undefined; this.updateClientSize(); }, 16); } if ( clientWidth != this.clientWidth || clientHeight != this.clientHeight ) { runInAction(() => { this.clientWidth = clientWidth; this.clientHeight = clientHeight; }); } } } }; componentDidMount() { if (this.ref.current) { this.updateClientSize(); this.createChart(this.ref.current); } } async componentDidUpdate() { if (this.ref.current) { this.updateClientSize(); if ( !this.plotly || !this.props.flowContext.flowState || this.clientWidth != this.plotlyWidth || this.clientHeight != this.plotlyHeight ) { this.createChart(this.ref.current); } else { this.plotly = await Plotly().react( this.ref.current, this.data, this.layout, this.config ); } } } componentWillUnmount(): void { if (this.updateClientSizeTimeoutId) { clearTimeout(this.updateClientSizeTimeoutId); this.updateClientSizeTimeoutId = undefined; } this.createChartState = "stop"; if (this.plotlyEl) { Plotly().purge(this.plotlyEl); } } render() { const { flowContext } = this.props; this.data; this.layout; this.config; this.clientWidth; this.clientHeight; return (
); } } ); export class PlotlyWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Visualiser", properties: [ makeDataPropertyInfo("data", { hideInPropertyGrid: true, hideInDocumentation: "all" }), makeExpressionProperty( { name: "plotlyData", displayName: "Chart data", formText: () => ( Plotly chart data is set via JSON value, check{" "} { event.preventDefault(); event.stopPropagation(); openLink( "https://plotly.com/javascript/reference/index/" ); }} > Plotly documentation {" "} for available options. ), type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "json" ), makeExpressionProperty( { name: "layout", displayName: "Layout options", formText: () => ( Plotly layout options are set via JSON value, check{" "} { event.preventDefault(); event.stopPropagation(); openLink( "https://plotly.com/javascript/reference/layout/" ); }} > Plotly documentation {" "} for available options. ), type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "json" ), makeExpressionProperty( { name: "config", displayName: "Configuration options", formText: () => ( Plotly configuration options are set via JSON value, check{" "} { event.preventDefault(); event.stopPropagation(); openLink( "https://plotly.com/javascript/configuration-options/" ); }} > Plotly documentation {" "} for available options. ), type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "json" ) ], defaultValue: { left: 0, top: 0, width: 320, height: 160 }, icon: PLOTLY_ICON, showTreeCollapseIcon: "never", execute: (context: IDashboardComponentContext) => { Widget.classInfo.execute!(context); let executionState = context.getComponentExecutionState(); if (!executionState) { context.setComponentExecutionState(new PlotlyExecutionState()); } } }); plotlyData: string; layout: string; config: string; override makeEditable() { super.makeEditable(); makeObservable(this, { plotlyData: observable, layout: observable, config: observable }); } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { return ( <> {super.render(flowContext, width, height)} ); } } registerClass("PlotlyWidget", PlotlyWidget); //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // LineChart Widget interface InputData { xValue: number; lineValues: number[]; } export class PlotlyLineChartExecutionState { updated: number = 0; values: InputData[] = []; maxPoints: number = 0; labels: string[] = []; visible: boolean[] = []; debounceTimerId: any; getInstrumentItemData?: () => { itemType: string; message: { data: any; layout: any; config: any; }; }; constructor() { makeObservable(this, { updated: observable }); } } const LineChartElement = observer( class LineChartElement extends React.Component<{ widget: LineChartWidget; flowContext: IFlowContext; width: number; height: number; }> { ref = React.createRef(); plotly: PlotlyModule.PlotlyHTMLElement | undefined; plotlyEl: HTMLDivElement | undefined; plotlyWidth: number; plotlyHeight: number; dispose1: IReactionDisposer | undefined; dispose2: IReactionDisposer | undefined; updateClientSizeTimeoutId: any; clientWidth = 0; clientHeight = 0; createChartState: "idle" | "create" | "cancel" | "stop" = "idle"; constructor(props: any) { super(props); makeObservable(this, { clientWidth: observable, clientHeight: observable }); } get data(): PlotlyModule.Data[] { const { widget, flowContext } = this.props; const executionState = flowContext.flowState?.getComponentExecutionState( widget ); if (!executionState) { return widget.lines.map((line, i) => { let name: string; try { name = evalConstantExpression( flowContext.projectStore.project, line.label ).value; } catch (err) { name = ""; } return { x: flowContext.flowState ? [] : [1, 2, 3, 4], y: flowContext.flowState ? [] : [i + 1, (i + 1) * 2, (i + 1) * 3, (i + 1) * 4], type: "scatter", name, showlegend: widget.showLegend, line: { color: line.color } }; }); } const data: (PlotlyModule.Data | undefined)[] = widget.lines.map( (line, i) => { let name: string; if (!executionState.visible[i]) { return undefined; } name = executionState.labels[i]; return { x: executionState.values.map( inputValue => inputValue.xValue ), y: executionState.values.map( inputValue => inputValue.lineValues[i] ), type: "scatter", name, showlegend: widget.showLegend, line: { color: line.color } }; } ); return data.filter( line => line != undefined ) as PlotlyModule.Data[]; } get emptyData(): PlotlyModule.Data[] { const { widget, flowContext } = this.props; const executionState = flowContext.flowState?.getComponentExecutionState( widget ); const lines = widget.lines.filter((line, i) => { if (executionState) { return executionState.visible[i]; } else { return true; } }); return lines.map((line, i) => { let name; if (executionState) { name = executionState.labels[i]; } else { try { name = evalConstantExpression( flowContext.projectStore.project, line.label ).value; } catch (err) { name = undefined; } } return { x: [], y: [], type: "scatter", name, showlegend: widget.showLegend, line: { color: line.color } }; }); } get layout(): Partial { const { widget, flowContext } = this.props; let xRange; if (this.props.widget.xAxisRangeOption == "fixed") { // this is calculated from expression const xAxisRangeFrom = getNumberValue( flowContext, widget, "xAxisRangeFrom", 0 ); // this is calculated from expression const xAxisRangeTo = getNumberValue( flowContext, widget, "xAxisRangeTo", 4 ); xRange = [xAxisRangeFrom, xAxisRangeTo]; } let yRange; if (this.props.widget.yAxisRangeOption == "fixed") { // this is calculated from expression const yAxisRangeFrom = getNumberValue( flowContext, widget, "yAxisRangeFrom", 0 ); // this is calculated from expression const yAxisRangeTo = getNumberValue( flowContext, widget, "yAxisRangeTo", 10 ); yRange = [yAxisRangeFrom, yAxisRangeTo]; } let shapes: Array> | undefined; if (this.props.widget.marker) { // this is calculated from expression let marker = getAnyValue(flowContext, widget, "marker", null); if (marker != null) { const color = (widget.markerStyle && widget.markerStyle.borderColor && getThemedColor( flowContext.projectStore, widget.markerStyle.borderColor ).colorValue) || "black"; const width = ((widget.markerStyle && widget.markerStyle.borderSize) as any as number) || 1; const dash: PlotlyModule.Dash = ((widget.markerStyle && widget.markerStyle .borderStyle) as PlotlyModule.Dash) || "solid"; shapes = [ { type: "line", x0: marker, y0: 0, x1: marker, yref: "paper", y1: 100, line: { color, width, dash } } ]; } } const bgcolor = widget.style.backgroundColor ? getThemedColor( flowContext.projectStore, widget.style.backgroundColor ).colorValue : "white"; return { title: widget.title, xaxis: { visible: widget.showXAxis, showgrid: widget.showGrid, zeroline: widget.showZeroLines, range: xRange, ticksuffix: widget.xAxisTickSuffix }, yaxis: { visible: widget.showYAxis, showgrid: widget.showGrid, zeroline: widget.showZeroLines, range: yRange, ticksuffix: widget.yAxisTickSuffix }, shapes, margin: { t: widget.margin.top, r: widget.margin.right, b: widget.margin.bottom, l: widget.margin.left }, plot_bgcolor: bgcolor, paper_bgcolor: bgcolor, width: this.clientWidth, height: this.clientHeight }; } get config(): Partial { return { autosizable: false, displayModeBar: this.props.widget.displayModebar == "hover" ? "hover" : this.props.widget.displayModebar == "always" ? true : false }; } async createChart(el: HTMLDivElement) { if (this.createChartState != "idle") { if (this.createChartState == "create") { this.createChartState = "cancel"; } return; } this.createChartState = "create"; if (this.dispose1) { this.dispose1(); this.dispose1 = undefined; } if (this.dispose2) { this.dispose2(); this.dispose2 = undefined; } if (this.plotlyEl) { Plotly().purge(this.plotlyEl); } this.plotly = await Plotly().newPlot( el, this.data, this.layout, this.config ); if (this.createChartState != "create") { if (this.createChartState == "cancel") { this.createChartState = "idle"; this.createChart(el); } return; } this.createChartState = "idle"; this.plotlyEl = el; this.plotlyWidth = this.clientWidth; this.plotlyHeight = this.clientHeight; const executionState = this.props.flowContext.flowState?.getComponentExecutionState( this.props.widget ); if (executionState) { executionState.getInstrumentItemData = () => { return { itemType: "instrument/plotly", message: { data: this.data, layout: this.layout, config: this.config } }; }; } this.dispose1 = reaction( () => { return { layout: this.layout }; }, async params => { try { this.plotly = await Plotly().react( el, this.data, params.layout, this.config ); } catch (err) { console.error(err); } }, { delay: 16 } ); this.dispose2 = autorun( () => { const { widget, flowContext } = this.props; const executionState = flowContext.flowState?.getComponentExecutionState( widget ); if (!executionState) { return; } executionState.updated; (async () => { this.plotly = await Plotly().react( el, this.data, this.layout, this.config ); })(); }, { delay: 16 } ); } updateClientSize = () => { if (this.ref.current) { const parentElement = this.ref.current.parentElement; if (parentElement) { const clientWidth = parentElement.clientWidth; const clientHeight = parentElement.clientHeight; if (clientWidth == 0 && clientHeight == 0) { this.updateClientSizeTimeoutId = setTimeout(() => { this.updateClientSizeTimeoutId = undefined; this.updateClientSize(); }, 16); } if ( clientWidth != this.clientWidth || clientHeight != this.clientHeight ) { runInAction(() => { this.clientWidth = clientWidth; this.clientHeight = clientHeight; }); } } } }; componentDidMount() { if (this.ref.current) { this.updateClientSize(); this.createChart(this.ref.current); } } async componentDidUpdate() { if (this.ref.current) { this.updateClientSize(); if ( !this.plotly || !this.props.flowContext.flowState || this.clientWidth != this.plotlyWidth || this.clientHeight != this.plotlyHeight ) { this.createChart(this.ref.current); } else { this.plotly = await Plotly().react( this.ref.current, this.data, this.layout, this.config ); } } } componentWillUnmount(): void { if (this.updateClientSizeTimeoutId) { clearTimeout(this.updateClientSizeTimeoutId); this.updateClientSizeTimeoutId = undefined; } this.createChartState = "stop"; if (this.plotlyEl) { Plotly().purge(this.plotlyEl); } if (this.dispose1) { this.dispose1(); this.dispose1 = undefined; } if (this.dispose2) { this.dispose2(); this.dispose2 = undefined; } } render() { const { flowContext } = this.props; return (
); } } ); class LineChartLine extends EezObject { label: string; color: string; value: string; visible: string; static classInfo: ClassInfo = { listLabel: (object: LineChartLine) => "", properties: [ makeExpressionProperty( { name: "label", type: PropertyType.MultilineText }, "string" ), { name: "color", type: PropertyType.Color, propertyGridGroup: specificGroup }, makeExpressionProperty( { name: "value", type: PropertyType.MultilineText }, "double" ), makeExpressionProperty( { name: "visible", type: PropertyType.MultilineText }, "boolean" ) ], beforeLoadHook: (object: LineChartLine, jsObject: any) => { if (jsObject.visible == undefined) { jsObject.visible = "true"; } }, check: (lineChartTrace: LineChartLine, messages: IMessage[]) => { try { checkExpression( getParent(getParent(lineChartTrace)!)! as LineChartWidget, lineChartTrace.label ); } catch (err) { messages.push( new Message( MessageType.ERROR, `Invalid expression: ${err}`, getChildOfObject(lineChartTrace, "label") ) ); } try { checkExpression( getParent(getParent(lineChartTrace)!)! as LineChartWidget, lineChartTrace.value ); } catch (err) { messages.push( new Message( MessageType.ERROR, `Invalid expression: ${err}`, getChildOfObject(lineChartTrace, "value") ) ); } try { checkExpression( getParent(getParent(lineChartTrace)!)! as LineChartWidget, lineChartTrace.visible ); } catch (err) { messages.push( new Message( MessageType.ERROR, `Invalid expression: ${err}`, getChildOfObject(lineChartTrace, "visible") ) ); } }, defaultValue: { color: "#333333" } }; override makeEditable() { super.makeEditable(); makeObservable(this, { label: observable, color: observable, value: observable, visible: observable }); } } export class LineChartWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Visualiser", properties: [ Object.assign(makeDataPropertyInfo("data"), { hideInPropertyGrid: true }), makeExpressionProperty( { name: "xValue", displayName: "X value", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "any" ), { name: "lines", type: PropertyType.Array, typeClass: LineChartLine, propertyGridGroup: specificGroup, partOfNavigation: false, enumerable: false, defaultValue: [] }, { name: "title", type: PropertyType.String, propertyGridGroup: specificGroup }, { name: "displayModebar", displayName: "Display mode bar", type: PropertyType.Enum, enumItems: [ { id: "hover", label: "Hover" }, { id: "always", label: "Always" }, { id: "never", label: "Never" } ], propertyGridGroup: specificGroup }, { name: "showLegend", type: PropertyType.Boolean, propertyGridGroup: specificGroup, checkboxStyleSwitch: true }, { name: "showGrid", type: PropertyType.Boolean, propertyGridGroup: specificGroup, checkboxStyleSwitch: true }, { name: "showZeroLines", type: PropertyType.Boolean, propertyGridGroup: specificGroup, checkboxStyleSwitch: true }, { name: "showXAxis", displayName: "Show X axis", type: PropertyType.Boolean, propertyGridGroup: specificGroup, checkboxStyleSwitch: true }, { name: "xAxisTickSuffix", type: PropertyType.String, propertyGridGroup: specificGroup }, { name: "xAxisRangeOption", type: PropertyType.Enum, enumItems: [ { id: "floating", label: "Floating" }, { id: "fixed", label: "Fixed" } ], propertyGridGroup: specificGroup }, makeExpressionProperty( { name: "xAxisRangeFrom", type: PropertyType.MultilineText, propertyGridGroup: specificGroup, disabled: (widget: LineChartWidget) => widget.xAxisRangeOption != "fixed" }, "double" ), makeExpressionProperty( { name: "xAxisRangeTo", type: PropertyType.MultilineText, propertyGridGroup: specificGroup, disabled: (widget: LineChartWidget) => widget.xAxisRangeOption != "fixed" }, "double" ), { name: "showYAxis", displayName: "Show Y axis", type: PropertyType.Boolean, propertyGridGroup: specificGroup, checkboxStyleSwitch: true }, { name: "yAxisTickSuffix", type: PropertyType.String, propertyGridGroup: specificGroup }, { name: "yAxisRangeOption", type: PropertyType.Enum, enumItems: [ { id: "floating", label: "Floating" }, { id: "fixed", label: "Fixed" } ], propertyGridGroup: specificGroup }, makeExpressionProperty( { name: "yAxisRangeFrom", type: PropertyType.MultilineText, propertyGridGroup: specificGroup, disabled: (widget: LineChartWidget) => widget.yAxisRangeOption != "fixed" }, "double" ), makeExpressionProperty( { name: "yAxisRangeTo", type: PropertyType.MultilineText, propertyGridGroup: specificGroup, disabled: (widget: LineChartWidget) => widget.yAxisRangeOption != "fixed" }, "double" ), makeExpressionProperty( { name: "maxPoints", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "integer" ), { name: "margin", type: PropertyType.Object, typeClass: RectObject, propertyGridGroup: specificGroup, enumerable: false }, makeExpressionProperty( { name: "marker", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "float" ), makeStylePropertyInfo("style", "Default style"), makeStylePropertyInfo("markerStyle", "Marker style") ], beforeLoadHook: (object: LineChartWidget, jsObject: any) => { if (jsObject.xValue == undefined) { jsObject.xValue = "Date.now()"; } if (jsObject.lines == undefined) { jsObject.lines = [ { label: `"${humanize(jsObject.data)}"`, value: jsObject.data } ]; delete jsObject.data; } if (jsObject.xAxisRangeOption == undefined) { jsObject.xAxisRangeOption = "floating"; jsObject.xAxisRangeFrom = 0; jsObject.xAxisRangeTo = 10; } if (jsObject.yAxisRangeOption == undefined) { jsObject.yAxisRangeOption = "floating"; jsObject.yAxisRangeFrom = 0; jsObject.yAxisRangeTo = 10; } if (jsObject.showLegend == undefined) { jsObject.showLegend = true; } if (jsObject.displayModebar == undefined) { jsObject.displayModebar = "hover"; } if (typeof jsObject.maxPoints == "number") { jsObject.maxPoints = jsObject.maxPoints.toString(); } if (jsObject.showXAxis == undefined) { jsObject.showXAxis = true; } if (jsObject.showYAxis == undefined) { jsObject.showYAxis = true; } if (jsObject.showGrid == undefined) { jsObject.showGrid = true; } if (jsObject.showZeroLines == undefined) { jsObject.showZeroLines = true; } migrateStyleProperty(jsObject, "markerStyle"); }, defaultValue: { left: 0, top: 0, width: 320, height: 160, xValue: "Date.now()", lines: [], title: "", showLegend: true, displayModebar: "hover", showXAxis: true, showYAxis: true, showGrid: true, showZeroLines: true, xAxisRangeOption: "floating", xAxisRangeFrom: 0, xAxisRangeTo: 10, yAxisRangeOption: "floating", yAxisRangeFrom: 0, yAxisRangeTo: 10, maxPoints: "40", minRange: 0, maxRange: 1, margin: { top: 50, right: 0, bottom: 50, left: 50 }, marker: "", customInputs: [ { name: "value", type: "any" } ] }, icon: LINE_CHART_ICON, showTreeCollapseIcon: "never", execute: (context: IDashboardComponentContext) => { Widget.classInfo.execute!(context); let executionState = context.getComponentExecutionState(); if (!executionState) { executionState = new PlotlyLineChartExecutionState(); context.setComponentExecutionState(executionState); return; } const resetInputValue = context.getInputValue("reset"); const labels = context.getExpressionListParam(0); const visible = context.getExpressionListParam(16); executionState!.labels = labels; executionState!.visible = visible; if (resetInputValue !== undefined) { context.clearInputValue("reset"); executionState!.values = []; } else { let maxPoints = context.evalProperty("maxPoints"); const xValue = context.evalProperty("xValue"); if (xValue != undefined) { const lineValues = context.getExpressionListParam(8); if (!executionState.values) { executionState.values = []; } let inserted = false; for (let i = 0; i < executionState.values.length; i++) { if (xValue < executionState.values[i].xValue) { executionState.values.splice(i, 0, { xValue, lineValues }); inserted = true; break; } if (xValue == executionState.values[i].xValue) { if (i < executionState.values.length - 1) { executionState.values[i].lineValues = lineValues; inserted = true; } break; } } if (!inserted) { executionState.values.push({ xValue, lineValues }); } while (executionState.values.length > maxPoints) { executionState.values.splice(0, 1); } executionState!.maxPoints = maxPoints; } } if (executionState.debounceTimerId) { clearTimeout(executionState.debounceTimerId); } executionState.debounceTimerId = setTimeout(() => { runInAction(() => { executionState!.updated++; }); }, 16); } }); xValue: string; lines: LineChartLine[]; title: string; displayModebar: "hover" | "always" | "never"; showLegend: boolean; showGrid: boolean; showZeroLines: boolean; showXAxis: boolean; xAxisTickSuffix: string; xAxisRangeOption: "floating" | "fixed"; xAxisRangeFrom: number; xAxisRangeTo: number; showYAxis: boolean; yAxisTickSuffix: string; yAxisRangeOption: "floating" | "fixed"; yAxisRangeFrom: number; yAxisRangeTo: number; maxPoints: string; margin: RectObject; marker: string; markerStyle: Style; override makeEditable() { super.makeEditable(); makeObservable(this, { xValue: observable, lines: observable, title: observable, displayModebar: observable, showLegend: observable, showXAxis: observable, showYAxis: observable, showGrid: observable, showZeroLines: observable, xAxisTickSuffix: observable, xAxisRangeOption: observable, xAxisRangeFrom: observable, xAxisRangeTo: observable, yAxisTickSuffix: observable, yAxisRangeOption: observable, yAxisRangeFrom: observable, yAxisRangeTo: observable, maxPoints: observable, margin: observable, marker: observable, markerStyle: observable }); } getInputs() { return [ { name: "reset", type: "any" as ValueType, isSequenceInput: false, isOptionalInput: true }, ...super.getInputs() ]; } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { return ( <> {super.render(flowContext, width, height)} ); } buildFlowComponentSpecific(assets: Assets, dataBuffer: DataBuffer) { dataBuffer.writeArray(this.lines, line => { try { // as property buildExpression(assets, dataBuffer, this, line.label); } catch (err) { assets.projectStore.outputSectionsStore.write( Section.OUTPUT, MessageType.ERROR, err, getChildOfObject(this, "label") ); dataBuffer.writeUint16NonAligned(makeEndInstruction()); } }); dataBuffer.writeArray(this.lines, line => { try { // as property buildExpression(assets, dataBuffer, this, line.value); } catch (err) { assets.projectStore.outputSectionsStore.write( Section.OUTPUT, MessageType.ERROR, err, getChildOfObject(this, "value") ); dataBuffer.writeUint16NonAligned(makeEndInstruction()); } }); dataBuffer.writeArray(this.lines, line => { try { // as property buildExpression(assets, dataBuffer, this, line.visible); } catch (err) { assets.projectStore.outputSectionsStore.write( Section.OUTPUT, MessageType.ERROR, err, getChildOfObject(this, "visible") ); dataBuffer.writeUint16NonAligned(makeEndInstruction()); } }); } } registerClass("LineChartWidget", LineChartWidget); //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // Gauge Widget const GaugeElement = observer( class GaugeElement extends React.Component<{ widget: GaugeWidget; flowContext: IFlowContext; width: number; height: number; }> { ref = React.createRef(); plotly: PlotlyModule.PlotlyHTMLElement | undefined; plotlyEl: HTMLDivElement | undefined; plotlyWidth: number; plotlyHeight: number; dispose1: IReactionDisposer | undefined; dispose2: IReactionDisposer | undefined; updateClientSizeTimeoutId: any; clientWidth = 0; clientHeight = 0; createChartState: "idle" | "create" | "cancel" | "stop" = "idle"; constructor(props: any) { super(props); makeObservable(this, { clientWidth: observable, clientHeight: observable }); } get data(): PlotlyModule.Data[] { const minRange = 0; const maxRange = 1; return [ { domain: { x: [0, 1], y: [0, 1] }, value: this.props.flowContext.flowState ? 0 : (minRange + maxRange) / 2, title: { text: this.props.widget.title }, type: "indicator", mode: "gauge+number", gauge: { bar: { color: this.props.widget.color }, axis: { range: [minRange, maxRange], color: this.props.widget.color } }, number: { valueformat: "f" } } ]; } get layout(): Partial { return { margin: { t: this.props.widget.margin.top, r: this.props.widget.margin.right, b: this.props.widget.margin.bottom, l: this.props.widget.margin.left } }; } get config(): Partial { return { displayModeBar: false, autosizable: false }; } async createChart(el: HTMLDivElement) { if (this.createChartState != "idle") { if (this.createChartState == "create") { this.createChartState = "cancel"; } return; } this.createChartState = "create"; if (this.dispose1) { this.dispose1(); this.dispose1 = undefined; } if (this.dispose2) { this.dispose2(); this.dispose2 = undefined; } if (this.plotlyEl) { Plotly().purge(this.plotlyEl); } this.plotly = await Plotly().newPlot( el, this.data, this.layout, this.config ); if (this.createChartState != "create") { if (this.createChartState == "cancel") { this.createChartState = "idle"; this.createChart(el); } return; } this.createChartState = "idle"; this.plotlyEl = el; this.plotlyWidth = this.clientWidth; this.plotlyHeight = this.clientHeight; this.dispose1 = reaction( () => { return { layout: this.layout }; }, async params => { try { this.plotly = await Plotly().react( el, this.data, params.layout, this.config ); } catch (err) { console.error(err); } }, { delay: 16 } ); this.dispose2 = autorun( () => { const inputData = this.props.flowContext.flowState ? { value: evalProperty( this.props.flowContext, this.props.widget, "data" ), minRange: evalProperty( this.props.flowContext, this.props.widget, "minRange" ), maxRange: evalProperty( this.props.flowContext, this.props.widget, "maxRange" ) } : undefined; if (inputData != undefined) { Plotly().update( el, { value: inputData.value, gauge: { bar: { color: this.props.widget.color }, axis: { range: [ inputData.minRange, inputData.maxRange ], color: this.props.widget.color } }, number: { valueformat: "f" } }, {} ); } }, { delay: 16 } ); } updateClientSize = () => { if (this.ref.current) { const parentElement = this.ref.current.parentElement; if (parentElement) { const clientWidth = parentElement.clientWidth; const clientHeight = parentElement.clientHeight; if (clientWidth == 0 && clientHeight == 0) { this.updateClientSizeTimeoutId = setTimeout(() => { this.updateClientSizeTimeoutId = undefined; this.updateClientSize(); }, 16); } if ( clientWidth != this.clientWidth || clientHeight != this.clientHeight ) { runInAction(() => { this.clientWidth = clientWidth; this.clientHeight = clientHeight; }); } } } }; componentDidMount() { if (this.ref.current) { this.updateClientSize(); this.createChart(this.ref.current); } } componentDidUpdate() { if (this.ref.current) { this.updateClientSize(); this.createChart(this.ref.current); } } componentWillUnmount(): void { if (this.updateClientSizeTimeoutId) { clearTimeout(this.updateClientSizeTimeoutId); this.updateClientSizeTimeoutId = undefined; } this.createChartState = "stop"; if (this.plotlyEl) { Plotly().purge(this.plotlyEl); } if (this.dispose1) { this.dispose1(); this.dispose1 = undefined; } if (this.dispose2) { this.dispose2(); this.dispose2 = undefined; } } render() { const { flowContext } = this.props; return (
); } } ); export class GaugeWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Visualiser", properties: [ makeDataPropertyInfo("data"), { name: "title", type: PropertyType.String, propertyGridGroup: specificGroup }, makeExpressionProperty( { name: "minRange", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "float" ), makeExpressionProperty( { name: "maxRange", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "float" ), { name: "color", type: PropertyType.Color, propertyGridGroup: specificGroup }, { name: "margin", type: PropertyType.Object, typeClass: RectObject, propertyGridGroup: specificGroup }, makeStylePropertyInfo("style", "Default style") ], defaultValue: { left: 0, top: 0, width: 160, height: 160, title: "", minRange: "0", maxRange: "1", color: "#008000", margin: { top: 50, right: 0, bottom: 0, left: 0 } }, icon: GAUGE_ICON, showTreeCollapseIcon: "never", beforeLoadHook: (object: GaugeWidget, jsObject: any) => { if (typeof jsObject.minRange == "number") { jsObject.minRange = jsObject.minRange.toString(); } if (typeof jsObject.maxRange == "number") { jsObject.maxRange = jsObject.maxRange.toString(); } } }); title: string; minRange: string; maxRange: string; color: string; margin: RectObject; override makeEditable() { super.makeEditable(); makeObservable(this, { title: observable, minRange: observable, maxRange: observable, color: observable, margin: observable }); } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { return ( <> {super.render(flowContext, width, height)} ); } } registerClass("GaugeWidget", GaugeWidget);