diff --git a/src/components/editors/extraCalcEditor.tsx b/src/components/editors/extraCalcEditor.tsx new file mode 100644 index 0000000..89c89b7 --- /dev/null +++ b/src/components/editors/extraCalcEditor.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { StandardEditorProps } from "@grafana/data"; +import { IExtraCalc } from 'utils/types'; +import { ControlledCollapse, useTheme2 } from '@grafana/ui'; +import { Mode } from 'utils/constants'; +import { ExtraCalcDefault } from 'utils/default'; +import { css } from '@emotion/css'; +import { ExtraCalcForm } from './extraCalcForm'; + + +interface Props extends StandardEditorProps { } + +export const ExtraCalcEditor: React.FC = ({ value: elements, onChange, context }) => { + + if (!elements || !elements.length) { + elements = []; + } + + const addElement = (newExtraCalc: IExtraCalc) => { + const updated = [...elements, newExtraCalc] + onChange(updated) + } + + const updateElement = (idx: number, extraCalcUpdated: IExtraCalc) => { + const updated = [...elements] + updated[idx] = extraCalcUpdated + onChange(updated) + } + + const deleteElement = (idx: number) => { + const updated = [...elements] + updated.splice(idx, 1) + onChange(updated) + } + + const listExtraCalcs = elements.map((element: IExtraCalc, idx: number) => { + return
+ + updateElement(idx, m)} + deleteFunction={() => deleteElement(idx)} + context={context} /> + +
+ }) + + return (
+

This is an optional feature. For now only recursive calculations are allowed.

+ {listExtraCalcs} + + + +
) +} diff --git a/src/components/editors/extraCalcForm.tsx b/src/components/editors/extraCalcForm.tsx new file mode 100644 index 0000000..6b53c13 --- /dev/null +++ b/src/components/editors/extraCalcForm.tsx @@ -0,0 +1,181 @@ +import React, { ChangeEvent, useEffect, useState } from 'react' +import { SelectableValue, StandardEditorContext } from "@grafana/data"; +import { Calc, ExtraCalcFormat, IExtraCalc, ISelect } from 'utils/types'; +import { Alert, Button, Collapse, ConfirmButton, Form, FormAPI, HorizontalGroup, InlineField, Input, InputControl, Select, useTheme2 } from '@grafana/ui'; +import { ExtraCalcDefault } from 'utils/default'; +import { Mode } from 'utils/constants'; +import { enumToSelect } from 'utils/utils'; +import { css } from '@emotion/css'; + +interface Props { + extraCalc: IExtraCalc, + updateFunction: any, + deleteFunction?: any, + mode: Mode, + context: StandardEditorContext +} + +export const ExtraCalcForm: React.FC = ({ extraCalc, updateFunction, deleteFunction, mode, context }) => { + + const calcOptions: ISelect[] = enumToSelect(Calc) + const formatOptions: ISelect[] = enumToSelect(ExtraCalcFormat) + + const [currentCalc, setCurrentCalc] = useState(ExtraCalcDefault) + const [selectedCalc, setSelectedCalc] = useState>(calcOptions[0]) + const [selectedFormat, setSelectedFormat] = useState>(formatOptions[0]) + const [disabled, setDisabled] = useState(false) + + const updateCurrentState = () => { + setCurrentCalc(extraCalc) + setSelectedCalc({value: extraCalc.calc, label: extraCalc.calc as string}) + setSelectedFormat({value: extraCalc.resFormat, label: extraCalc.resFormat as string}) + } + + const handleOnChangeCalc = (event: ChangeEvent) => { + setCurrentCalc({ + ...currentCalc, + [event.currentTarget.name]: event.target.value + }) + } + + const handleOnSubmitAddFormat = () => { + updateFunction({ + ...currentCalc, + calc: selectedCalc.value, + resFormat: selectedFormat.value + }) + if (mode === Mode.EDIT) { + setDisabled(true) + } else { + setCurrentCalc(ExtraCalcDefault) + } + //console.log("SUBMIT ADD") + } + + const handleOnClickEdit = () => { + setDisabled(!disabled) + } + + const handleOnClickCancel = () => { + updateCurrentState() + setDisabled(true) + //console.log("cancel") + } + + const handleOnConfirmDeleteFormat = () => { + if (deleteFunction) deleteFunction() + } + + useEffect(() => { + if (mode === Mode.EDIT) setDisabled(true) + }, [mode]) + + useEffect(() => { + updateCurrentState() + }, [extraCalc]) + + useEffect(() => { + }, [currentCalc]) + + const buttonEdit = () => { + if (mode === Mode.CREATE) { + return
+ } else { + return ( +
+ + + + +
) + } + } + + return
+ {buttonEdit()} + +
{({ register, errors, control }: FormAPI) => { + return ( +
+ + + + + + + + + + + + + + + $out = model output
$dyn = value of dynamic field
$[X] = value of tag X (replace X with the tag desired) +
+ + + + + + + + + + +
+ + + $res = number of iterations + + + + + + +
+ ) + }} + + + + + +
+} diff --git a/src/components/editors/formatForm.tsx b/src/components/editors/formatForm.tsx index 5f955c5..3921c13 100644 --- a/src/components/editors/formatForm.tsx +++ b/src/components/editors/formatForm.tsx @@ -104,7 +104,7 @@ export const FormatForm: React.FC = ({ format, updateFunction, deleteFunc return (
- +
) diff --git a/src/components/editors/modelForm.tsx b/src/components/editors/modelForm.tsx index aa1725a..f2a1752 100644 --- a/src/components/editors/modelForm.tsx +++ b/src/components/editors/modelForm.tsx @@ -1,9 +1,9 @@ import React, { ChangeEvent, useEffect, useState } from 'react' import { SelectableValue, StandardEditorContext } from "@grafana/data"; -import { FormatTags, ICredentials, IFormat, IModel, ISelect, ITag, Method } from 'utils/types'; +import { FormatTags, ICredentials, IExtraCalc, IFormat, IModel, ISelect, ITag, Method } from 'utils/types'; import { Button, Checkbox, CodeEditor, Collapse, ConfirmButton, ControlledCollapse, Form, FormAPI, HorizontalGroup, InlineField, InlineFieldRow, Input, InputControl, Select, useTheme2 } from '@grafana/ui'; import { ModelDefault } from 'utils/default'; -import { dataFrameToOptions, enumToSelect, formatsToOptions } from 'utils/utils' +import { dataFrameToOptions, enumToSelect, extraCalcToOptions, formatsToOptions } from 'utils/utils' import { TagsForm } from './tagsForm'; import { Mode } from 'utils/constants'; import { css } from '@emotion/css'; @@ -31,11 +31,13 @@ export const ModelForm: React.FC = ({ model, updateFunction, deleteFuncti const [selectedQuery, setSelectedQuery] = useState>() const [selectedExtraInfo, setSelectedExtraInfo] = useState>() const [selectedFormat, setSelectedFormat] = useState>() + const [selectedExtraCalc, setSelectedExtraCalc] = useState>() const [selectedVarTime, setSelectedVarTime] = useState>() const [selectedVarTags, setSelectedVarTags] = useState>() const [selectedQuotesListItems, setSelectedQuotesListItems] = useState>({ label: FormatTags.None, value: FormatTags['None'] }) const [queryOptions, setQueryOptions] = useState([]) const [formatOptions, setFormatOptions] = useState([]) + const [extraCalcOptions, setExtraCalcOptions] = useState([]) const [code, setCode] = useState("") const [disabled, setDisabled] = useState(false) const [scaler, setScaler] = useState("") @@ -49,6 +51,7 @@ export const ModelForm: React.FC = ({ model, updateFunction, deleteFuncti setSelectedQuery({ label: model.queryId, value: model.queryId }) if (model.extraInfo !== undefined) setSelectedExtraInfo({ label: model.extraInfo, value: model.extraInfo }) if (model.format !== undefined) setSelectedFormat({ label: model.format.id, value: model.format }) + if (model.extraCalc !== undefined) setSelectedExtraCalc({ label: model.extraCalc.id, value: model.extraCalc }) setSelectedVarTags({ label: model.varTags, value: model.varTags }) setSelectedVarTime({ label: model.varTime, value: model.varTime }) setSelectedQuotesListItems({ label: model.formatTags, value: model.formatTags }) @@ -90,6 +93,7 @@ export const ModelForm: React.FC = ({ model, updateFunction, deleteFuncti varTags: selectedVarTags?.value, formatTags: selectedQuotesListItems?.value, format: selectedFormat?.value, + extraCalc: selectedExtraCalc?.value, preprocess: code, credentials: credentials, isListValues: listValues, @@ -147,6 +151,14 @@ export const ModelForm: React.FC = ({ model, updateFunction, deleteFuncti setQueryOptions(dataFrameToOptions(context.data)) }, [context.data]) + useEffect(() => { + if (context.options.extraCalcs !== undefined) { + setExtraCalcOptions(extraCalcToOptions(context.options.extraCalcs)) + } else { + setExtraCalcOptions([]) + } + }, [context.options.formats]) + useEffect(() => { if (context.options.formats !== undefined) { setFormatOptions(formatsToOptions(context.options.formats)) @@ -211,6 +223,21 @@ export const ModelForm: React.FC = ({ model, updateFunction, deleteFuncti + + + -
{(fileCSV == undefined) ? context.messages._panel._step2.noFile : fileCSV.name}
+
{(fileCSV === undefined) ? context.messages._panel._step2.noFile : fileCSV.name}
/* = ({ model, collections, addCollection, /> */ - const ImportDatetimeSet =
- + -
+
= ({ model, collections, addCollection,
-} \ No newline at end of file +} diff --git a/src/components/step4_PredictModel.tsx b/src/components/step4_PredictModel.tsx index 9e6173a..97d4024 100644 --- a/src/components/step4_PredictModel.tsx +++ b/src/components/step4_PredictModel.tsx @@ -1,4 +1,4 @@ -import { Button, HorizontalGroup, IconButton, Modal, Spinner, useTheme2, VerticalGroup } from '@grafana/ui' +import { Button, HorizontalGroup, IconButton, InlineField, Input, Modal, Pagination, Spinner, useTheme2, VerticalGroup } from '@grafana/ui' import React, { Fragment, useContext, useEffect, useState } from 'react' import { Context, dateTimeLocalToString, getMean, round, groupBy } from 'utils/utils' import { IData, IDataCollection, IModel, IntervalTypeEnum, IResult, ITag } from 'utils/types' @@ -8,6 +8,7 @@ import Plot from 'react-plotly.js' import { Config, Icons, Layout, ModeBarButtonAny, PlotlyHTMLElement, toImage } from 'plotly.js' import { getAppEvents } from '@grafana/runtime' import { AppEvents, isDateTime, PanelData } from '@grafana/data' +import { extraCalcCollection } from 'utils/datasources/extraCalc' interface Props { model?: IModel, collections: IDataCollection[], @@ -32,6 +33,8 @@ export const PredictModel: React.FC = ({ model, collections, updateCollec const [sizePlot, setSizePlot] = useState<{ width: number, height: number }>({ width: 0, height: 0 }) const [isOpenModal, setIsOpenModal] = useState(false) const [isCollapseExtraInfo, setIsCollapseExtraInfo] = useState(false) + const [dynamicFieldValue, setDynamicFieldValue] = useState(0) + const [currentPage, setCurrentPage] = useState(1) const disabled = (context.actualStep) ? context.actualStep < Steps.step_3 : false const disabledModifyAgain = (context.actualStep) ? context.actualStep < Steps.step_4 : false @@ -67,6 +70,7 @@ export const PredictModel: React.FC = ({ model, collections, updateCollec setState(StatePredict.LOADING) predictAllCollections(model, collections).then((res: IDataCollection[]) => { updateCollections(res) + setCurrentPage(1) }) } if (context.setActualStep) { @@ -75,11 +79,32 @@ export const PredictModel: React.FC = ({ model, collections, updateCollec } } + const onClickExtraCalcHandle = () => { + if (validate()) { + //console.log("COLLECTIONS PREDICT", collections) + if (model && currentCollIdx !== undefined && currentCollIdx < collections.length) { + setState(StatePredict.LOADING) + let col: IDataCollection = collections[currentCollIdx] + delete col.resultsExtraCalc + delete col.conclusionExtraCalc + console.log("collll", col) + extraCalcCollection(model, col, dynamicFieldValue).then((res: IDataCollection) => { + let aux = [...collections] + aux[currentCollIdx] = res + updateCollections(aux) + setCurrentPage(2) + }) + } + } + } + const onClickModifyAgainHandle = () => { let newCollections: IDataCollection[] = [] collections.forEach((col: IDataCollection) => { let newCol = { ...col } delete newCol.results + delete newCol.resultsExtraCalc + delete newCol.conclusionExtraCalc newCollections.push(newCol) }) updateCollections(newCollections) @@ -108,9 +133,10 @@ export const PredictModel: React.FC = ({ model, collections, updateCollec const divResults = document.getElementById('id-results') const divResultsBase = document.getElementById('id-results-base') const divHeaders = document.getElementById('idResultsHeaders') + const divPagination = document.getElementById('id-pagination') if (divResults && divHeaders && divResultsBase) { setSizePlot({ - height: context.height - divHeaders.offsetHeight - divResultsBase.offsetHeight - 41.5,//- 47, + height: context.height - divHeaders.offsetHeight - divResultsBase.offsetHeight - 41.5 - ((divPagination) ? (divPagination.offsetHeight+9.5) : 0),//- 47, width: (divResults.offsetWidth < context.width) ? divResults.offsetWidth : context.width }) //console.log('width', divResults.offsetWidth) @@ -119,107 +145,103 @@ export const PredictModel: React.FC = ({ model, collections, updateCollec }, [context.width, context.height, state, currentCollIdx, isCollapseExtraInfo]) - + // HTML // ------------------------------------------------------------------------------------------------------------- - const showPlot = (col: IDataCollection) => { - if (col.results) { - const results = col.results.filter((r: IResult) => r.id !== idDefault && r.id !== idNew && r.correspondsWith !== undefined && r.result !== 'ERROR' && r.result !== undefined) - if (results.length < 1) return
- - const tagsGroup: { [tag: string]: IResult[] } = groupBy(results, "tag") - //console.log("tagGroup", tagsGroup) - - const dataArray: any[] = Object.entries(tagsGroup).map(([tag, resultsOfTag]) => { - // No pongo la x global por si alguna falla que no se rompa toda la grafica - let values_x: number[] = [], values_y: number[] = [], text: number[] = [] - resultsOfTag.forEach((r: IResult) => { - if (r.result !== undefined && r.result !== 'ERROR' - && r.correspondsWith !== undefined && r.data[r.correspondsWith.tag] !== undefined && typeof r.result === 'number') { - values_x.push(r.correspondsWith.intervalValue) - values_y.push(r.result) - text.push(setDecimals(getMean(r.data[r.correspondsWith.tag]))) - } - }) - - values_y = values_y.map((v: number) => setDecimals(v)) - //console.log("text", text) - return { - x: values_x, - y: values_y, - text: text, - type: 'scatter', - name: tag + const showPlot = (results: IResult[], intervalType: IntervalTypeEnum) => { + const filterResults = results.filter((r: IResult) => r.id !== idDefault && r.id !== idNew && r.correspondsWith !== undefined && r.result !== 'ERROR' && r.result !== undefined) + if (filterResults.length < 1) return
+ + const tagsGroup: { [tag: string]: IResult[] } = groupBy(filterResults, "tag") + //console.log("tagGroup", tagsGroup) + + const dataArray: any[] = Object.entries(tagsGroup).map(([tag, resultsOfTag]) => { + // No pongo la x global por si alguna falla que no se rompa toda la grafica + let values_x: number[] = [], values_y: number[] = [], text: number[] = [] + resultsOfTag.forEach((r: IResult) => { + if (r.result !== undefined && r.result !== 'ERROR' + && r.correspondsWith !== undefined && r.data[r.correspondsWith.tag] !== undefined && typeof r.result === 'number') { + values_x.push(r.correspondsWith.intervalValue) + values_y.push(r.result) + text.push(setDecimals(getMean(r.data[r.correspondsWith.tag]))) } }) - //console.log('dataArrayPLOT', dataArray) - - const layoutObj: Partial = { - width: sizePlot.width, - height: sizePlot.height, - showlegend: true, - legend: { - orientation: 'h', - font: { - color: theme.colors.text.primary - } - }, - margin: { - t: 30, - b: 80, - l: 80, - r: 50 - }, - paper_bgcolor: 'rgba(0,0,0,0)', - plot_bgcolor: 'rgba(0,0,0,0)', - xaxis: { - tickcolor: theme.colors.text.primary, - zerolinecolor: theme.colors.text.primary, - gridcolor: theme.colors.text.primary, - color: theme.colors.text.primary, - ticksuffix: (col.interval.type === IntervalTypeEnum.percentage) ? "%" : "" - }, - yaxis: { - tickcolor: theme.colors.text.primary, - zerolinecolor: theme.colors.text.primary, - gridcolor: theme.colors.text.primary, - color: theme.colors.text.primary - } + values_y = values_y.map((v: number) => setDecimals(v)) + //console.log("text", text) + return { + x: values_x, + y: values_y, + text: text, + type: 'scatter', + name: tag } + }) - const newButton: ModeBarButtonAny = { - title: 'Download plot as png', - name: 'Download plot as png', - icon: Icons.camera, - click: async (gd: PlotlyHTMLElement) => { - //console.log("AAA") - await toImage(gd, { - width: 900, - height: 900, - format: 'png' - }).then((img: string) => { - //let image = new Image() - //image.src = img - let w = window.open("") - if (w !== null) w.document.write(''); - - //w.document.write(image.outerHTML) - }) + //console.log('dataArrayPLOT', dataArray) + + const layoutObj: Partial = { + width: sizePlot.width, + height: sizePlot.height, + showlegend: true, + legend: { + orientation: 'h', + font: { + color: theme.colors.text.primary } + }, + margin: { + t: 30, + b: 80, + l: 80, + r: 50 + }, + paper_bgcolor: 'rgba(0,0,0,0)', + plot_bgcolor: 'rgba(0,0,0,0)', + xaxis: { + tickcolor: theme.colors.text.primary, + zerolinecolor: theme.colors.text.primary, + gridcolor: theme.colors.text.primary, + color: theme.colors.text.primary, + ticksuffix: (intervalType === IntervalTypeEnum.percentage) ? "%" : "" + }, + yaxis: { + tickcolor: theme.colors.text.primary, + zerolinecolor: theme.colors.text.primary, + gridcolor: theme.colors.text.primary, + color: theme.colors.text.primary } + } - const config: Partial = { - //modeBarButtonsToAdd: [btnCapture], - modeBarButtonsToRemove: ['toImage'], - modeBarButtonsToAdd: [newButton] + const newButton: ModeBarButtonAny = { + title: 'Download plot as png', + name: 'Download plot as png', + icon: Icons.camera, + click: async (gd: PlotlyHTMLElement) => { + //console.log("AAA") + await toImage(gd, { + width: 900, + height: 900, + format: 'png' + }).then((img: string) => { + //let image = new Image() + //image.src = img + let w = window.open("") + if (w !== null) w.document.write(''); + + //w.document.write(image.outerHTML) + }) } + } - return - } else { - return
+ const config: Partial = { + //modeBarButtonsToAdd: [btnCapture], + modeBarButtonsToRemove: ['toImage'], + modeBarButtonsToAdd: [newButton] } + + return } const defaultValue = (col: IDataCollection) => { @@ -289,21 +311,58 @@ export const PredictModel: React.FC = ({ model, collections, updateCollec return
} + const resultExtraCalc = (col: IDataCollection) => { + if (col.conclusionExtraCalc !== undefined) { + return
+

{msgs.resultCalc}

+

{col.conclusionExtraCalc}

+
+ } + return
+ } + + const divExtraCalc = () => { + if (model !== undefined && model.extraCalc !== undefined) { + return
+ {(model.extraCalc.dynamicFieldName !== undefined) ? + + { v.preventDefault(); setDynamicFieldValue(parseFloat(v.currentTarget.value)) }} /> + :
} + +
+ } + return
+ } + const getButton = (currentCollIdx !== undefined && currentCollIdx < collections.length && collections[currentCollIdx].results) ? : - const showResults = () => { - if (currentCollIdx !== undefined && currentCollIdx < collections.length && collections[currentCollIdx].results) { - const col: IDataCollection = collections[currentCollIdx] + const extraCalcResult = (col: IDataCollection) => { + if (col.resultsExtraCalc || col.conclusionExtraCalc) { + return
+
+ {resultExtraCalc(col)} +
+
+ {(col.resultsExtraCalc) ? showPlot(col.resultsExtraCalc, IntervalTypeEnum.units) :
} +
+
+ } else { + return
+ } + } + + const predictResult = (col: IDataCollection) => { + if (col.results) { return
{defaultValue(col)} {newValue(col)}
- {showPlot(col)} + {(col.results) ? showPlot(col.results, col.interval.type) :
}
} else { @@ -311,6 +370,25 @@ export const PredictModel: React.FC = ({ model, collections, updateCollec } } + const showResults = () => { + if (currentCollIdx !== undefined && currentCollIdx < collections.length) { + const col: IDataCollection = collections[currentCollIdx] + if (col.results && col.resultsExtraCalc) { + return
+
+ setCurrentPage(num)} /> +
+ {(currentPage === 1) ? predictResult(col) : extraCalcResult(col)} +
+ } else if (col.results) { + return predictResult(col) + } else if (col.resultsExtraCalc) { + return extraCalcResult(col) + } + } + return
+ } + const viewResults = () => { switch (state) { case StatePredict.LOADING: @@ -330,6 +408,7 @@ export const PredictModel: React.FC = ({ model, collections, updateCollec {getButton} + {divExtraCalc()}
{divExtraInfo()}
diff --git a/src/css/others.css b/src/css/others.css index 4200a18..f4fc2eb 100644 --- a/src/css/others.css +++ b/src/css/others.css @@ -1,3 +1,11 @@ +.paginationBlock { + width: 100% !important; + height: 30px; + justify-content: center; + display: flex; + margin-top: 10px; +} + .fullWidth { width: 100%; } diff --git a/src/module.ts b/src/module.ts index bbb9234..4145f8a 100644 --- a/src/module.ts +++ b/src/module.ts @@ -7,6 +7,7 @@ import './css/bootstrap-grid.css'; import './css/grid.css'; import './css/others.css'; import { Options } from 'utils/types'; +import { ExtraCalcEditor } from 'components/editors/extraCalcEditor'; export const plugin = new PanelPlugin(Main).setPanelOptions((builder) => { return builder @@ -33,6 +34,12 @@ export const plugin = new PanelPlugin(Main).setPanelOptions((builder) = name: 'Formats', category: ['Formats'], editor: FormatEditor + }).addCustomEditor({ + path: 'extraCalcs', + id: 'extraCalcs', + name: 'Extra calculations', + category: ['Extra calculations'], + editor: ExtraCalcEditor }).addCustomEditor({ path: 'models', id: 'models', diff --git a/src/utils/datasources/extraCalc.tsx b/src/utils/datasources/extraCalc.tsx new file mode 100644 index 0000000..4cf9d07 --- /dev/null +++ b/src/utils/datasources/extraCalc.tsx @@ -0,0 +1,109 @@ +import { Calc, ExtraCalcFormat, IDataCollection, IDataPred, IModel, IResult, ITag } from "utils/types" +import { getListValuesFromNew, newDataToObject, predictResults } from "./predictions" +import vm from 'vm' +import { getMean } from "utils/utils" +import { dateTime, DateTime } from "@grafana/data" + +const replaceVariables = (text: string, results: IResult[], dyn?: number ) => { + const lastResult = results[results.length-1] + + // $out + if(lastResult.result) text = text.replace(/\$out/g, lastResult.result as string) + + // $dyn + if(dyn !== undefined) text = text.replace(/\$dyn/g, dyn.toString()) + + // $[X] + text = text.replace(/\$\[(\w+)\]/g, (match, varName) => { + return (lastResult.data.hasOwnProperty(varName) && lastResult.data[varName] !== undefined) ? lastResult.data[varName][0].toString() : match; + }); + + console.log("text", text) + return text +} + +const executeString = (code: string) => { + try { + let context = vm.createContext() + let res = vm.runInContext(code, context) + return res + } catch (error) { + console.log(error) + console.error("Execution failed") + } +} + +const applyCalcValue = (first: number, second: number, calc: Calc): number => { + switch(calc) { + case Calc.sub: + return first - second + case Calc.div: + return first / second + case Calc.mul: + return first * second + default: + return first + second + } +} + +const applyFormatToRes = (res: number, format: ExtraCalcFormat, selectedDate?: DateTime, process?: string) => { + if (format !== ExtraCalcFormat.raw) { + if (process) { + const code = process.replace(/\$res/g, res.toString()) + res = executeString(code) as number + } + if(format === ExtraCalcFormat.addDays && selectedDate) { + let copyDate = dateTime(selectedDate) + return copyDate.add(res, 'days').toDate().toLocaleDateString('en-EN', { year: 'numeric', month: 'long', day: 'numeric' }) + " (" + res + "d)" + } + } + return res.toString() +} + +export const extraCalcCollection = async (model: IModel, col: IDataCollection, dyn?: number): Promise => { + if (model.extraCalc && model.tags.some((tag: ITag) => tag.id === model.extraCalc?.tag)) { + const tag = model.extraCalc.tag + let data: IDataPred = prepareInitialData(col, model.numberOfValues) + const iniResult: IResult = { + id: "extraCalc", + data: {...data}, + correspondsWith: { tag: tag, intervalValue: getMean(data[tag])} + } + let results: IResult[] = await predictResults(model, [iniResult]) + + let num_iter = 0 + let condition = replaceVariables(model.extraCalc.until, results, dyn) + const calcValue = executeString(replaceVariables(model.extraCalc.calcValue, results, dyn)) + + while (!executeString(condition)) { + num_iter += 1 + const mean = getMean(data[tag]) + let newValue = applyCalcValue(mean, calcValue, model.extraCalc.calc) + data = { + ...data, + [tag]: getListValuesFromNew(newValue, mean, data[tag]) + } + let newRes: IResult[] = await predictResults(model, [{ + id: ("extraCalc_" + num_iter), + data: {...data}, + correspondsWith: { + tag: tag, + intervalValue: newValue + } + }]) + + results = [...results, ...newRes] + condition = replaceVariables(model.extraCalc.until, results, dyn) + } + col.resultsExtraCalc = results + col.conclusionExtraCalc = applyFormatToRes(num_iter, model.extraCalc.resFormat, col.dateTime ,model.extraCalc.resProcess) + } else { + col.conclusionExtraCalc = "ERROR" + } + return col +} + +const prepareInitialData = (dataCollection: IDataCollection, numberOfValues?: number): IDataPred => { + const hasInterval = dataCollection.interval.max !== undefined && dataCollection.interval.min !== undefined && dataCollection.interval.steps !== undefined + return newDataToObject(dataCollection.data, hasInterval, numberOfValues) +} diff --git a/src/utils/datasources/predictions.tsx b/src/utils/datasources/predictions.tsx index 8263f7e..1fe0ab6 100644 --- a/src/utils/datasources/predictions.tsx +++ b/src/utils/datasources/predictions.tsx @@ -14,10 +14,7 @@ export const predictAllCollections = async (model: IModel, allData: IDataCollect return allData } -const predictData = async (model: IModel, dataCollection: IDataCollection) => { - console.log("numValues", model.numberOfValues) - console.log("dataCollection", dataCollection) - let results: IResult[] = prepareData(dataCollection, model.numberOfValues) +export const predictResults = async (model: IModel, results: IResult[]) => { let dataToPredict: IDataPred[] = [] for (const [i, r] of results.entries()) { let finalData = deepCopy(r.data) @@ -39,6 +36,13 @@ const predictData = async (model: IModel, dataCollection: IDataCollection) => { return results.map((r: IResult, indx: number) => { return { ...r, result: predictions[indx] } }) } +const predictData = async (model: IModel, dataCollection: IDataCollection) => { + console.log("numValues", model.numberOfValues) + console.log("dataCollection", dataCollection) + let results: IResult[] = prepareData(dataCollection, model.numberOfValues) + return await predictResults(model, results) +} + const getValuesFromInterval = (interval: IInterval): number[] => { if (interval.max === undefined || interval.min === undefined || interval.steps === undefined) return [] @@ -57,7 +61,7 @@ const calculatePercentage = (percent: number, total: number) => { return (percent / 100) * total } -const getListValuesFromNew = (newValue: number, mean: number, rawValues: number[]): number[] => { +export const getListValuesFromNew = (newValue: number, mean: number, rawValues: number[]): number[] => { let weights = rawValues.map((r: number) => Math.abs(r) / Math.abs(mean)) // Obtener PESOS return weights.map((w: number) => w * newValue) // Otener nuevos valores a partir del nuevo y los pesos } @@ -95,7 +99,7 @@ const defaultDataToObject = (data: IData[]): IDataPred => { } -const newDataToObject = (data: IData[], hasInterval: boolean, numberOfElements = 1): IDataPred => { +export const newDataToObject = (data: IData[], hasInterval: boolean, numberOfElements = 1): IDataPred => { let res: IDataPred = {} data.forEach((d: IData) => { let vals = (d.raw_values) ? d.raw_values : [] diff --git a/src/utils/default.tsx b/src/utils/default.tsx index 686d828..54df62c 100644 --- a/src/utils/default.tsx +++ b/src/utils/default.tsx @@ -1,5 +1,5 @@ import { messages_en } from "./localization/en"; -import { FormatTags, IContext, IDataCollection, IFormat, IModel, IInterval, ITag, Language, Method, IntervalTypeEnum } from "./types"; +import { FormatTags, IContext, IDataCollection, IFormat, IModel, IInterval, ITag, Language, Method, IntervalTypeEnum, IExtraCalc, Calc, ExtraCalcFormat } from "./types"; export const PreprocessCodeDefault = "console.log('Preprocess')" @@ -9,6 +9,18 @@ export const FormatDefault: IFormat = { output: "" } +export const ExtraCalcDefault: IExtraCalc = { + id: "", + name: "", + tag: "", + calc: Calc.sum, + calcValue: "", + until: "", + resProcess: "$res", + maxIterations: 1000, + resFormat: ExtraCalcFormat.raw +} + export const ModelDefault: IModel = { id: "", url: "", @@ -42,7 +54,8 @@ export const ContextDefault: IContext = { options: { language: Language.English, models: [], - formats: [] + formats: [], + extraCalcs: [] } } diff --git a/src/utils/localization/en.ts b/src/utils/localization/en.ts index 6b7f5dd..642492f 100644 --- a/src/utils/localization/en.ts +++ b/src/utils/localization/en.ts @@ -21,8 +21,8 @@ export const messages_en: ILocalization = { _step3: { modifyData: "Modify data", interval: "Interval", - min: "Min", - max: "Max", + min: "Backward", + max: "Forward", steps: "Steps", type: "Type", searchPlaceholder: "Search", @@ -44,7 +44,8 @@ export const messages_en: ILocalization = { newValue: "New value", modifyAgain: "Modify data again", extraInfo : "Extra information", - seeMore : "See more" + seeMore : "See more", + resultCalc: "Calculation result" }, _step5: { exportData: "Export data", diff --git a/src/utils/localization/es.ts b/src/utils/localization/es.ts index e351199..c4375ee 100644 --- a/src/utils/localization/es.ts +++ b/src/utils/localization/es.ts @@ -21,8 +21,8 @@ export const messages_es: ILocalization = { _step3: { modifyData: "Modificar datos", interval: "Intervalo", - min: "Min", - max: "Max", + min: "Atrás", + max: "Adelante", steps: "Pasos", type: "Tipo", searchPlaceholder: "Buscar", @@ -44,7 +44,8 @@ export const messages_es: ILocalization = { newValue: "Valor nuevo", modifyAgain: "Volver a modificar los datos", extraInfo : "Información extra", - seeMore : "Ver más" + seeMore : "Ver más", + resultCalc: "Resultado del cálculo" }, _step5: { exportData: "Exportar datos", diff --git a/src/utils/localization/scheme.ts b/src/utils/localization/scheme.ts index ac8aedb..c485b7d 100644 --- a/src/utils/localization/scheme.ts +++ b/src/utils/localization/scheme.ts @@ -1,56 +1,57 @@ export interface IPanel { - step : string - _step1 : { - selectModel : string + step: string + _step1: { + selectModel: string } - _step2 : { - importData : string - addData : string - setDatetime : string - variablesGrafana : string - usingQuery : string - uploadFile : string - noFile : string - alertCollectionAdded : string - alertCSVnoData : string - alertDateTimenoData : string + _step2: { + importData: string + addData: string + setDatetime: string + variablesGrafana: string + usingQuery: string + uploadFile: string + noFile: string + alertCollectionAdded: string + alertCSVnoData: string + alertDateTimenoData: string } - _step3 : { - modifyData : string - interval : string - min : string - max : string - steps : string - type : string - searchPlaceholder : string - units : string - intervalTypeTooltipPercentage : string - intervalTypeTooltipUnits : string, - showCategories : string - currentCollection : string - tooltipShowCategory : string - tooltipDeleteCurrentCollection : string - tooltipInterval : string - alertCollectionDeleted : string - delete : string + _step3: { + modifyData: string + interval: string + min: string + max: string + steps: string + type: string + searchPlaceholder: string + units: string + intervalTypeTooltipPercentage: string + intervalTypeTooltipUnits: string, + showCategories: string + currentCollection: string + tooltipShowCategory: string + tooltipDeleteCurrentCollection: string + tooltipInterval: string + alertCollectionDeleted: string + delete: string } - _step4 : { - predict : string - predictResult : string - originalValue : string + _step4: { + predict: string + predictResult: string + originalValue: string + resultCalc: string newValue: string - modifyAgain : string - extraInfo : string, - seeMore : string + modifyAgain: string + extraInfo: string, + seeMore: string } - _step5 : { - exportData : string - downloadData : string - downloadResults : string - tooltipButton : string + _step5: { + exportData: string + downloadData: string + downloadResults: string + tooltipButton: string } } export interface ILocalization { - _panel : IPanel -} \ No newline at end of file + _panel: IPanel +} diff --git a/src/utils/types.tsx b/src/utils/types.tsx index 25e3c96..90c07e9 100644 --- a/src/utils/types.tsx +++ b/src/utils/types.tsx @@ -7,6 +7,19 @@ export enum Language { Spanish = 'es' } +export enum Calc { + sum = "+", + sub = "-", + mul = "*", + div = "/" +} + +export enum ExtraCalcFormat { + raw = "Raw", + process = "Number processed", + addDays = "Add as days to selected date" +} + export enum FormatTags { None = 'None', dq = 'Double quotes', @@ -55,11 +68,13 @@ export interface ITag { export interface IDataCollection { id: string, name: string, - dateTime?: DateTime + dateTime?: DateTime, data: IData[], interval: IInterval, results?: IResult[] - extraInfo?: { [key: string]: any } + extraInfo?: { [key: string]: any }, + resultsExtraCalc?: IResult[], + conclusionExtraCalc?: string } export interface IData { @@ -101,6 +116,7 @@ export interface IModel { preprocess?: string, scaler?: IScaler, format?: IFormat, + extraCalc?: IExtraCalc, extraInfo?: string, varTags: string, formatTags: FormatTags, @@ -121,6 +137,19 @@ export interface IFormat { output: string } +export interface IExtraCalc { + id: string, + name: string, + dynamicFieldName?: string, + tag: string, + calc: Calc, + calcValue: string, + until: string, + resProcess: string, + maxIterations: number, + resFormat: ExtraCalcFormat +} + export interface IScaler { mean: number[] scale: number[] @@ -136,7 +165,8 @@ export interface ICSVScheme { export interface Options { language: Language, models: IModel[], - formats: IFormat[] + formats: IFormat[], + extraCalcs: IExtraCalc[] } export type IInterval = { diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx index 4e955d5..0d7bf85 100644 --- a/src/utils/utils.tsx +++ b/src/utils/utils.tsx @@ -1,7 +1,7 @@ import { DataFrame, DateTime } from '@grafana/data' import { createContext } from 'react' import { ContextDefault } from './default' -import { FormatTags, IContext, IDataCollection, IFormat, IModel, ISelect, ITag, Language } from './types' +import { FormatTags, IContext, IDataCollection, IExtraCalc, IFormat, IModel, ISelect, ITag, Language } from './types' import { ILocalization } from './localization/scheme' import { messages_es } from './localization/es' import { messages_en } from './localization/en' @@ -39,7 +39,7 @@ export const collectionsToSelect = (collections: IDataCollection[]): ISelect[] = } export const enumToSelect = (e: any) => { - return Object.entries(e).map(([key, value]) => ({ label: value as string, value: value})) + return Object.entries(e).map(([key, value]) => ({ label: value as string, value: value })) } export const tagsToString = (tags: ITag[], format: FormatTags) => { @@ -55,7 +55,7 @@ export const tagsToString = (tags: ITag[], format: FormatTags) => { } export const defaultIfUndefined = (obj: any, def: any) => { - return (obj == undefined) ? def : obj + return (obj === undefined) ? def : obj } export const disabledByJS = (disabled: boolean, id: string, document: any) => { @@ -96,6 +96,15 @@ export const dataFrameToOptions = (dataFrame: DataFrame[]): ISelect[] => { }) } +export const extraCalcToOptions = (extraCalcs: IExtraCalc[]): ISelect[] => { + return extraCalcs.map((f: IExtraCalc) => { + return { + value: f, + label: f.id + } + }) +} + export const formatsToOptions = (formats: IFormat[]): ISelect[] => { return formats.map((f: IFormat) => { return { @@ -141,7 +150,7 @@ export const getMessagesByLanguage = (l: Language): ILocalization => { export const getValueByKeyAnyDepth = (obj: any, search: string) => { let res: any = undefined - if(obj != undefined && obj != null) { + if (obj !== undefined && obj != null) { const keys = Object.keys(obj) for (let i = 0; i < keys.length && res === undefined; i++) { const key = Object.keys(obj)[i] @@ -159,11 +168,11 @@ export const isEmpty = (obj: any) => { return Object.keys(obj).length === 0; } -export const getMean = (list:number[]) : number => { +export const getMean = (list: number[]): number => { let newlist = list.filter((e) => e != null) - return (newlist.length > 0) ? newlist.reduce((previous, current) => current += previous)/newlist.length : -1 //Throw error + return (newlist.length > 0) ? newlist.reduce((previous, current) => current += previous) / newlist.length : -1 //Throw error } -export const transposeMatrix = (matrix:number[][]) : number[][] => { - return matrix[0].map((_, colIndex) => matrix.map(row => row[colIndex])); -} \ No newline at end of file +export const transposeMatrix = (matrix: number[][]): number[][] => { + return matrix[0].map((_, colIndex) => matrix.map(row => row[colIndex])); +}