From a7bda3400bdf7d7bfd7fcccecabd0b09589167c0 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Thu, 20 Mar 2025 14:43:01 +0100 Subject: [PATCH 01/15] very very wip --- .../query/AssetDatapointIntervalQuery.java | 15 +- ui/component/or-attribute-report/.npmignore | 8 + ui/component/or-attribute-report/README.md | 31 + ui/component/or-attribute-report/build.gradle | 15 + ui/component/or-attribute-report/package.json | 42 + ui/component/or-attribute-report/src/index.ts | 1394 +++++++++++++++++ .../or-attribute-report/tsconfig.json | 20 + .../or-attribute-report/webpack.config.js | 18 + .../src/settings/report-settings.ts | 496 ++++++ .../src/widgets/report-widget.ts | 251 +++ 10 files changed, 2287 insertions(+), 3 deletions(-) create mode 100644 ui/component/or-attribute-report/.npmignore create mode 100644 ui/component/or-attribute-report/README.md create mode 100644 ui/component/or-attribute-report/build.gradle create mode 100644 ui/component/or-attribute-report/package.json create mode 100644 ui/component/or-attribute-report/src/index.ts create mode 100644 ui/component/or-attribute-report/tsconfig.json create mode 100644 ui/component/or-attribute-report/webpack.config.js create mode 100644 ui/component/or-dashboard-builder/src/settings/report-settings.ts create mode 100644 ui/component/or-dashboard-builder/src/widgets/report-widget.ts diff --git a/model/src/main/java/org/openremote/model/datapoint/query/AssetDatapointIntervalQuery.java b/model/src/main/java/org/openremote/model/datapoint/query/AssetDatapointIntervalQuery.java index dc38a60c54..e10444e7e7 100644 --- a/model/src/main/java/org/openremote/model/datapoint/query/AssetDatapointIntervalQuery.java +++ b/model/src/main/java/org/openremote/model/datapoint/query/AssetDatapointIntervalQuery.java @@ -16,7 +16,7 @@ public class AssetDatapointIntervalQuery extends AssetDatapointQuery { public Formula formula; public enum Formula { - MIN, AVG, MAX + MIN, AVG, MAX, DELTA } public AssetDatapointIntervalQuery() { @@ -42,9 +42,18 @@ public String getSQLQuery(String tableName, Class attributeType) throws Illeg boolean isBoolean = Boolean.class.isAssignableFrom(attributeType); String function = (gapFill ? "public.time_bucket_gapfill" : "public.time_bucket"); if (isNumber) { - return "select " + function + "(cast(? as interval), timestamp) AS x, " + this.formula.toString().toLowerCase() + "(cast(value as numeric)) FROM " + tableName + " WHERE ENTITY_ID = ? and ATTRIBUTE_NAME = ? and TIMESTAMP >= ? and TIMESTAMP <= ? GROUP BY x ORDER by x ASC"; + if(this.formula == Formula.DELTA) { + return "select " + function + "(cast(? as interval), timestamp) AS x, (last(cast(value as numeric), timestamp) - first(cast(value as numeric), timestamp)) FROM " + tableName + " WHERE ENTITY_ID = ? and ATTRIBUTE_REF = ? and " + + "timestamp BETWEEN ? AND ? GROUP BY x ORDER BY x"; + } else { + return "select " + function + "(cast(? as interval), timestamp) AS x, " + this.formula.toString().toLowerCase() + "(cast(value as numeric)) FROM " + tableName + " WHERE ENTITY_ID = ? and ATTRIBUTE_NAME = ? and TIMESTAMP >= ? and TIMESTAMP <= ? GROUP BY x ORDER by x ASC"; + } } else if (isBoolean) { - return "select " + function + "(cast(? as interval), timestamp) AS x, " + this.formula.toString().toLowerCase() + "(case when cast(cast(value as text) as boolean) is true then 1 else 0 end) FROM " + tableName + " WHERE ENTITY_ID = ? and ATTRIBUTE_NAME = ? and TIMESTAMP >= ? and TIMESTAMP <= ? GROUP BY x ORDER by x ASC"; + if (this.formula == Formula.DELTA) { + throw new IllegalStateException("Query of type DELTA is not applicable for boolean attributes."); + } else { + return "select " + function + "(cast(? as interval), timestamp) AS x, " + this.formula.toString().toLowerCase() + "(case when cast(cast(value as text) as boolean) is true then 1 else 0 end) FROM " + tableName + " WHERE ENTITY_ID = ? and ATTRIBUTE_NAME = ? and TIMESTAMP >= ? and TIMESTAMP <= ? GROUP BY x ORDER by x ASC"; + } } else { throw new IllegalStateException("Query of type Interval requires either a number or a boolean attribute."); } diff --git a/ui/component/or-attribute-report/.npmignore b/ui/component/or-attribute-report/.npmignore new file mode 100644 index 0000000000..e4d5a449c8 --- /dev/null +++ b/ui/component/or-attribute-report/.npmignore @@ -0,0 +1,8 @@ +build/ +build.gradle +test/ +src/ +webpack.config.js +tsconfig.json +dist/*.map +*.tsbuildinfo diff --git a/ui/component/or-attribute-report/README.md b/ui/component/or-attribute-report/README.md new file mode 100644 index 0000000000..41d230087e --- /dev/null +++ b/ui/component/or-attribute-report/README.md @@ -0,0 +1,31 @@ +# @openremote/or-attribute-history +[![NPM Version][npm-image]][npm-url] +[![Linux Build][travis-image]][travis-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Web Component for displaying chart values. + +## Install +```bash +npm i @openremote/or-attribute-report +yarn add @openremote/or-attribute-report +``` + +## Usage +For a full list of properties, methods and options refer to the TypeDoc generated [documentation](). + + +## Supported Browsers +The last 2 versions of all modern browsers are supported, including Chrome, Safari, Opera, Firefox, Edge. In addition, +Internet Explorer 11 is also supported. + + +## License +[GNU AGPL](https://www.gnu.org/licenses/agpl-3.0.en.html) + +[npm-image]: https://img.shields.io/npm/v/live-xxx.svg +[npm-url]: https://npmjs.org/package/@openremote/or-chart +[travis-image]: https://img.shields.io/travis/live-js/live-xxx/master.svg +[travis-url]: https://travis-ci.org/live-js/live-xxx +[coveralls-image]: https://img.shields.io/coveralls/live-js/live-xxx/master.svg +[coveralls-url]: https://coveralls.io/r/live-js/live-xxx?branch=master diff --git a/ui/component/or-attribute-report/build.gradle b/ui/component/or-attribute-report/build.gradle new file mode 100644 index 0000000000..98cdcb8e26 --- /dev/null +++ b/ui/component/or-attribute-report/build.gradle @@ -0,0 +1,15 @@ +buildDir = "dist" + +task clean() { + doLast { + delete "dist" + } +} + +task prepareUi() { + dependsOn clean, npmPrepare +} + +task publishUi() { + dependsOn clean, npmPublish +} \ No newline at end of file diff --git a/ui/component/or-attribute-report/package.json b/ui/component/or-attribute-report/package.json new file mode 100644 index 0000000000..1de79028b9 --- /dev/null +++ b/ui/component/or-attribute-report/package.json @@ -0,0 +1,42 @@ +{ + "name": "@openremote/or-chart", + "version": "1.3.3", + "description": "OpenRemote chart", + "main": "dist/umd/index.bundle.js", + "module": "lib/index.js", + "exports": { + ".": "./lib/index.js", + "./*": "./lib/*.js" + }, + "types": "lib/index.d.ts", + "scripts": { + "test": "echo \"No tests\" && exit 0", + "prepack": "npx webpack" + }, + "author": "OpenRemote", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@material/data-table": "^9.0.0", + "@material/dialog": "^9.0.0", + "@openremote/core": "workspace:*", + "@openremote/or-asset-tree": "workspace:*", + "@openremote/or-attribute-picker": "workspace:*", + "@openremote/or-components": "workspace:*", + "@openremote/or-icon": "workspace:*", + "@openremote/or-mwc-components": "workspace:*", + "@openremote/or-translate": "workspace:*", + "chart.js": "^3.6.0", + "echarts": "^5.6.0", + "jsonpath-plus": "^6.0.1", + "lit": "^2.0.2", + "moment": "^2.29.4" + }, + "devDependencies": { + "@openremote/util": "workspace:*", + "@types/chart.js": "^2.9.34", + "@types/offscreencanvas": "^2019.6.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts new file mode 100644 index 0000000000..db1d66662d --- /dev/null +++ b/ui/component/or-attribute-report/src/index.ts @@ -0,0 +1,1394 @@ +import {css, html, LitElement, PropertyValues, TemplateResult, unsafeCSS} from "lit"; +import {customElement, property, query} from "lit/decorators.js"; +import i18next from "i18next"; +import {translate} from "@openremote/or-translate"; +import { + Asset, + AssetDatapointQueryUnion, + AssetDatapointIntervalQueryFormula, + AssetEvent, + AssetModelUtil, + AssetQuery, + Attribute, + AttributeRef, + DatapointInterval, + ReadAssetEvent, + ValueDatapoint, + WellknownMetaItems +} from "@openremote/model"; +import manager, {DefaultColor2, DefaultColor3, DefaultColor4, DefaultColor5, Util} from "@openremote/core"; +import "@openremote/or-asset-tree"; +import "@openremote/or-mwc-components/or-mwc-input"; +import "@openremote/or-components/or-panel"; +import "@openremote/or-translate"; +import {ECharts, EChartsOption, init, graphic} from "echarts"; +import { + + TimeUnit, + +} from "chart.js"; +import {InputType, OrMwcInput} from "@openremote/or-mwc-components/or-mwc-input"; +import "@openremote/or-components/or-loading-indicator"; +import moment from "moment"; +import {OrAssetTreeSelectionEvent} from "@openremote/or-asset-tree"; +import {getAssetDescriptorIconTemplate} from "@openremote/or-icon"; +import {GenericAxiosResponse, isAxiosError} from "@openremote/rest"; +import {OrAttributePicker, OrAttributePickerPickedEvent} from "@openremote/or-attribute-picker"; +import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; +import {cache} from "lit/directives/cache.js"; +import {debounce, throttle} from "lodash"; +import {getContentWithMenuTemplate} from "@openremote/or-mwc-components/or-mwc-menu"; +import {ListItem} from "@openremote/or-mwc-components/or-mwc-list"; +import { when } from "lit/directives/when.js"; +import {createRef, Ref, ref } from "lit/directives/ref.js"; + +export class OrChartEvent extends CustomEvent { + + public static readonly NAME = "or-chart-event"; + + constructor(value?: any, previousValue?: any) { + super(OrChartEvent.NAME, { + detail: { + value: value, + previousValue: previousValue + }, + bubbles: true, + composed: true + }); + } +} + +export interface ChartViewConfig { + attributeRefs?: AttributeRef[]; + fromTimestamp?: number; + toTimestamp?: number; + /*compareOffset?: number;*/ + period?: moment.unitOfTime.Base; + deltaFormat?: "absolute" | "percentage"; + decimals?: number; +} + +export interface OrChartEventDetail { + value?: any; + previousValue?: any; +} + +declare global { + export interface HTMLElementEventMap { + [OrChartEvent.NAME]: OrChartEvent; + } +} + +export interface ChartConfig { + xLabel?: string; + yLabel?: string; +} + +export interface OrChartConfig { + chart?: ChartConfig; + realm?: string; + views: {[name: string]: { + [panelName: string]: ChartViewConfig + }}; +} + +// Declare require method which we'll use for importing webpack resources (using ES6 imports will confuse typescript parser) +declare function require(name: string): any; + +// TODO: Add webpack/rollup to build so consumers aren't forced to use the same tooling +const dialogStyle = require("@material/dialog/dist/mdc.dialog.css"); +const tableStyle = require("@material/data-table/dist/mdc.data-table.css"); + +// language=CSS +const style = css` + :host { + + --internal-or-chart-background-color: var(--or-chart-background-color, var(--or-app-color2, ${unsafeCSS(DefaultColor2)})); + --internal-or-chart-text-color: var(--or-chart-text-color, var(--or-app-color3, ${unsafeCSS(DefaultColor3)})); + --internal-or-chart-controls-margin: var(--or-chart-controls-margin, 0 0 20px 0); + --internal-or-chart-controls-margin-children: var(--or-chart-controls-margin-children, 0 auto 20px auto); + --internal-or-chart-graph-fill-color: var(--or-chart-graph-fill-color, var(--or-app-color4, ${unsafeCSS(DefaultColor4)})); + --internal-or-chart-graph-fill-opacity: var(--or-chart-graph-fill-opacity, 0.25); + --internal-or-chart-graph-line-color: var(--or-chart-graph-line-color, var(--or-app-color4, ${unsafeCSS(DefaultColor4)})); + --internal-or-chart-graph-point-color: var(--or-chart-graph-point-color, var(--or-app-color3, ${unsafeCSS(DefaultColor3)})); + --internal-or-chart-graph-point-border-color: var(--or-chart-graph-point-border-color, var(--or-app-color5, ${unsafeCSS(DefaultColor5)})); + --internal-or-chart-graph-point-radius: var(--or-chart-graph-point-radius, 4); + --internal-or-chart-graph-point-hit-radius: var(--or-chart-graph-point-hit-radius, 20); + --internal-or-chart-graph-point-border-width: var(--or-chart-graph-point-border-width, 2); + --internal-or-chart-graph-point-hover-color: var(--or-chart-graph-point-hover-color, var(--or-app-color5, ${unsafeCSS(DefaultColor5)})); + --internal-or-chart-graph-point-hover-border-color: var(--or-chart-graph-point-hover-border-color, var(--or-app-color3, ${unsafeCSS(DefaultColor3)})); + --internal-or-chart-graph-point-hover-radius: var(--or-chart-graph-point-hover-radius, 4); + --internal-or-chart-graph-point-hover-border-width: var(--or-chart-graph-point-hover-border-width, 2); + + width: 100%; + display: block; + } + + .line-label { + border-width: 1px; + border-color: var(--or-app-color3); + margin-right: 5px; + } + + .line-label.solid { + border-style: solid; + } + + .line-label.dashed { + background-image: linear-gradient(to bottom, var(--or-app-color3) 50%, white 50%); + width: 2px; + border: none; + background-size: 10px 16px; + background-repeat: repeat-y; + } + + .button-icon { + align-self: center; + padding: 10px; + cursor: pointer; + } + + a { + display: flex; + cursor: pointer; + text-decoration: underline; + font-weight: bold; + color: var(--or-app-color1); + --or-icon-width: 12px; + } + + .mdc-dialog .mdc-dialog__surface { + min-width: 600px; + height: calc(100vh - 50%); + } + + :host([hidden]) { + display: none; + } + + #container { + display: flex; + min-width: 0; + flex-direction: row; + height: 100%; + } + + #msg { + height: 100%; + width: 100%; + justify-content: center; + align-items: center; + text-align: center; + } + + #msg:not([hidden]) { + display: flex; + } + .period-controls { + display: flex; + min-width: 180px; + align-items: center; + } + + #controls { + display: flex; + flex-wrap: wrap; + margin: var(--internal-or-chart-controls-margin); + width: 100%; + flex-direction: column; + margin: 0; + } + + #attribute-list { + overflow: hidden auto; + min-height: 50px; + flex: 1 1 0; + width: 100%; + display: flex; + flex-direction: column; + } + .attribute-list-dense { + flex-wrap: wrap; + } + + .attribute-list-item { + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + padding: 0; + min-height: 50px; + } + .attribute-list-item-dense { + min-height: 28px; + } + + .button-clear { + background: none; + visibility: hidden; + color: ${unsafeCSS(DefaultColor5)}; + --or-icon-fill: ${unsafeCSS(DefaultColor5)}; + display: inline-block; + border: none; + padding: 0; + cursor: pointer; + } + + .attribute-list-item:hover .button-clear { + visibility: visible; + } + + .button-clear:hover { + --or-icon-fill: var(--or-app-color4); + } + + .attribute-list-item-label { + display: flex; + flex: 1 1 0; + line-height: 16px; + flex-direction: column; + } + + .attribute-list-item-bullet { + width: 12px; + height: 12px; + border-radius: 7px; + margin-right: 10px; + } + + .attribute-list-item .button.delete { + display: none; + } + + .attribute-list-item:hover .button.delete { + display: block; + } + + #controls > * { + margin-top: 5px; + margin-bottom: 5px; + } + + .dialog-container { + display: flex; + flex-direction: row; + flex: 1 1 0; + } + + .dialog-container > * { + flex: 1 1 0; + } + + .dialog-container > or-mwc-input { + background-color: var(--or-app-color2); + border-left: 3px solid var(--or-app-color4); + } + + #chart-container { + flex: 1 1 0; + position: relative; + overflow: hidden; + /*min-height: 400px; + max-height: 550px;*/ + } + #chart-controls { + display: flex; + flex-direction: column; + align-items: center; + } + #chart { + width: 100% !important; + height: 100%; !important; + } + + @media screen and (max-width: 1280px) { + #chart-container { + max-height: 330px; + } + } + + @media screen and (max-width: 769px) { + .mdc-dialog .mdc-dialog__surface { + min-width: auto; + + max-width: calc(100vw - 32px); + max-height: calc(100% - 32px); + } + + #container { + flex-direction: column; + } + + #controls { + min-width: 100%; + padding-left: 0; + } + .interval-controls, + .period-controls { + flex-direction: row; + justify-content: left; + align-items: center; + gap: 8px; + } + } +`; + +@customElement("or-attribute-report") +export class OrChart extends translate(i18next)(LitElement) { + + public static DEFAULT_TIMESTAMP_FORMAT = "L HH:mm:ss"; + + static get styles() { + return [ + css`${unsafeCSS(tableStyle)}`, + css`${unsafeCSS(dialogStyle)}`, + style + ]; + } + + @property({type: Object}) + public assets: Asset[] = []; + + @property({type: Object}) + private activeAsset?: Asset; + + @property({type: Object}) + public assetAttributes: [number, Attribute][] = []; + + @property({type: Array}) + public colorPickedAttributes: Array<{ attributeRef: AttributeRef; color: string }> = []; + + @property({type: Array}) + public attributeQueryFormula: Array<{ attributeRef: AttributeRef; formula: AssetDatapointIntervalQueryFormula }> = []; + + @property({type: Object}) + public attributeSettings: { + rightAxisAttributes: AttributeRef[], + smoothAttributes: AttributeRef[], + steppedAttributes: AttributeRef[], + areaAttributes: AttributeRef[], + faintAttributes: AttributeRef[], + extendedAttributes: AttributeRef[], + } = { + rightAxisAttributes: [], + smoothAttributes: [], + steppedAttributes: [], + areaAttributes: [], + faintAttributes: [], + extendedAttributes: [], + }; + + + @property() + public dataProvider?: (startOfPeriod: number, endOfPeriod: number, timeUnits: TimeUnit, stepSize: number) => Promise<[]> + + @property({type: Array}) + public colors: string[] = ["#3869B1", "#DA7E30", "#3F9852", "#CC2428", "#6B4C9A", "#922427", "#958C3D", "#535055"]; + + @property({type: Object}) + public readonly datapointQuery!: AssetDatapointQueryUnion; + + @property({type: Object}) + public config?: OrChartConfig; + + @property({type: Object}) + public chartOptions?: any + + @property({type: String}) + public realm?: string; + + @property() + public panelName?: string; + + @property() + public attributeControls: boolean = true; + + @property() + public timeframe?: [Date, Date]; + + @property() + public timestampControls: boolean = true; + + @property() + protected timePrefixOptions?: string[]; + + @property() + public timeWindowOptions?: Map; + + @property() + public timePrefixKey?: string; + + @property() + public timeWindowKey?: string; + + + + + @property() + public showLegend: boolean = true; + + @property() + public denseLegend: boolean = false; + + + @property() + public showToolBox: boolean = true; + + @property() + public showSymbolMaxDatapoints: number = 30; + + @property() + public maxConcurrentDatapoints: number = 100; + + @property() + protected _loading: boolean = false; + + + + @property() + protected _data?: ValueDatapoint[]; + + @property() + protected _tableTemplate?: TemplateResult; + + @query("#chart") + protected _chartElem!: HTMLDivElement; + protected _chartOptions: EChartsOption = {}; + protected _chart?: ECharts; + protected _style!: CSSStyleDeclaration; + protected _startOfPeriod?: number; + protected _endOfPeriod?: number; + + protected _timeUnits?: TimeUnit; + protected _stepSize?: number; + protected _latestError?: string; + protected _dataAbortController?: AbortController; + + protected _resizeHandler?: any; + protected _containerResizeObserver?: ResizeObserver; + + constructor() { + super(); + this.addEventListener(OrAssetTreeSelectionEvent.NAME, this._onTreeSelectionChanged); + } + + connectedCallback() { + super.connectedCallback(); + this._style = window.getComputedStyle(this); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this._cleanup(); + + } + + firstUpdated() { + this.loadSettings(false); + } + + updated(changedProperties: PropertyValues) { + + super.updated(changedProperties); + + if (changedProperties.has("realm")) { + if(changedProperties.get("realm") != undefined) { // Checking whether it was undefined previously, to prevent loading 2 times and resetting attribute properties. + this.assets = []; + this.loadSettings(true); + } + } + + const reloadData = changedProperties.has('colorPickedAttributes') || changedProperties.has("datapointQuery") || changedProperties.has("timeframe") || changedProperties.has("timePrefixKey") || changedProperties.has("timeWindowKey")|| + changedProperties.has("attributeSettings") || changedProperties.has("assetAttributes") || changedProperties.has("realm") || changedProperties.has("dataProvider"); + + if (reloadData) { + this._data = undefined; + if (this._chart) { + // Remove event listeners + this._toggleChartEventListeners(false); + this._chart.dispose(); + this._chart = undefined; + } + this._loadData(); + } + + if (!this._data) { + return; + } + + if (!this._chart) { + + let bgColor = this._style.getPropertyValue("--internal-or-chart-graph-fill-color").trim(); + const opacity = Number(this._style.getPropertyValue("--internal-or-chart-graph-fill-opacity").trim()); + if (!isNaN(opacity)) { + if (bgColor.startsWith("#") && (bgColor.length === 4 || bgColor.length === 7)) { + bgColor += (bgColor.length === 4 ? Math.round(opacity * 255).toString(16).substr(0, 1) : Math.round(opacity * 255).toString(16)); + } else if (bgColor.startsWith("rgb(")) { + bgColor = bgColor.substring(0, bgColor.length - 1) + opacity; + } + } + + + + this._chartOptions = { + animation: false, + grid: { + show: true, + backgroundColor: this._style.getPropertyValue("--internal-or-asset-tree-background-color"), + borderColor: this._style.getPropertyValue("--internal-or-chart-text-color"), + left: 50,//'5%', // 5% padding + right: 50,//'5%', + top: this.showToolBox ? 28 : 10, + bottom: 20 + }, + backgroundColor: this._style.getPropertyValue("--internal-or-asset-tree-background-color"), + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross' + }, + }, + toolbox: {}, + xAxis: { + type: 'category', + //axisLine: { + // onZero: false, + // lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")} + //}, + //splitLine: {show: true}, + //min: this._startOfPeriod, + //max: this._endOfPeriod, + //axisLabel: { + // showMinLabel: true, + // showMaxLabel: true, + // hideOverlap: true, + // fontSize: 10, + // formatter: { + // year: '{yyyy}-{MMM}', + // month: '{yy}-{MMM}', + // day: '{d}-{MMM}', + // hour: '{HH}:{mm}', + // minute: '{HH}:{mm}', + // second: '{HH}:{mm}:{ss}', + // millisecond: '{d}-{MMM} {HH}:{mm}', + // // @ts-ignore + // none: '{MMM}-{dd} {HH}:{mm}' + // } + //} + }, + yAxis: [ + { + type: 'value', + axisLine: { lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")}}, + boundaryGap: ['10%', '10%'], + scale: true, + min: this.chartOptions.options.scales.y.min ? this.chartOptions.options.scales.y.min : undefined, //NOG FIXEN MET MERGEN VAN CHARTOPTIONS + max: this.chartOptions.options.scales.y.max ? this.chartOptions.options.scales.y.max : undefined + }, + { + type: 'value', + show: this.attributeSettings.rightAxisAttributes.length > 0, + axisLine: { lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")}}, + boundaryGap: ['10%', '10%'], + scale: true, + min: this.chartOptions.options.scales.y1.min ? this.chartOptions.options.scales.y1.min : undefined, + max: this.chartOptions.options.scales.y1.max ? this.chartOptions.options.scales.y1.max : undefined + } + ], + dataZoom: [ + { + type: 'inside', + start: 0, + end: 100 + } + ], + series: [], + }; + + // Add dataZoom bar if enabled + (this._chartOptions!.dataZoom! as any[]).push({ + start: 0, + end: 100, + backgroundColor: bgColor, + fillerColor: bgColor, + dataBackground: { + areaStyle: { + color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") + } + }, + selectedDataBackground: { + areaStyle: { + color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color"), + } + }, + moveHandleStyle: { + color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") + }, + emphasis: { + moveHandleStyle: { + color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") + }, + handleLabel: { + show: false + } + }, + handleLabel: { + show: false + } + }) + + + // Add toolbox if enabled + if(this.showToolBox) { + this._chartOptions!.toolbox! = { + right: 45, + top: 0, + feature: { + dataView: {readOnly: true}, + //magicType: { + // type: ['line', 'bar'] + //}, + saveAsImage: {} + } + } + } + + // Initialize echarts instance + this._chart = init(this._chartElem); + // Set chart options to default + this._chart.setOption(this._chartOptions); + this._toggleChartEventListeners(true); + } + + if (changedProperties.has("_data")) { + //Update chart to data from set period + this._updateChartData(); + } + + this.onCompleted().then(() => { + this.dispatchEvent(new OrChartEvent('rendered')); + }); + + } + + // Not the best implementation, but it changes the legend & controls to wrap under the chart. + // Also sorts the attribute lists horizontally when it is below the chart + applyChartResponsiveness(): void { + if(this.shadowRoot) { + const container = this.shadowRoot.getElementById('container'); + if(container) { + const bottomLegend: boolean = (container.clientWidth < 600); + container.style.flexDirection = bottomLegend ? 'column' : 'row'; + const periodControls = this.shadowRoot.querySelector('.period-controls') as HTMLElement; + if(periodControls) { + periodControls.style.justifyContent = bottomLegend ? 'center' : 'space-between'; + periodControls.style.paddingLeft = bottomLegend ? '' : '18px'; + } + const attributeList = this.shadowRoot.getElementById('attribute-list'); + if(attributeList) { + attributeList.style.gap = bottomLegend ? '4px 12px' : ''; + attributeList.style.maxHeight = bottomLegend ? '90px' : ''; + attributeList.style.flexFlow = bottomLegend ? 'row wrap' : 'column nowrap'; + attributeList.style.padding = bottomLegend ? '0' : '12px 0'; + } + this.shadowRoot.querySelectorAll('.attribute-list-item').forEach((item: Element) => { + (item as HTMLElement).style.minHeight = bottomLegend ? '0px' : '44px'; + (item as HTMLElement).style.paddingLeft = bottomLegend ? '' : '16px'; + (item.children[1] as HTMLElement).style.flexDirection = bottomLegend ? 'row' : 'column'; + (item.children[1] as HTMLElement).style.gap = bottomLegend ? '4px' : ''; + }); + } + } + } + + render() { + const disabled = this._loading || this._latestError; + return html` +
+
+ ${when(this._loading, () => html` +
+ +
+ `)} + ${when(this._latestError, () => html` +
+ +
+ `)} +
+
+ + ${(this.timestampControls || this.attributeControls || this.showLegend) ? html` +
+
+
+ ${this.timePrefixKey && this.timePrefixOptions && this.timeWindowKey && this.timeWindowOptions ? html` + ${this.timestampControls ? html` + + + + ${getContentWithMenuTemplate( + html``, + this.timePrefixOptions.map((option) => ({ value: option } as ListItem)), + this.timePrefixKey, + (value: string | string[]) => { + this.timeframe = undefined; // remove any custom start & end times + this.timePrefixKey = value.toString(); + }, + undefined, + undefined, + undefined, + true + )} + + ${getContentWithMenuTemplate( + html``, + Array.from(this.timeWindowOptions!.keys()).map((key) => ({ value: key } as ListItem)), + this.timeWindowKey, + (value: string | string[]) => { + this.timeframe = undefined; // remove any custom start & end times + this.timeWindowKey = value.toString(); + }, + undefined, + undefined, + undefined, + true + )} + + + ` : html` + + `} + ` : undefined} +
+ ${this.timeframe ? html` +
+ + + + + + + + + + + + + +
${i18next.t('from')}:${moment(this.timeframe[0]).format("L HH:mm")}
${i18next.t('to')}:${moment(this.timeframe[1]).format("L HH:mm")}
+
+ ` : undefined} + ${this.attributeControls ? html` + + ` : undefined} +
+ ${cache(this.showLegend ? html` +
+ ${this.assetAttributes == null || this.assetAttributes.length == 0 ? html` +
+ ${i18next.t('noAttributesConnected')} +
+ ` : undefined} + ${this.assetAttributes && this.assetAttributes.map(([assetIndex, attr], index) => { + const asset: Asset | undefined = this.assets[assetIndex]; + const colourIndex = index % this.colors.length; + const color = this.colorPickedAttributes.find(({ attributeRef }) => attributeRef.name === attr.name && attributeRef.id === asset.id)?.color; + const descriptors = AssetModelUtil.getAttributeAndValueDescriptors(asset!.type, attr.name, attr); + const label = Util.getAttributeLabel(attr, descriptors[0], asset!.type, true); + const axisNote = (this.attributeSettings.rightAxisAttributes.find(ar => asset!.id === ar.id && attr.name === ar.name)) ? i18next.t('right') : undefined; + const bgColor = ( color ?? this.colors[colourIndex] ) || ""; + return html` +
+ ${getAssetDescriptorIconTemplate(AssetModelUtil.getAssetDescriptor(this.assets[assetIndex]!.type!), undefined, undefined, bgColor.split('#')[1])} +
+
+ ${this.assets[assetIndex].name} + ${when(axisNote, () => html`(${axisNote})`)} +
+ ${label} +
+
+ ` + })} +
+ ` : undefined)} +
+ ` : undefined} +
+ `; + } + + protected async _onTreeSelectionChanged(event: OrAssetTreeSelectionEvent) { + // Need to fully load the asset + if (!manager.events) { + return; + } + + const selectedNode = event.detail && event.detail.newNodes.length > 0 ? event.detail.newNodes[0] : undefined; + + if (!selectedNode) { + this.activeAsset = undefined; + } else { + // fully load the asset + const assetEvent: AssetEvent = await manager.events.sendEventWithReply({ + eventType: "read-asset", + assetId: selectedNode.asset!.id + } as ReadAssetEvent); + this.activeAsset = assetEvent.asset; + } + } + + removeDatasetHighlight() { + if(this._chart){ + let options = this._chart.getOption(); + if (options.series && Array.isArray(options.series)) { + options.series.forEach(function (series) { + if (series.lineStyle.opacity == 0.2 || series.lineStyle.opacity == 0.99) { + series.lineStyle.opacity = 0.31; + } else { + series.lineStyle.opacity = 1; + } + }); + } + this._chart.setOption(options); + } + } + + addDatasetHighlight(assetId?:string, attrName?:string) { + if (this._chart) { + let options = this._chart.getOption(); + if (options.series && Array.isArray(options.series)) { + options.series.forEach(function (series) { + if (series.assetId != assetId || series.attrName != attrName) { + if (series.lineStyle.opacity == 0.31) { // 0.31 is faint setting, 1 is normal + series.lineStyle.opacity = 0.2; + } else { + series.lineStyle.opacity = 0.3; + } + } else if (series.lineStyle.opacity == 0.31) { // extra highlight if selected is faint + series.lineStyle.opacity = 0.99; + } + }); + } + this._chart.setOption(options) + } + }; + + + + + async loadSettings(reset: boolean) { + + if(this.assetAttributes == undefined || reset) { + this.assetAttributes = []; + } + + if (!this.realm) { + this.realm = manager.getRealm(); + } + + if (!this.timePrefixOptions) { + this.timePrefixOptions = this._getDefaultTimePrefixOptions(); + } + + if (!this.timeWindowOptions) { + this.timeWindowOptions = this._getDefaultTimeWindowOptions(); + } + + if (!this.timeWindowKey) { + this.timeWindowKey = this.timeWindowOptions.keys().next().value.toString(); + } + + if (!this.timePrefixKey) { + this.timePrefixKey = this.timePrefixOptions[1]; + } + + if (!this.panelName) { + return; + } + + const viewSelector = window.location.hash; + const allConfigs: OrChartConfig[] = await manager.console.retrieveData("OrChartConfig") || []; + + if (!Array.isArray(allConfigs)) { + manager.console.storeData("OrChartConfig", [allConfigs]); + } + + let config: OrChartConfig | undefined = allConfigs.find(e => e.realm === this.realm); + + if (!config) { + return; + } + + const view = config.views && config.views[viewSelector] ? config.views[viewSelector][this.panelName] : undefined; + + if (!view) { + return; + } + + if (!view.attributeRefs) { + // Old/invalid config format remove it + delete config.views[viewSelector][this.panelName]; + const cleanData = [...allConfigs.filter(e => e.realm !== this.realm), config]; + manager.console.storeData("OrChartConfig", cleanData); + return; + } + + const assetIds = view.attributeRefs.map((attrRef) => attrRef.id!); + + if (assetIds.length === 0) { + return; + } + + this._loading = true; + + if (!assetIds.every(id => !!this.assets.find(asset => asset.id === id))) { + const query = { + ids: assetIds + } as AssetQuery; + + try { + const response = await manager.rest.api.AssetResource.queryAssets(query); + const assets = response.data || []; + view.attributeRefs = view.attributeRefs.filter((attrRef) => !!assets.find((asset) => asset.id === attrRef.id && asset.attributes && asset.attributes.hasOwnProperty(attrRef.name!))); + + manager.console.storeData("OrChartConfig", [...allConfigs.filter(e => e.realm !== this.realm), config]); + this.assets = assets.filter((asset) => view.attributeRefs!.find((attrRef) => attrRef.id === asset.id)); + } catch (e) { + console.error("Failed to get assets requested in settings", e); + } + + this._loading = false; + + if (this.assets && this.assets.length > 0) { + this.assetAttributes = view.attributeRefs.map((attrRef) => { + const assetIndex = this.assets.findIndex((asset) => asset.id === attrRef.id); + const asset = assetIndex >= 0 ? this.assets[assetIndex] : undefined; + return asset && asset.attributes ? [assetIndex!, asset.attributes[attrRef.name!]] : undefined; + }).filter((indexAndAttr) => !!indexAndAttr) as [number, Attribute][]; + } + } + } + + async saveSettings() { + if (!this.panelName) { + return; + } + + const viewSelector = window.location.hash; + const allConfigs: OrChartConfig[] = await manager.console.retrieveData("OrChartConfig") || []; + let config: OrChartConfig | undefined = allConfigs.find(e => e.realm === this.realm); + + if (!config) { + config = { + realm: this.realm, + views: { + } + } + } + + if (!config.views[viewSelector]) { + config.views[viewSelector] = {}; + } + + if (!this.assets || !this.assetAttributes || this.assets.length === 0 || this.assetAttributes.length === 0) { + delete config.views[viewSelector][this.panelName]; + } else { + config.realm = this.realm; + config.views[viewSelector][this.panelName] = { + attributeRefs: this.assetAttributes.map(([index, attr]) => { + const asset = this.assets[index]; + return !!asset ? {id: asset.id, name: attr.name} as AttributeRef : undefined; + }).filter((attrRef) => !!attrRef) as AttributeRef[], + }; + } + + manager.console.storeData("OrChartConfig", [...allConfigs.filter(e => e.realm !== this.realm), config]); + } + + protected _openDialog() { + const dialog = showDialog(new OrAttributePicker() + .setShowOnlyDatapointAttrs(true) + .setMultiSelect(true) + .setSelectedAttributes(this._getSelectedAttributes())); + + dialog.addEventListener(OrAttributePickerPickedEvent.NAME, (ev: any) => this._addAttribute(ev.detail)); + } + + + protected async _addAttribute(selectedAttrs?: AttributeRef[]) { + if (!selectedAttrs) return; + + this.assetAttributes = []; + for (const attrRef of selectedAttrs) { + const response = await manager.rest.api.AssetResource.get(attrRef.id!); + this.activeAsset = response.data; + if (this.activeAsset) { + let assetIndex = this.assets.findIndex((asset) => asset.id === attrRef.id); + if (assetIndex < 0) { + assetIndex = this.assets.length; + this.assets = [...this.assets, this.activeAsset]; + } + this.assetAttributes.push([assetIndex, attrRef]); + } + } + this.assetAttributes = [...this.assetAttributes]; + this.saveSettings(); + } + + protected _getSelectedAttributes() { + return this.assetAttributes.map(([assetIndex, attr]) => { + return {id: this.assets[assetIndex].id, name: attr.name}; + }); + } + + async onCompleted() { + await this.updateComplete; + } + + protected _cleanup() { + if (this._chart) { + //('cleanup found _chart exists so disposing'); + this._toggleChartEventListeners(false); + this._chart.dispose(); + this._chart = undefined; + this.requestUpdate(); + } + } + + protected _deleteAttribute (index: number) { + const removed = this.assetAttributes.splice(index, 1)[0]; + const assetIndex = removed[0]; + this.assetAttributes = [...this.assetAttributes]; + if (!this.assetAttributes.some(([index, attrRef]) => index === assetIndex)) { + // Asset no longer referenced + this.assets.splice(index, 1); + this.assetAttributes.forEach((indexRef) => { + if (indexRef[0] >= assetIndex) { + indexRef[0] -= 1; + } + }); + } + this.saveSettings(); + } + + + + + protected _getDefaultTimePrefixOptions(): string[] { + return ["this", "last"]; + } + + + protected _getDefaultTimeWindowOptions(): Map { + return new Map([ + ["hour", ['hours', 1]], + ["6Hours", ['hours', 6]], + ["24Hours", ['hours', 24]], + ["day", ['days', 1]], + ["7Days", ['days', 7]], + ["week", ['weeks', 1]], + ["30Days", ['days', 30]], + ["month", ['months', 1]], + ["365Days", ['days', 365]], + ["year", ['years', 1]] + ]); + }; + + + protected _getTimeSelectionDates(timePrefixSelected: string, timeWindowSelected: string): [Date, Date] { + let startDate = moment(); + let endDate = moment(); + + const timeWindow: [moment.unitOfTime.DurationConstructor, number] | undefined = this.timeWindowOptions!.get(timeWindowSelected); + + if (!timeWindow) { + throw new Error(`Unsupported time window selected: ${timeWindowSelected}`); + } + + const [unit , value]: [moment.unitOfTime.DurationConstructor, number] = timeWindow; + + switch (timePrefixSelected) { + case "this": + if (value == 1) { // For singulars like this hour + startDate = moment().startOf(unit); + endDate = moment().endOf(unit); + } else { // For multiples like this 5 min, put now in the middle + startDate = moment().subtract(value*0.5, unit); + endDate = moment().add(value*0.5, unit); + } + break; + case "last": + startDate = moment().subtract(value, unit).startOf(unit); + if (value == 1) { // For singulars like last hour + endDate = moment().startOf(unit); + } else { //For multiples like last 5 min + endDate = moment(); + } + break; + } + return [startDate.toDate(), endDate.toDate()]; + } + + protected _shiftTimeframe(currentStart: Date, timeWindowSelected: string, direction: string) { + const timeWindow = this.timeWindowOptions!.get(timeWindowSelected); + + if (!timeWindow) { + throw new Error(`Unsupported time window selected: ${timeWindowSelected}`); + } + + const [unit, value] = timeWindow; + let newStart = moment(currentStart); + + direction === "previous" ? newStart.subtract(value, unit as moment.unitOfTime.DurationConstructor) : newStart.add(value, unit as moment.unitOfTime.DurationConstructor); + + let newEnd = moment(newStart).add(value, unit as moment.unitOfTime.DurationConstructor); + + this.timeframe = [newStart.toDate(), newEnd.toDate()]; + } + + + protected _getInterval(diffInHours: number): [number, DatapointInterval] { + + if(diffInHours <= 1) { + return [5, DatapointInterval.MINUTE]; + } else if(diffInHours <= 3) { + return [10, DatapointInterval.MINUTE]; + } else if(diffInHours <= 6) { + return [30, DatapointInterval.MINUTE]; + } else if(diffInHours <= 24) { // one day + return [1, DatapointInterval.HOUR]; + } else if(diffInHours <= 48) { // two days + return [3, DatapointInterval.HOUR]; + } else if(diffInHours <= 96) { + return [12, DatapointInterval.HOUR]; + } else if(diffInHours <= 744) { // one month + return [1, DatapointInterval.DAY]; + } else { + return [1, DatapointInterval.MONTH]; + } + } + + protected async _loadData() { + if ( this._data || !this.assetAttributes || !this.assets || (this.assets.length === 0 && !this.dataProvider) || (this.assetAttributes.length === 0 && !this.dataProvider) || !this.datapointQuery) { + return; + } + + if(this._loading) { + if(this._dataAbortController) { + this._dataAbortController.abort("Data request overridden"); + delete this._dataAbortController; + } else { + return; + } + } + + this._loading = true; + + const dates: [Date, Date] = this._getTimeSelectionDates(this.timePrefixKey!, this.timeWindowKey!); + + if(!this._startOfPeriod || !this._endOfPeriod) { + this._startOfPeriod = this.timeframe ? this.timeframe[0].getTime() : dates[0].getTime(); + this._endOfPeriod = this.timeframe ? this.timeframe[1].getTime() : dates[1].getTime(); + } + + const diffInHours = (this._endOfPeriod - this._startOfPeriod) / 1000 / 60 / 60; + const intervalArr = this._getInterval(diffInHours); + + const stepSize: number = intervalArr[0]; + const interval: DatapointInterval = intervalArr[1]; + + const lowerCaseInterval = interval.toLowerCase(); + this._timeUnits = lowerCaseInterval as TimeUnit; + this._stepSize = stepSize; + + const data: any = []; + let promises; + + try { + if(this.dataProvider) { + await this.dataProvider(this._startOfPeriod, this._endOfPeriod, (interval.toString() as TimeUnit), stepSize).then((dataset) => { + dataset.forEach((set) => { data.push(set); }); + }); + } else { + this._dataAbortController = new AbortController(); + promises = this.assetAttributes.map(async ([assetIndex, attribute], index) => { + + const asset = this.assets[assetIndex]; + const shownOnRightAxis = !!this.attributeSettings.rightAxisAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name); + const color = this.colorPickedAttributes.find(({ attributeRef }) => attributeRef.name === attribute.name && attributeRef.id === asset.id)?.color; + const descriptors = AssetModelUtil.getAttributeAndValueDescriptors(asset.type, attribute.name, attribute); + const label = Util.getAttributeLabel(attribute, descriptors[0], asset.type, false); + const unit = Util.resolveUnits(Util.getAttributeUnits(attribute, descriptors[0], asset.type)); + const colourIndex = index % this.colors.length; + const options = { signal: this._dataAbortController?.signal }; + //Load Historic Data + let dataset = await this._loadAttributeData(asset, attribute, color ?? this.colors[colourIndex], this._startOfPeriod!, this._endOfPeriod!, false, smooth, stepped, area, faint, false, asset.name + " " + label, options, unit); + (dataset as any).assetId = asset.id; + (dataset as any).attrName = attribute.name; + (dataset as any).unit = unit; + (dataset as any).yAxisIndex = shownOnRightAxis ? '1' : '0'; + (dataset as any).color = color ?? this.colors[colourIndex]; + data.push(dataset); + + //Load Comparison Data + // this is predicted dataset = await this._loadAttributeData(this.assets[assetIndex], attribute, color ?? this.colors[colourIndex], predictedFromTimestamp, this._endOfPeriod!, true, smooth, stepped, area, faint, false , asset.name + " " + label + " " + i18next.t("predicted"), options, unit); + //data.push(dataset); + + }); + } + + if(promises) { + await Promise.all(promises); + } + + this._data = data; + this._loading = false; + + + } catch (ex) { + console.error(ex); + if((ex as Error)?.message === "canceled") { + return; // If request has been canceled (using AbortController); return, and prevent _loading is set to false. + } + this._loading = false; + + + if(isAxiosError(ex)) { + if(ex.message.includes("timeout")) { + this._latestError = "noAttributeDataTimeout"; + return; + } else if(ex.response?.status === 413) { + this._latestError = "datapointRequestTooLarge"; + return; + } + } + this._latestError = "errorOccurred"; + } + } + + + protected async _loadAttributeData(asset: Asset, attribute: Attribute, color: string, from: number, to: number, predicted: boolean, smooth: boolean, stepped: boolean, area: boolean, faint: boolean, extended: boolean, label?: string, options?: any, unit?: any) { + + function rgba (color: string, alpha: number) { + return `rgba(${parseInt(color.slice(-6,-4), 16)}, ${parseInt(color.slice(-4,-2), 16)}, ${parseInt(color.slice(-2), 16)}, ${alpha})`; + } + + const dataset = { + name: label, + type: 'line', + showSymbol: false, + data: [] as [any, any][], + sampling: 'lttb', + lineStyle: { + color: color, + type: predicted ? [2, 4] : extended ? [0.8, 10] : undefined, + opacity: faint ? 0.31 : 1, + }, + itemStyle: { + color: color + }, + tooltip: { + // @ts-ignore + valueFormatter: value => value + unit + }, + smooth: smooth, + step: stepped ? 'end' : undefined, + areaStyle: area ? {color: new graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: rgba(color, faint ? 0.1 : 0.5) + }, + { + offset: 1, + color: rgba(color, 0) + } + ])} as any : undefined, + } + + + if (asset.id && attribute.name && this.datapointQuery) { + let response: GenericAxiosResponse[]>; + const query = JSON.parse(JSON.stringify(this.datapointQuery)); // recreating object, since the changes shouldn't apply to parent components; only or-chart itself. + + + query.fromTimestamp = this._startOfPeriod; + query.toTimestamp = this._endOfPeriod; + + + //if(query.type === 'interval' && !query.interval) { + const diffInHours = (this.datapointQuery.toTimestamp! - this.datapointQuery.fromTimestamp!) / 1000 / 60 / 60; + const intervalArr = this._getInterval(diffInHours); + query.interval = (intervalArr[0].toString() + " " + intervalArr[1].toString()); // for example: "5 minute" + + + + response = await manager.rest.api.AssetDatapointResource.getDatapoints(asset.id, attribute.name, query, options); + + + let data: ValueDatapoint[] = []; + + if (response.status === 200) { + data = response.data + .filter(value => value.y !== null && value.y !== undefined) + .map(point => ({ x: point.x, y: point.y } as ValueDatapoint)) + + dataset.data = data.map(point => [point.x, point.y]); + dataset.showSymbol = data.length <= this.showSymbolMaxDatapoints; + } + + + + } + return dataset; + } + + + protected _updateChartData(){ + this._chart!.setOption({ + xAxis: { + min: this._startOfPeriod, + max: this._endOfPeriod + }, + series: this._data!.map(series => ({ + ...series, + //markLine: { + // symbol: 'circle', + // silent: true, + // data: [{ name: '', xAxis: new Date().toISOString(), label: { formatter: '{b}' } }], + // lineStyle: { + // color: this._style.getPropertyValue("--internal-or-chart-text-color"), + // type: 'solid', + // width: 2, + // opacity: 1 + // } + //} + })) + }); + } + + protected _toggleChartEventListeners(connect: boolean){ + if (connect) { + //Connect event listeners + // Make chart size responsive + //window.addEventListener("resize", () => this._chart!.resize()); + this._containerResizeObserver = new ResizeObserver(() => this._chart!.resize()); + this._containerResizeObserver.observe(this._chartElem); + // Add event listener for chart resize + this._resizeHandler = this._chart!.on('resize', throttle(() => { this.applyChartResponsiveness(); }, 200)); + } + else if (!connect) { + //Disconnect event listeners + this._chart!.off('resize', this._resizeHandler); + this._containerResizeObserver?.disconnect(); + this._containerResizeObserver = undefined; + } + + + + + + } +} diff --git a/ui/component/or-attribute-report/tsconfig.json b/ui/component/or-attribute-report/tsconfig.json new file mode 100644 index 0000000000..4db20cbd35 --- /dev/null +++ b/ui/component/or-attribute-report/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "./lib", + "rootDir": "src", + "types": ["offscreencanvas"] + }, + "include": [ + "./src" + ], + "references": [ + { "path": "../core" }, + { "path": "../or-components" }, + { "path": "../or-icon" }, + { "path": "../or-asset-tree" }, + { "path": "../or-mwc-components" }, + { "path": "../or-attribute-picker" }, + { "path": "../or-translate" } + ] +} diff --git a/ui/component/or-attribute-report/webpack.config.js b/ui/component/or-attribute-report/webpack.config.js new file mode 100644 index 0000000000..c48ec7b800 --- /dev/null +++ b/ui/component/or-attribute-report/webpack.config.js @@ -0,0 +1,18 @@ +const util = require("@openremote/util"); + +bundles = { + "index": { + vendor: { + "chart.js": "Chart", + "jsonpath-plus": "JSONPath", + "moment": "moment" + }, + excludeOr: true + }, + "index.bundle": { + excludeOr: true, + }, + "index.orbundle": undefined +}; + +module.exports = util.generateExports(__dirname); diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts new file mode 100644 index 0000000000..814ebbf26c --- /dev/null +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -0,0 +1,496 @@ +import {css, html, TemplateResult } from "lit"; +import { customElement } from "lit/decorators.js"; +import {WidgetSettings} from "../util/widget-settings"; +import "../panels/attributes-panel"; +import "../util/settings-panel"; +import {i18next} from "@openremote/or-translate"; +import {AttributeAction, AttributeActionEvent, AttributesSelectEvent} from "../panels/attributes-panel"; +import {Asset, AssetDatapointIntervalQuery, AssetDatapointIntervalQueryFormula, Attribute, AttributeRef} from "@openremote/model"; +import {ReportWidgetConfig} from "../widgets/report-widget"; +import {InputType, OrInputChangedEvent} from "@openremote/or-mwc-components/or-mwc-input"; +import {when} from "lit/directives/when.js"; +import moment from "moment/moment"; + +const styling = css` + .switch-container { + display: flex; + align-items: center; + justify-content: space-between; + } +` + + +@customElement("report-settings") +export class ReportSettings extends WidgetSettings { + + protected readonly widgetConfig!: ReportWidgetConfig; + + + + protected timeWindowOptions: Map = new Map; + protected timePrefixOptions: string[] = []; + protected samplingOptions: Map = new Map(); + + public setTimeWindowOptions(options: Map) { + this.timeWindowOptions = options; + } + + public setTimePrefixOptions(options: string[]) { + this.timePrefixOptions = options; + } + + public setSamplingOptions(options: Map) { + this.samplingOptions = options; + } + + static get styles() { + return [...super.styles, styling]; + } + + protected render(): TemplateResult { + const attributeFilter: (attr: Attribute) => boolean = (attr): boolean => { + return ["boolean", "positiveInteger", "positiveNumber", "number", "long", "integer", "bigInteger", "negativeInteger", "negativeNumber", "bigNumber", "integerByte", "direction"].includes(attr.type!) + }; + const attrSettings = this.widgetConfig.attributeSettings; + const min = this.widgetConfig.chartOptions.options?.scales?.y?.min; + const max = this.widgetConfig.chartOptions.options?.scales?.y?.max; + const isMultiAxis = attrSettings.rightAxisAttributes.length > 0; + const samplingValue = Array.from(this.samplingOptions.entries()).find((entry => entry[1] === this.widgetConfig.datapointQuery.type))![0] + const attributeLabelCallback = (asset: Asset, attribute: Attribute, attributeLabel: string) => { + const isOnRightAxis = isMultiAxis && attrSettings.rightAxisAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; + const isFaint = attrSettings.faintAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; + const isSmooth = attrSettings.smoothAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; + const isStepped = attrSettings.steppedAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; + const isArea = attrSettings.areaAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; + const isExtended = attrSettings.extendedAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; + return html` + ${asset.name} + ${attributeLabel} + ${when(isOnRightAxis, () => html` + + `)} + ${when(isFaint, () => html` + + `)} + ${when(isSmooth, () => html` + + `)} + ${when(isStepped, () => html` + + `)} + ${when(isArea, () => html` + + `)} + ${when(isExtended, () => html` + + `)} + ` + } + + + const attributeActionCallback = (attributeRef: AttributeRef): AttributeAction[] => { + return [ + { + icon: 'palette', + tooltip: i18next.t('dashboard.lineColor'), + disabled: false + }, + { + icon: 'chart-bell-curve-cumulative', + tooltip: i18next.t("dashboard.smooth"), + disabled: false + }, + { + icon: 'square-wave', + tooltip: i18next.t('dashboard.stepped'), + disabled: false + }, + { + icon: 'chart-areaspline-variant', + tooltip: i18next.t('dashboard.fill'), + disabled: false + }, + { + icon: 'arrange-send-backward', + tooltip: i18next.t('dashboard.faint'), + disabled: false + }, + { + icon: 'arrow-expand-right', + tooltip: i18next.t('dashboard.extendData'), + disabled: false + }, + + { + icon: this.widgetConfig.attributeSettings.rightAxisAttributes.includes(attributeRef) ? "arrow-right-bold" : "arrow-left-bold", + tooltip: i18next.t('dashboard.toggleAxis'), + disabled: false + }, + { + icon: 'mdi-blank', + tooltip: '', + disabled: true + } + ] + } + return html` +
+ + + + + + + +
+ +
+ + +
+ +
+
+ + +
+
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + + +
+ + +
+ ${when(isMultiAxis, () => html` +
+ +
+ `)} +
+ ${max !== undefined ? html` + + ` : html` + + `} + +
+
+ ${min !== undefined ? html` + + ` : html` + + `} + +
+
+ + + ${when(isMultiAxis, () => { + const rightMin = this.widgetConfig.chartOptions.options?.scales?.y1?.min; + const rightMax = this.widgetConfig.chartOptions.options?.scales?.y1?.max; + return html` +
+
+ +
+
+ ${rightMax !== undefined ? html` + + ` : html` + + `} + +
+
+ ${rightMin !== undefined ? html` + + ` : html` + + `} + +
+
+ ` + })} +
+
+ + + +
+
+ +
+
+ ${this.getSamplingOptionsTemplate(this.widgetConfig.datapointQuery.type)} +
+
+
+
+ `; + } + + protected getSamplingOptionsTemplate(type: any): TemplateResult { + switch (type) { + case 'interval': { + const intervalQuery = this.widgetConfig.datapointQuery as AssetDatapointIntervalQuery; + const formulaOptions = [AssetDatapointIntervalQueryFormula.AVG, AssetDatapointIntervalQueryFormula.MIN, AssetDatapointIntervalQueryFormula.MAX, AssetDatapointIntervalQueryFormula.DELTA]; + return html` + + `; + } + case 'lttb': { + return html ` + + `; + } + default: + return html``; + } + } + + // Check which icon was pressed and act accordingly. + protected onAttributeAction(ev: AttributeActionEvent) { + const { asset ,attributeRef, action } = ev.detail; + + const findAttributeIndex = (array: AttributeRef[], ref: AttributeRef) => { + return array.findIndex(item => item.id === ref.id && item.name === ref.name); + }; + + switch (action.icon) { + case "palette": // Change color + const colorInput = document.createElement('input'); + colorInput.type = 'color'; + colorInput.style.border = 'none'; + colorInput.style.height = '31px'; + colorInput.style.width = '31px'; + colorInput.style.padding = '1px 3px'; + colorInput.style.minHeight = '22px'; + colorInput.style.minWidth = '30px'; + colorInput.style.cursor = 'pointer'; + colorInput.addEventListener('change', (e: any) => { + const color = e.target.value; + const existingIndex = this.widgetConfig.colorPickedAttributes.findIndex(item => + item.attributeRef.id === attributeRef.id && item.attributeRef.name === attributeRef.name + ); + if (existingIndex >= 0) { + this.widgetConfig.colorPickedAttributes[existingIndex].color = color; + } else { + this.widgetConfig.colorPickedAttributes.push({ attributeRef, color }); + } + this.notifyConfigUpdate(); + }); + colorInput.click(); + break; + case "arrow-right-bold": + case "arrow-left-bold": + this.toggleAttributeSetting("rightAxisAttributes", attributeRef); + break; + case "chart-bell-curve-cumulative": + this.toggleAttributeSetting("smoothAttributes", attributeRef); + break; + case "square-wave": + this.toggleAttributeSetting("steppedAttributes", attributeRef); + break; + case "chart-areaspline-variant": + this.toggleAttributeSetting("areaAttributes", attributeRef); + break; + case "arrange-send-backward": + this.toggleAttributeSetting("faintAttributes", attributeRef); + break; + case "arrow-expand-right": + this.toggleAttributeSetting("extendedAttributes", attributeRef); + break; + default: + console.warn('Unknown attribute panel action:', action); + } + console.log("end of onAttributeAction" + JSON.stringify(this.widgetConfig.attributeSettings)); + } + + // When the list of attributeRefs is changed by the asset selector, + // we should remove the settings references for the attributes that got removed. + // Also update the WidgetConfig attributeRefs field as usual + protected onAttributesSelect(ev: AttributesSelectEvent) { + const removedAttributeRefs = this.widgetConfig.attributeRefs.filter(ar => !ev.detail.attributeRefs.includes(ar)); + + removedAttributeRefs.forEach(raf => { + this.removeFromAttributeSettings(raf); + this.removeFromColorPickedAttributes(raf); + }); + + this.widgetConfig.attributeRefs = ev.detail.attributeRefs; + this.notifyConfigUpdate(); + } + + protected removeFromAttributeSettings(attributeRef: AttributeRef) { + const settings = this.widgetConfig.attributeSettings; + (Object.keys(settings) as (keyof typeof settings)[]).forEach(key => { + settings[key] = settings[key].filter((ar: AttributeRef) => ar.id !== attributeRef.id || ar.name !== attributeRef.name); + }); + } + + protected toggleAttributeSetting( + setting: keyof ReportWidgetConfig["attributeSettings"], + attributeRef: AttributeRef, + ): void { + const attributes = this.widgetConfig.attributeSettings[setting]; + const index = attributes.findIndex( + (item: AttributeRef) => item.id === attributeRef.id && item.name === attributeRef.name + ); + if (index < 0) { + attributes.push(attributeRef); + } else { + attributes.splice(index, 1); + } + this.notifyConfigUpdate(); + } + + protected removeFromColorPickedAttributes(attributeRef: AttributeRef) { + this.widgetConfig.colorPickedAttributes = this.widgetConfig.colorPickedAttributes.filter( + item => item.attributeRef.id !== attributeRef.id || item.attributeRef.name !== attributeRef.name + ); + } + + protected onTimePreFixSelect(ev: OrInputChangedEvent) { + this.widgetConfig.defaultTimePrefixKey = ev.detail.value.toString(); + this.notifyConfigUpdate(); + } + + protected onTimeWindowSelect(ev: OrInputChangedEvent) { + this.widgetConfig.defaultTimeWindowKey = ev.detail.value.toString(); + this.notifyConfigUpdate(); + } + + protected onTimestampControlsToggle(ev: OrInputChangedEvent) { + this.widgetConfig.showTimestampControls = !ev.detail.value; + this.notifyConfigUpdate(); + } + + protected onShowLegendToggle(ev: OrInputChangedEvent) { + this.widgetConfig.showLegend = ev.detail.value; + this.notifyConfigUpdate(); + } + + protected onShowZoomBarToggle(ev: OrInputChangedEvent) { + this.widgetConfig.showZoomBar = ev.detail.value; + this.notifyConfigUpdate(); + } + + protected onShowToolBoxToggle(ev: OrInputChangedEvent) { + this.widgetConfig.showToolBox = ev.detail.value; + this.notifyConfigUpdate(); + } + + protected setAxisMinMaxValue(axis: 'left' | 'right', type: 'min' | 'max', value?: number) { + if(axis === 'left') { + if(type === 'min') { + this.widgetConfig.chartOptions.options.scales.y.min = value; + } else { + this.widgetConfig.chartOptions.options.scales.y.max = value; + } + } else { + if(type === 'min') { + this.widgetConfig.chartOptions.options.scales.y1.min = value; + } else { + this.widgetConfig.chartOptions.options.scales.y1.max = value; + } + } + this.notifyConfigUpdate(); + } + + protected onMinMaxValueChange(axis: 'left' | 'right', type: 'min' | 'max', ev: OrInputChangedEvent) { + this.setAxisMinMaxValue(axis, type, ev.detail.value); + } + + protected onMinMaxValueToggle(axis: 'left' | 'right', type: 'min' | 'max', ev: OrInputChangedEvent) { + this.setAxisMinMaxValue(axis, type, (ev.detail.value ? (type === 'min' ? 0 : 100) : undefined)); + } + + protected onSamplingQueryChange(ev: OrInputChangedEvent) { + this.widgetConfig.datapointQuery.type = this.samplingOptions.get(ev.detail.value)! as any; + this.notifyConfigUpdate(); + } + + protected onMaxConcurrentDatapointsValueChange(ev: OrInputChangedEvent) { + this.widgetConfig.maxConcurrentDatapoints = ev.detail.value; + this.notifyConfigUpdate(); + } + + protected onShowSymbolMaxDatapointsValueChange(ev: OrInputChangedEvent) { + this.widgetConfig.showSymbolMaxDatapoints = ev.detail.value; + this.notifyConfigUpdate(); + } +} diff --git a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts new file mode 100644 index 0000000000..ac0e8e2851 --- /dev/null +++ b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts @@ -0,0 +1,251 @@ +import {AssetDatapointLTTBQuery, AssetDatapointQueryUnion, Attribute, AttributeRef} from "@openremote/model"; +import {html, PropertyValues, TemplateResult } from "lit"; +import { when } from "lit/directives/when.js"; +import moment from "moment"; +import {OrAssetWidget} from "../util/or-asset-widget"; +import { customElement, state } from "lit/decorators.js"; +import {WidgetConfig} from "../util/widget-config"; +import {OrWidget, WidgetManifest} from "../util/or-widget"; +import {ReportSettings} from "../settings/report-settings"; +import {WidgetSettings} from "../util/widget-settings"; +import "@openremote/or-attribute-report"; + +export interface ReportWidgetConfig extends WidgetConfig { + attributeRefs: AttributeRef[]; + colorPickedAttributes: Array<{ attributeRef: AttributeRef; color: string }>; + attributeSettings: { + rightAxisAttributes: AttributeRef[], + smoothAttributes: AttributeRef[], + steppedAttributes: AttributeRef[], + areaAttributes: AttributeRef[], + faintAttributes: AttributeRef[], + extendedAttributes: AttributeRef[], + }, + datapointQuery: AssetDatapointQueryUnion; + chartOptions?: any; + showTimestampControls: boolean; + defaultTimeWindowKey: string; + defaultTimePrefixKey: string; + showLegend: boolean; + showZoomBar: boolean; + showToolBox: boolean; + showSymbolMaxDatapoints: number; + maxConcurrentDatapoints: number; +} + +function getDefaultTimeWindowOptions(): Map { + return new Map([ + ["5Minutes", ['minutes', 5]], + ["20Minutes", ['minutes', 20]], + ["60Minutes", ['minutes', 60]], + ["hour", ['hours', 1]], + ["6Hours", ['hours', 6]], + ["24Hours", ['hours', 24]], + ["day", ['days', 1]], + ["7Days", ['days', 7]], + ["week", ['weeks', 1]], + ["30Days", ['days', 30]], + ["month", ['months', 1]], + ["365Days", ['days', 365]], + ["year", ['years', 1]] + ]); +} + +function getDefaultTimePreFixOptions(): string[] { + return ["this", "last"]; +} + +function getDefaultSamplingOptions(): Map { + return new Map([["lttb", 'lttb'], ["withInterval", 'interval']]); +} + +function getDefaultWidgetConfig(): ReportWidgetConfig { + const preset = "24Hours"; // Default time preset, "last" prefix is hardcoded in startDate and endDate below. + const dateFunc = getDefaultTimeWindowOptions().get(preset); + const startDate = moment().subtract(dateFunc![1], dateFunc![0]).startOf(dateFunc![0]); + const endDate = dateFunc![1]== 1 ? moment().endOf(dateFunc![0]) : moment(); + return { + attributeRefs: [], + colorPickedAttributes: [], + attributeSettings: { + rightAxisAttributes: [], + smoothAttributes: [], + steppedAttributes: [], + areaAttributes: [], + faintAttributes: [], + extendedAttributes: [], + }, + datapointQuery: { + type: "lttb", + fromTimestamp: startDate.toDate().getTime(), + toTimestamp: endDate.toDate().getTime(), + }, + chartOptions: { + options: { + scales: { + y: { + min: undefined, + max: undefined + }, + y1: { + min: undefined, + max: undefined + } + } + }, + }, + showTimestampControls: false, + defaultTimeWindowKey: preset, + defaultTimePrefixKey: "last", + showLegend: true, + showZoomBar: false, + showToolBox: false, + showSymbolMaxDatapoints: 30, + maxConcurrentDatapoints: 100 + + }; +} + +/* --------------------------------------------------- */ + +@customElement('report-widget') +export class ReportWidget extends OrAssetWidget { + + @state() + protected datapointQuery!: AssetDatapointQueryUnion; + + @state() + protected _loading = false; + + // Override of widgetConfig with extended type + protected widgetConfig!: ReportWidgetConfig; + + static getManifest(): WidgetManifest { + return { + displayName: "Line Chart", + displayIcon: "chart-line", + minColumnWidth: 2, + minColumnHeight: 2, + getContentHtml(config: ReportWidgetConfig): OrWidget { + return new ReportWidget(config); + }, + getSettingsHtml(config: ReportWidgetConfig): WidgetSettings { + const settings = new ReportSettings(config); + settings.setTimeWindowOptions(getDefaultTimeWindowOptions()); + settings.setTimePrefixOptions(getDefaultTimePreFixOptions()); + settings.setSamplingOptions(getDefaultSamplingOptions()); + return settings; + }, + getDefaultConfig(): ReportWidgetConfig { + return getDefaultWidgetConfig(); + } + } + } + + // Method called on every refresh/reload of the widget + // We either refresh the datapointQuery or the full widgetConfig depending on the force parameter. + // TODO: Improve this to a more efficient approach, instead of duplicating the object + public refreshContent(force: boolean) { + if(!force) { + const datapointQuery = JSON.parse(JSON.stringify(this.widgetConfig.datapointQuery)) as AssetDatapointQueryUnion; + datapointQuery.fromTimestamp = undefined; + datapointQuery.toTimestamp = undefined; + this.datapointQuery = datapointQuery; + } else { + this.widgetConfig = JSON.parse(JSON.stringify(this.widgetConfig)) as ReportWidgetConfig; + } + } + + + /* ---------------------------------- */ + + // WebComponent lifecycle method, that occurs DURING every state update + protected willUpdate(changedProps: PropertyValues) { + + // Add datapointQuery if not set yet (due to migration) + if(!this.widgetConfig.datapointQuery) { + this.widgetConfig.datapointQuery = this.getDefaultQuery(); + if(!changedProps.has("widgetConfig")) { + changedProps.set("widgetConfig", this.widgetConfig); + } + } + + if(changedProps.has('widgetConfig') && this.widgetConfig) { + this.datapointQuery = this.widgetConfig.datapointQuery; + + const attributeRefs = this.widgetConfig.attributeRefs; + if(attributeRefs.length === 0) { + this._error = "noAttributesConnected"; + } else { + const missingAssets = attributeRefs?.filter((attrRef: AttributeRef) => !this.isAttributeRefLoaded(attrRef)); + if (missingAssets.length > 0) { + this.loadAssets(attributeRefs); + } + } + } + + return super.willUpdate(changedProps); + } + + protected loadAssets(attributeRefs: AttributeRef[]): void { + if(attributeRefs.length === 0) { + this._error = "noAttributesConnected"; + return; + } + this._loading = true; + this._error = undefined; + this.fetchAssets(attributeRefs).then((assets) => { + this.loadedAssets = assets; + this.assetAttributes = attributeRefs?.map((attrRef: AttributeRef) => { + const assetIndex = assets.findIndex((asset) => asset.id === attrRef.id); + const foundAsset = assetIndex >= 0 ? assets[assetIndex] : undefined; + return foundAsset && foundAsset.attributes ? [assetIndex, foundAsset.attributes[attrRef.name!]] : undefined; + }).filter((indexAndAttr: any) => !!indexAndAttr) as [number, Attribute][]; + }).catch(e => { + this._error = e.message; + }).finally(() => { + this._loading = false; + }); + } + + protected render(): TemplateResult { + return html` + ${when(this._loading, () => html` + + + `, () => when(this._error, () => html` +
+ +
+ + `, () => { + return html` + + `; + }))} + `; + } + + protected getDefaultQuery(): AssetDatapointLTTBQuery { + return { + type: "lttb", + fromTimestamp: moment().set('minute', -60).toDate().getTime(), + toTimestamp: moment().set('minute', 60).toDate().getTime() + } + } +} From 4532b74ff0d6ce5ea0dc17d1618c0e7bcb1b3ca5 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Thu, 20 Mar 2025 15:55:23 +0100 Subject: [PATCH 02/15] very very wip --- ui/component/or-attribute-report/package.json | 4 +- ui/component/or-attribute-report/src/index.ts | 136 +++++++++--------- .../or-dashboard-builder/src/index.ts | 2 + .../src/panels/attributes-panel.ts | 28 ++-- 4 files changed, 91 insertions(+), 79 deletions(-) diff --git a/ui/component/or-attribute-report/package.json b/ui/component/or-attribute-report/package.json index 1de79028b9..3c5e8fb9f2 100644 --- a/ui/component/or-attribute-report/package.json +++ b/ui/component/or-attribute-report/package.json @@ -1,7 +1,7 @@ { - "name": "@openremote/or-chart", + "name": "@openremote/or-attribute-report", "version": "1.3.3", - "description": "OpenRemote chart", + "description": "OpenRemote Attribute Report", "main": "dist/umd/index.bundle.js", "module": "lib/index.js", "exports": { diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index db1d66662d..74c44e8e4b 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -582,7 +582,7 @@ export class OrChart extends translate(i18next)(LitElement) { axisLine: { lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")}}, boundaryGap: ['10%', '10%'], scale: true, - min: this.chartOptions.options.scales.y.min ? this.chartOptions.options.scales.y.min : undefined, //NOG FIXEN MET MERGEN VAN CHARTOPTIONS + min: this.chartOptions.options.scales.y.min ? this.chartOptions.options.scales.y.min : undefined, max: this.chartOptions.options.scales.y.max ? this.chartOptions.options.scales.y.max : undefined }, { @@ -606,36 +606,36 @@ export class OrChart extends translate(i18next)(LitElement) { }; // Add dataZoom bar if enabled - (this._chartOptions!.dataZoom! as any[]).push({ - start: 0, - end: 100, - backgroundColor: bgColor, - fillerColor: bgColor, - dataBackground: { - areaStyle: { - color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") - } - }, - selectedDataBackground: { - areaStyle: { - color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color"), - } - }, - moveHandleStyle: { - color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") - }, - emphasis: { - moveHandleStyle: { - color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") - }, - handleLabel: { - show: false - } - }, - handleLabel: { - show: false - } - }) + //(this._chartOptions!.dataZoom! as any[]).push({ + // start: 0, + // end: 100, + // backgroundColor: bgColor, + // fillerColor: bgColor, + // dataBackground: { + // areaStyle: { + // color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") + // } + // }, + // selectedDataBackground: { + // areaStyle: { + // color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color"), + // } + // }, + // moveHandleStyle: { + // color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") + // }, + // emphasis: { + // moveHandleStyle: { + // color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") + // }, + // handleLabel: { + // show: false + // } + // }, + // handleLabel: { + // show: false + // } + // }) // Add toolbox if enabled @@ -1150,24 +1150,27 @@ export class OrChart extends translate(i18next)(LitElement) { } - protected _getInterval(diffInHours: number): [number, DatapointInterval] { + protected _getInterval(diffInHours: number): [number, DatapointInterval, string] { + //Returns amount of steps, interval size and moment.js time format + + //This must be reworked for my own use case if(diffInHours <= 1) { - return [5, DatapointInterval.MINUTE]; + return [5, DatapointInterval.MINUTE,"h:mm:ss"]; } else if(diffInHours <= 3) { - return [10, DatapointInterval.MINUTE]; + return [10, DatapointInterval.MINUTE,"h:mm:ss"]; } else if(diffInHours <= 6) { - return [30, DatapointInterval.MINUTE]; + return [30, DatapointInterval.MINUTE,"h:mm:ss"]; } else if(diffInHours <= 24) { // one day - return [1, DatapointInterval.HOUR]; + return [1, DatapointInterval.HOUR,"h:mm"]; } else if(diffInHours <= 48) { // two days - return [3, DatapointInterval.HOUR]; + return [3, DatapointInterval.HOUR,"h:mm"]; } else if(diffInHours <= 96) { - return [12, DatapointInterval.HOUR]; + return [12, DatapointInterval.HOUR,"LT"]; } else if(diffInHours <= 744) { // one month - return [1, DatapointInterval.DAY]; + return [1, DatapointInterval.DAY,"Do"]; } else { - return [1, DatapointInterval.MONTH]; + return [1, DatapointInterval.MONTH,"MMM"]; } } @@ -1278,15 +1281,15 @@ export class OrChart extends translate(i18next)(LitElement) { const dataset = { name: label, - type: 'line', - showSymbol: false, + type: 'bar', + //showSymbol: false, data: [] as [any, any][], - sampling: 'lttb', - lineStyle: { - color: color, - type: predicted ? [2, 4] : extended ? [0.8, 10] : undefined, - opacity: faint ? 0.31 : 1, - }, + //sampling: 'lttb', + //lineStyle: { + // color: color, + // type: predicted ? [2, 4] : extended ? [0.8, 10] : undefined, + // opacity: faint ? 0.31 : 1, + //}, itemStyle: { color: color }, @@ -1294,18 +1297,18 @@ export class OrChart extends translate(i18next)(LitElement) { // @ts-ignore valueFormatter: value => value + unit }, - smooth: smooth, - step: stepped ? 'end' : undefined, - areaStyle: area ? {color: new graphic.LinearGradient(0, 0, 0, 1, [ - { - offset: 0, - color: rgba(color, faint ? 0.1 : 0.5) - }, - { - offset: 1, - color: rgba(color, 0) - } - ])} as any : undefined, + //smooth: smooth, + //step: stepped ? 'end' : undefined, + //areaStyle: area ? {color: new graphic.LinearGradient(0, 0, 0, 1, [ + // { + // offset: 0, + // color: rgba(color, faint ? 0.1 : 0.5) + // }, + // { + // offset: 1, + // color: rgba(color, 0) + // } + // ])} as any : undefined, } @@ -1333,7 +1336,9 @@ export class OrChart extends translate(i18next)(LitElement) { if (response.status === 200) { data = response.data .filter(value => value.y !== null && value.y !== undefined) - .map(point => ({ x: point.x, y: point.y } as ValueDatapoint)) + .map(point => ({ x: moment(point.x).format(intervalArr[2]), y: point.y } as ValueDatapoint)) + + dataset.data = data.map(point => [point.x, point.y]); dataset.showSymbol = data.length <= this.showSymbolMaxDatapoints; @@ -1342,16 +1347,17 @@ export class OrChart extends translate(i18next)(LitElement) { } + console.log("dataset", dataset); return dataset; } protected _updateChartData(){ this._chart!.setOption({ - xAxis: { - min: this._startOfPeriod, - max: this._endOfPeriod - }, + //xAxis: { + // min: this._startOfPeriod, + // max: this._endOfPeriod + //}, series: this._data!.map(series => ({ ...series, //markLine: { diff --git a/ui/component/or-dashboard-builder/src/index.ts b/ui/component/or-dashboard-builder/src/index.ts index d7a7c14fb0..3b6970c577 100644 --- a/ui/component/or-dashboard-builder/src/index.ts +++ b/ui/component/or-dashboard-builder/src/index.ts @@ -31,6 +31,7 @@ import {MapWidget} from "./widgets/map-widget"; import {AttributeInputWidget} from "./widgets/attribute-input-widget"; import {TableWidget} from "./widgets/table-widget"; import {GatewayWidget} from "./widgets/gateway-widget"; +import {ReportWidget} from "./widgets/report-widget"; // language=CSS const styling = css` @@ -218,6 +219,7 @@ export function registerWidgetTypes() { widgetTypes.set("attributeinput", AttributeInputWidget.getManifest()); widgetTypes.set("table", TableWidget.getManifest()); widgetTypes.set("gateway", GatewayWidget.getManifest()); + widgetTypes.set("report", ReportWidget.getManifest()); } @customElement("or-dashboard-builder") diff --git a/ui/component/or-dashboard-builder/src/panels/attributes-panel.ts b/ui/component/or-dashboard-builder/src/panels/attributes-panel.ts index c635c3a0ab..86d5eb6381 100644 --- a/ui/component/or-dashboard-builder/src/panels/attributes-panel.ts +++ b/ui/component/or-dashboard-builder/src/panels/attributes-panel.ts @@ -55,7 +55,7 @@ export class AttributesSelectEvent extends CustomEvent<{ assets: Asset[], attrib const styling = css` #attribute-list { - overflow: auto; + overflow: visible; flex: 1 1 0; width: 100%; display: flex; @@ -66,7 +66,7 @@ const styling = css` position: relative; cursor: pointer; display: flex; - flex-direction: row; + flex-direction: column; align-items: stretch; gap: 10px; padding: 0; @@ -91,8 +91,9 @@ const styling = css` flex: 1; justify-content: end; align-items: center; - display: flex; + display: none; gap: 8px; + margin-bottom: 20px; } .attribute-list-item-bullet { @@ -124,6 +125,7 @@ const styling = css` .attribute-list-item:hover .attribute-list-item-actions { background: white; z-index: 1; + display: flex; } .attribute-list-item:hover .button-action { @@ -252,17 +254,19 @@ export class AttributesPanel extends LitElement { const label = Util.getAttributeLabel(attribute, descriptors[0], asset.type, true); return html`
-
- ${getAssetDescriptorIconTemplate(AssetModelUtil.getAssetDescriptor(asset.type))} -
-
- ${when(!!this.attributeLabelCallback, +
+
+ ${getAssetDescriptorIconTemplate(AssetModelUtil.getAssetDescriptor(asset.type))} +
+
+ ${when(!!this.attributeLabelCallback, () => this.attributeLabelCallback!(asset, attribute, label), () => html` - ${asset.name} - ${label} - ` - )} + ${asset.name} + ${label} + ` + ) } +
From 86fb25230acd1fe8eb48cc156494813ba60aa79f Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Thu, 20 Mar 2025 17:05:56 +0100 Subject: [PATCH 03/15] very wip --- CONTRIBUTORS.txt | 1 + package.json | 3 ++ ui/component/or-attribute-report/README.md | 2 +- ui/component/or-attribute-report/package.json | 2 +- ui/component/or-attribute-report/src/index.ts | 36 +++++++++---------- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 9d14f51feb..4293eb4c5c 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -32,3 +32,4 @@ Mihaela Aleksandrova Kevin Lin Sam Sulaimanov Igor Melnyk +Wouter Voorberg diff --git a/package.json b/package.json index 3d60ac7f7a..faedca3cde 100644 --- a/package.json +++ b/package.json @@ -10,5 +10,8 @@ ], "devDependencies": { "@openremote/util": "workspace:*" + }, + "dependencies": { + "@openremote/or-attribute-report": "workspace:^" } } diff --git a/ui/component/or-attribute-report/README.md b/ui/component/or-attribute-report/README.md index 41d230087e..f8113b818a 100644 --- a/ui/component/or-attribute-report/README.md +++ b/ui/component/or-attribute-report/README.md @@ -3,7 +3,7 @@ [![Linux Build][travis-image]][travis-url] [![Test Coverage][coveralls-image]][coveralls-url] -Web Component for displaying chart values. +Web Component for displaying interval value reports. ## Install ```bash diff --git a/ui/component/or-attribute-report/package.json b/ui/component/or-attribute-report/package.json index 3c5e8fb9f2..e34983ece2 100644 --- a/ui/component/or-attribute-report/package.json +++ b/ui/component/or-attribute-report/package.json @@ -1,7 +1,7 @@ { "name": "@openremote/or-attribute-report", "version": "1.3.3", - "description": "OpenRemote Attribute Report", + "description": "OpenRemote attribute report", "main": "dist/umd/index.bundle.js", "module": "lib/index.js", "exports": { diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 74c44e8e4b..54c8ba7a12 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -42,12 +42,12 @@ import {ListItem} from "@openremote/or-mwc-components/or-mwc-list"; import { when } from "lit/directives/when.js"; import {createRef, Ref, ref } from "lit/directives/ref.js"; -export class OrChartEvent extends CustomEvent { +export class OrAttributeReportEvent extends CustomEvent { - public static readonly NAME = "or-chart-event"; + public static readonly NAME = "or-report-event"; constructor(value?: any, previousValue?: any) { - super(OrChartEvent.NAME, { + super(OrAttributeReportEvent.NAME, { detail: { value: value, previousValue: previousValue @@ -58,7 +58,7 @@ export class OrChartEvent extends CustomEvent { } } -export interface ChartViewConfig { +export interface AttributeReportViewConfig { attributeRefs?: AttributeRef[]; fromTimestamp?: number; toTimestamp?: number; @@ -68,27 +68,27 @@ export interface ChartViewConfig { decimals?: number; } -export interface OrChartEventDetail { +export interface OrAttributeReportEventDetail { value?: any; previousValue?: any; } declare global { export interface HTMLElementEventMap { - [OrChartEvent.NAME]: OrChartEvent; + [OrAttributeReportEvent.NAME]: OrAttributeReportEvent; } } -export interface ChartConfig { +export interface AttributeReportConfig { xLabel?: string; yLabel?: string; } -export interface OrChartConfig { - chart?: ChartConfig; +export interface OrAttributeReportConfig { + report?: AttributeReportConfig; realm?: string; views: {[name: string]: { - [panelName: string]: ChartViewConfig + [panelName: string]: AttributeReportViewConfig }}; } @@ -334,7 +334,7 @@ const style = css` `; @customElement("or-attribute-report") -export class OrChart extends translate(i18next)(LitElement) { +export class OrAttributeReport extends translate(i18next)(LitElement) { public static DEFAULT_TIMESTAMP_FORMAT = "L HH:mm:ss"; @@ -389,7 +389,7 @@ export class OrChart extends translate(i18next)(LitElement) { public readonly datapointQuery!: AssetDatapointQueryUnion; @property({type: Object}) - public config?: OrChartConfig; + public config?: OrAttributeReportConfig; @property({type: Object}) public chartOptions?: any @@ -666,7 +666,7 @@ export class OrChart extends translate(i18next)(LitElement) { } this.onCompleted().then(() => { - this.dispatchEvent(new OrChartEvent('rendered')); + this.dispatchEvent(new OrAttributeReportEvent('rendered')); }); } @@ -910,13 +910,13 @@ export class OrChart extends translate(i18next)(LitElement) { } const viewSelector = window.location.hash; - const allConfigs: OrChartConfig[] = await manager.console.retrieveData("OrChartConfig") || []; + const allConfigs: OrAttributeReportConfig[] = await manager.console.retrieveData("OrChartConfig") || []; if (!Array.isArray(allConfigs)) { manager.console.storeData("OrChartConfig", [allConfigs]); } - let config: OrChartConfig | undefined = allConfigs.find(e => e.realm === this.realm); + let config: OrAttributeReportConfig | undefined = allConfigs.find(e => e.realm === this.realm); if (!config) { return; @@ -978,8 +978,8 @@ export class OrChart extends translate(i18next)(LitElement) { } const viewSelector = window.location.hash; - const allConfigs: OrChartConfig[] = await manager.console.retrieveData("OrChartConfig") || []; - let config: OrChartConfig | undefined = allConfigs.find(e => e.realm === this.realm); + const allConfigs: OrAttributeReportConfig[] = await manager.console.retrieveData("OrChartConfig") || []; + let config: OrAttributeReportConfig | undefined = allConfigs.find(e => e.realm === this.realm); if (!config) { config = { @@ -1341,7 +1341,7 @@ export class OrChart extends translate(i18next)(LitElement) { dataset.data = data.map(point => [point.x, point.y]); - dataset.showSymbol = data.length <= this.showSymbolMaxDatapoints; + //dataset.showSymbol = data.length <= this.showSymbolMaxDatapoints; } From 808bc3112d6ba01b0843eca07f522b2864e8a08b Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Fri, 21 Mar 2025 10:54:52 +0100 Subject: [PATCH 04/15] compiling --- ui/component/or-app/tsconfig.json | 1 + ui/component/or-attribute-report/README.md | 6 +-- ui/component/or-attribute-report/src/index.ts | 30 +++++------ .../or-dashboard-builder/package.json | 1 + .../src/widgets/report-widget.ts | 8 +-- yarn.lock | 52 +++++++++++++++++++ 6 files changed, 73 insertions(+), 25 deletions(-) diff --git a/ui/component/or-app/tsconfig.json b/ui/component/or-app/tsconfig.json index 99e2258fe7..d4a7e7bbbf 100644 --- a/ui/component/or-app/tsconfig.json +++ b/ui/component/or-app/tsconfig.json @@ -15,6 +15,7 @@ { "path": "../../component/or-map" }, { "path": "../../component/or-attribute-input" }, { "path": "../../component/or-attribute-picker" }, + { "path": "../../component/or-attribute-report" }, { "path": "../../component/or-asset-tree" }, { "path": "../../component/or-asset-viewer" }, { "path": "../../component/or-data-viewer" }, diff --git a/ui/component/or-attribute-report/README.md b/ui/component/or-attribute-report/README.md index f8113b818a..9d96a420f2 100644 --- a/ui/component/or-attribute-report/README.md +++ b/ui/component/or-attribute-report/README.md @@ -1,9 +1,9 @@ -# @openremote/or-attribute-history +# @openremote/or-attribute-report \ [![NPM Version][npm-image]][npm-url] [![Linux Build][travis-image]][travis-url] [![Test Coverage][coveralls-image]][coveralls-url] -Web Component for displaying interval value reports. +Web Component for showing calculated values at intervals of an attribute. ## Install ```bash @@ -24,7 +24,7 @@ Internet Explorer 11 is also supported. [GNU AGPL](https://www.gnu.org/licenses/agpl-3.0.en.html) [npm-image]: https://img.shields.io/npm/v/live-xxx.svg -[npm-url]: https://npmjs.org/package/@openremote/or-chart +[npm-url]: https://npmjs.org/package/@openremote/or-attribute-report [travis-image]: https://img.shields.io/travis/live-js/live-xxx/master.svg [travis-url]: https://travis-ci.org/live-js/live-xxx [coveralls-image]: https://img.shields.io/coveralls/live-js/live-xxx/master.svg diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 54c8ba7a12..77a269e54e 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -4,8 +4,8 @@ import i18next from "i18next"; import {translate} from "@openremote/or-translate"; import { Asset, - AssetDatapointQueryUnion, AssetDatapointIntervalQueryFormula, + AssetDatapointQueryUnion, AssetEvent, AssetModelUtil, AssetQuery, @@ -13,34 +13,28 @@ import { AttributeRef, DatapointInterval, ReadAssetEvent, - ValueDatapoint, - WellknownMetaItems + ValueDatapoint } from "@openremote/model"; import manager, {DefaultColor2, DefaultColor3, DefaultColor4, DefaultColor5, Util} from "@openremote/core"; import "@openremote/or-asset-tree"; import "@openremote/or-mwc-components/or-mwc-input"; import "@openremote/or-components/or-panel"; import "@openremote/or-translate"; -import {ECharts, EChartsOption, init, graphic} from "echarts"; -import { - - TimeUnit, - -} from "chart.js"; -import {InputType, OrMwcInput} from "@openremote/or-mwc-components/or-mwc-input"; +import {ECharts, EChartsOption, init} from "echarts"; +import {TimeUnit,} from "chart.js"; +import {InputType} from "@openremote/or-mwc-components/or-mwc-input"; import "@openremote/or-components/or-loading-indicator"; import moment from "moment"; import {OrAssetTreeSelectionEvent} from "@openremote/or-asset-tree"; import {getAssetDescriptorIconTemplate} from "@openremote/or-icon"; import {GenericAxiosResponse, isAxiosError} from "@openremote/rest"; import {OrAttributePicker, OrAttributePickerPickedEvent} from "@openremote/or-attribute-picker"; -import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; +import {showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; import {cache} from "lit/directives/cache.js"; -import {debounce, throttle} from "lodash"; +import {throttle} from "lodash"; import {getContentWithMenuTemplate} from "@openremote/or-mwc-components/or-mwc-menu"; import {ListItem} from "@openremote/or-mwc-components/or-mwc-list"; -import { when } from "lit/directives/when.js"; -import {createRef, Ref, ref } from "lit/directives/ref.js"; +import {when} from "lit/directives/when.js"; export class OrAttributeReportEvent extends CustomEvent { @@ -1228,7 +1222,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { const colourIndex = index % this.colors.length; const options = { signal: this._dataAbortController?.signal }; //Load Historic Data - let dataset = await this._loadAttributeData(asset, attribute, color ?? this.colors[colourIndex], this._startOfPeriod!, this._endOfPeriod!, false, smooth, stepped, area, faint, false, asset.name + " " + label, options, unit); + let dataset = await this._loadAttributeData(asset, attribute, color ?? this.colors[colourIndex], this._startOfPeriod!, this._endOfPeriod!, false, asset.name + " " + label, options, unit); (dataset as any).assetId = asset.id; (dataset as any).attrName = attribute.name; (dataset as any).unit = unit; @@ -1273,7 +1267,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } - protected async _loadAttributeData(asset: Asset, attribute: Attribute, color: string, from: number, to: number, predicted: boolean, smooth: boolean, stepped: boolean, area: boolean, faint: boolean, extended: boolean, label?: string, options?: any, unit?: any) { + protected async _loadAttributeData(asset: Asset, attribute: Attribute, color: string, from: number, to: number, predicted: boolean, label?: string, options?: any, unit?: any) { function rgba (color: string, alpha: number) { return `rgba(${parseInt(color.slice(-6,-4), 16)}, ${parseInt(color.slice(-4,-2), 16)}, ${parseInt(color.slice(-2), 16)}, ${alpha})`; @@ -1336,11 +1330,11 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { if (response.status === 200) { data = response.data .filter(value => value.y !== null && value.y !== undefined) - .map(point => ({ x: moment(point.x).format(intervalArr[2]), y: point.y } as ValueDatapoint)) + .map(point => ({ x: point.x , y: point.y } as ValueDatapoint)) - dataset.data = data.map(point => [point.x, point.y]); + dataset.data = data.map(point => [moment(point.x).format(intervalArr[2]), point.y]); //dataset.showSymbol = data.length <= this.showSymbolMaxDatapoints; } diff --git a/ui/component/or-dashboard-builder/package.json b/ui/component/or-dashboard-builder/package.json index ecd4bb7c12..137adb9826 100644 --- a/ui/component/or-dashboard-builder/package.json +++ b/ui/component/or-dashboard-builder/package.json @@ -18,6 +18,7 @@ "dependencies": { "@openremote/core": "workspace:*", "@openremote/model": "workspace:*", + "@openremote/or-attribute-report": "workspace:*", "@openremote/or-chart": "workspace:*", "@openremote/rest": "workspace:*", "gridstack": "^7.2.0", diff --git a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts index ac0e8e2851..daa041b910 100644 --- a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts @@ -122,8 +122,8 @@ export class ReportWidget extends OrAssetWidget { static getManifest(): WidgetManifest { return { - displayName: "Line Chart", - displayIcon: "chart-line", + displayName: "Report", + displayIcon: "chart-bar", minColumnWidth: 2, minColumnHeight: 2, getContentHtml(config: ReportWidgetConfig): OrWidget { @@ -220,7 +220,7 @@ export class ReportWidget extends OrAssetWidget { `, () => { return html` - + > `; }))} `; diff --git a/yarn.lock b/yarn.lock index 511b515e60..b78353df38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2553,6 +2553,30 @@ __metadata: languageName: unknown linkType: soft +"@openremote/or-attribute-report@workspace:*, @openremote/or-attribute-report@workspace:^, @openremote/or-attribute-report@workspace:ui/component/or-attribute-report": + version: 0.0.0-use.local + resolution: "@openremote/or-attribute-report@workspace:ui/component/or-attribute-report" + dependencies: + "@material/data-table": "npm:^9.0.0" + "@material/dialog": "npm:^9.0.0" + "@openremote/core": "workspace:*" + "@openremote/or-asset-tree": "workspace:*" + "@openremote/or-attribute-picker": "workspace:*" + "@openremote/or-components": "workspace:*" + "@openremote/or-icon": "workspace:*" + "@openremote/or-mwc-components": "workspace:*" + "@openremote/or-translate": "workspace:*" + "@openremote/util": "workspace:*" + "@types/chart.js": "npm:^2.9.34" + "@types/offscreencanvas": "npm:^2019.6.4" + chart.js: "npm:^3.6.0" + echarts: "npm:^5.6.0" + jsonpath-plus: "npm:^6.0.1" + lit: "npm:^2.0.2" + moment: "npm:^2.29.4" + languageName: unknown + linkType: soft + "@openremote/or-bottom-navigation@workspace:ui/component/or-bottom-navigation": version: 0.0.0-use.local resolution: "@openremote/or-bottom-navigation@workspace:ui/component/or-bottom-navigation" @@ -2605,6 +2629,7 @@ __metadata: dependencies: "@openremote/core": "workspace:*" "@openremote/model": "workspace:*" + "@openremote/or-attribute-report": "workspace:*" "@openremote/or-chart": "workspace:*" "@openremote/rest": "workspace:*" "@openremote/util": "workspace:*" @@ -4867,6 +4892,16 @@ __metadata: languageName: node linkType: hard +"echarts@npm:^5.6.0": + version: 5.6.0 + resolution: "echarts@npm:5.6.0" + dependencies: + tslib: "npm:2.3.0" + zrender: "npm:5.6.1" + checksum: 10c0/6d6a2ee88534d1ff0433e935c542237b9896de1c94959f47ebc7e0e9da26f59bf11c91ed6fc135b62ad2786c779ee12bc536fa481e60532dad5b6a2f5167e9ea + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -7792,6 +7827,7 @@ __metadata: version: 0.0.0-use.local resolution: "openremote@workspace:." dependencies: + "@openremote/or-attribute-report": "workspace:^" "@openremote/util": "workspace:*" languageName: unknown linkType: soft @@ -9401,6 +9437,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.3.0": + version: 2.3.0 + resolution: "tslib@npm:2.3.0" + checksum: 10c0/a845aed84e7e7dbb4c774582da60d7030ea39d67307250442d35c4c5dd77e4b44007098c37dd079e100029c76055f2a362734b8442ba828f8cc934f15ed9be61 + languageName: node + linkType: hard + "tslib@npm:^1.10.0, tslib@npm:^1.8.1, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -10071,3 +10114,12 @@ __metadata: checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f languageName: node linkType: hard + +"zrender@npm:5.6.1": + version: 5.6.1 + resolution: "zrender@npm:5.6.1" + dependencies: + tslib: "npm:2.3.0" + checksum: 10c0/dc1cc570054640cbd8fbb7b92e6252f225319522bfe3e8dc8bf02cc02d414e00a4c8d0a6f89bfc9d96e5e9511fdca94dd3d06bf53690df2b2f12b0fc560ac307 + languageName: node + linkType: hard From ef0a380646338d0d1a89360bec568e511d4b8d3d Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Tue, 25 Mar 2025 12:27:53 +0100 Subject: [PATCH 05/15] added delta options to interval datapoint query --- .../datapoint/query/AssetDatapointIntervalQuery.java | 9 ++++++--- ui/component/or-attribute-report/src/index.ts | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/model/src/main/java/org/openremote/model/datapoint/query/AssetDatapointIntervalQuery.java b/model/src/main/java/org/openremote/model/datapoint/query/AssetDatapointIntervalQuery.java index e10444e7e7..b7826decf1 100644 --- a/model/src/main/java/org/openremote/model/datapoint/query/AssetDatapointIntervalQuery.java +++ b/model/src/main/java/org/openremote/model/datapoint/query/AssetDatapointIntervalQuery.java @@ -42,9 +42,12 @@ public String getSQLQuery(String tableName, Class attributeType) throws Illeg boolean isBoolean = Boolean.class.isAssignableFrom(attributeType); String function = (gapFill ? "public.time_bucket_gapfill" : "public.time_bucket"); if (isNumber) { - if(this.formula == Formula.DELTA) { - return "select " + function + "(cast(? as interval), timestamp) AS x, (last(cast(value as numeric), timestamp) - first(cast(value as numeric), timestamp)) FROM " + tableName + " WHERE ENTITY_ID = ? and ATTRIBUTE_REF = ? and " + - "timestamp BETWEEN ? AND ? GROUP BY x ORDER BY x"; + if (this.formula == Formula.DELTA) { + this.gapFill = true; //check where this is set in using this code + //Returns the delta between the start and end of the period. Ex. for interval 1 minute, 10:41 will hold the difference between 10:41:00 and 10:42:00. + return "WITH interval_data AS (" + "SELECT " + function + "(cast(? as interval), timestamp) AS x, public.locf(public.last(value::DOUBLE PRECISION, timestamp)) AS numeric_value " + + "FROM " + tableName + " " + "WHERE ENTITY_ID = ? AND ATTRIBUTE_NAME = ? AND TIMESTAMP >= ? AND TIMESTAMP <= ? GROUP BY x ) SELECT x, COALESCE(numeric_value - LAG(numeric_value, 1, numeric_value) OVER (ORDER BY x), numeric_value) AS delta FROM interval_data ORDER BY x ASC"; + } else { return "select " + function + "(cast(? as interval), timestamp) AS x, " + this.formula.toString().toLowerCase() + "(cast(value as numeric)) FROM " + tableName + " WHERE ENTITY_ID = ? and ATTRIBUTE_NAME = ? and TIMESTAMP >= ? and TIMESTAMP <= ? GROUP BY x ORDER by x ASC"; } diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 77a269e54e..1a1fcf6d08 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -1321,13 +1321,14 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { query.interval = (intervalArr[0].toString() + " " + intervalArr[1].toString()); // for example: "5 minute" - + console.log("datapointResoursequery:", query); response = await manager.rest.api.AssetDatapointResource.getDatapoints(asset.id, attribute.name, query, options); let data: ValueDatapoint[] = []; if (response.status === 200) { + console.log("response:", response.data); data = response.data .filter(value => value.y !== null && value.y !== undefined) .map(point => ({ x: point.x , y: point.y } as ValueDatapoint)) From 1319bd952988c47fb3a0dad63c60a6a96188c68d Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Tue, 25 Mar 2025 18:21:52 +0100 Subject: [PATCH 06/15] working first version of bar-chart --- ui/component/or-attribute-report/src/index.ts | 141 +++++++----------- .../src/settings/report-settings.ts | 95 ------------ .../src/widgets/report-widget.ts | 35 +---- 3 files changed, 61 insertions(+), 210 deletions(-) diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 1a1fcf6d08..2d639885df 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -163,7 +163,8 @@ const style = css` #container { display: flex; min-width: 0; - flex-direction: row; + flex-direction: column; + align-items: center; height: 100%; } @@ -189,7 +190,7 @@ const style = css` flex-wrap: wrap; margin: var(--internal-or-chart-controls-margin); width: 100%; - flex-direction: column; + flex-direction: row; margin: 0; } @@ -282,6 +283,7 @@ const style = css` flex: 1 1 0; position: relative; overflow: hidden; + width: 100%; /*min-height: 400px; max-height: 550px;*/ } @@ -415,30 +417,18 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { @property() public timeWindowKey?: string; - - - @property() public showLegend: boolean = true; @property() public denseLegend: boolean = false; - @property() public showToolBox: boolean = true; - @property() - public showSymbolMaxDatapoints: number = 30; - - @property() - public maxConcurrentDatapoints: number = 100; - @property() protected _loading: boolean = false; - - @property() protected _data?: ValueDatapoint[]; @@ -525,7 +515,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { this._chartOptions = { - animation: false, + //animation: false, grid: { show: true, backgroundColor: this._style.getPropertyValue("--internal-or-asset-tree-background-color"), @@ -533,42 +523,44 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { left: 50,//'5%', // 5% padding right: 50,//'5%', top: this.showToolBox ? 28 : 10, - bottom: 20 + bottom: 55 }, backgroundColor: this._style.getPropertyValue("--internal-or-asset-tree-background-color"), tooltip: { trigger: 'axis', axisPointer: { - type: 'cross' + type: 'shadow' }, }, + legend: {show: true}, toolbox: {}, xAxis: { type: 'category', - //axisLine: { + axisLine: { // onZero: false, - // lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")} - //}, - //splitLine: {show: true}, + lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")} + }, + splitLine: {show: true}, //min: this._startOfPeriod, //max: this._endOfPeriod, - //axisLabel: { - // showMinLabel: true, - // showMaxLabel: true, - // hideOverlap: true, - // fontSize: 10, - // formatter: { - // year: '{yyyy}-{MMM}', - // month: '{yy}-{MMM}', - // day: '{d}-{MMM}', - // hour: '{HH}:{mm}', - // minute: '{HH}:{mm}', - // second: '{HH}:{mm}:{ss}', - // millisecond: '{d}-{MMM} {HH}:{mm}', - // // @ts-ignore - // none: '{MMM}-{dd} {HH}:{mm}' - // } - //} + axisLabel: { + showMinLabel: true, + showMaxLabel: true, + hideOverlap: true, + rotate: 25, + //fontSize: 10, + //formatter: { + // year: '{yyyy}-{MMM}', + // month: '{yy}-{MMM}', + // day: '{d}-{MMM}', + // hour: '{HH}:{mm}', + // minute: '{HH}:{mm}', + // second: '{HH}:{mm}:{ss}', + // millisecond: '{d}-{MMM} {HH}:{mm}', + // // @ts-ignore + // none: '{MMM}-{dd} {HH}:{mm}' + //} + } }, yAxis: [ { @@ -599,39 +591,6 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { series: [], }; - // Add dataZoom bar if enabled - //(this._chartOptions!.dataZoom! as any[]).push({ - // start: 0, - // end: 100, - // backgroundColor: bgColor, - // fillerColor: bgColor, - // dataBackground: { - // areaStyle: { - // color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") - // } - // }, - // selectedDataBackground: { - // areaStyle: { - // color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color"), - // } - // }, - // moveHandleStyle: { - // color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") - // }, - // emphasis: { - // moveHandleStyle: { - // color: this._style.getPropertyValue("--internal-or-chart-graph-fill-color") - // }, - // handleLabel: { - // show: false - // } - // }, - // handleLabel: { - // show: false - // } - // }) - - // Add toolbox if enabled if(this.showToolBox) { this._chartOptions!.toolbox! = { @@ -639,9 +598,9 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { top: 0, feature: { dataView: {readOnly: true}, - //magicType: { - // type: ['line', 'bar'] - //}, + magicType: { + type: ['stack'] + }, saveAsImage: {} } } @@ -671,7 +630,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { if(this.shadowRoot) { const container = this.shadowRoot.getElementById('container'); if(container) { - const bottomLegend: boolean = (container.clientWidth < 600); + const bottomLegend: boolean = (container.clientWidth < 2000); // CHANGE THIS container.style.flexDirection = bottomLegend ? 'column' : 'row'; const periodControls = this.shadowRoot.querySelector('.period-controls') as HTMLElement; if(periodControls) { @@ -1156,15 +1115,15 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } else if(diffInHours <= 6) { return [30, DatapointInterval.MINUTE,"h:mm:ss"]; } else if(diffInHours <= 24) { // one day - return [1, DatapointInterval.HOUR,"h:mm"]; + return [1, DatapointInterval.HOUR,"h:mmA"]; } else if(diffInHours <= 48) { // two days - return [3, DatapointInterval.HOUR,"h:mm"]; + return [3, DatapointInterval.HOUR,"h:mmA"]; } else if(diffInHours <= 96) { - return [12, DatapointInterval.HOUR,"LT"]; + return [12, DatapointInterval.HOUR,"MMM Mo hA"]; } else if(diffInHours <= 744) { // one month - return [1, DatapointInterval.DAY,"Do"]; + return [1, DatapointInterval.DAY,"ddd MMM Do"]; } else { - return [1, DatapointInterval.MONTH,"MMM"]; + return [1, DatapointInterval.MONTH,"MMM 'YY"]; } } @@ -1185,14 +1144,19 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { this._loading = true; const dates: [Date, Date] = this._getTimeSelectionDates(this.timePrefixKey!, this.timeWindowKey!); + console.log("Selected prefix&window", dates); - if(!this._startOfPeriod || !this._endOfPeriod) { + //if(!this._startOfPeriod || !this._endOfPeriod) { + //Above is commented to work BUT WHY IS IT LIKE THIS, OR CHART WORKS WITH IT + console.log("periods were empty"); this._startOfPeriod = this.timeframe ? this.timeframe[0].getTime() : dates[0].getTime(); this._endOfPeriod = this.timeframe ? this.timeframe[1].getTime() : dates[1].getTime(); - } + //} const diffInHours = (this._endOfPeriod - this._startOfPeriod) / 1000 / 60 / 60; + console.log('diffinhours1',diffInHours); const intervalArr = this._getInterval(diffInHours); + console.log("Interval selectione", intervalArr); const stepSize: number = intervalArr[0]; const interval: DatapointInterval = intervalArr[1]; @@ -1279,11 +1243,11 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { //showSymbol: false, data: [] as [any, any][], //sampling: 'lttb', - //lineStyle: { - // color: color, + lineStyle: { + color: color, // type: predicted ? [2, 4] : extended ? [0.8, 10] : undefined, // opacity: faint ? 0.31 : 1, - //}, + }, itemStyle: { color: color }, @@ -1316,11 +1280,12 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { //if(query.type === 'interval' && !query.interval) { - const diffInHours = (this.datapointQuery.toTimestamp! - this.datapointQuery.fromTimestamp!) / 1000 / 60 / 60; + const diffInHours = (this._endOfPeriod! - this._startOfPeriod!) / 1000 / 60 / 60; + console.log("DiffInHours", diffInHours); const intervalArr = this._getInterval(diffInHours); + console.log("intervallArr2",intervalArr) query.interval = (intervalArr[0].toString() + " " + intervalArr[1].toString()); // for example: "5 minute" - - + console.log('Start:', new Date(this._startOfPeriod!), 'End:', new Date(this._endOfPeriod!)); console.log("datapointResoursequery:", query); response = await manager.rest.api.AssetDatapointResource.getDatapoints(asset.id, attribute.name, query, options); diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts index 814ebbf26c..0f88167497 100644 --- a/ui/component/or-dashboard-builder/src/settings/report-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -58,32 +58,12 @@ export class ReportSettings extends WidgetSettings { const samplingValue = Array.from(this.samplingOptions.entries()).find((entry => entry[1] === this.widgetConfig.datapointQuery.type))![0] const attributeLabelCallback = (asset: Asset, attribute: Attribute, attributeLabel: string) => { const isOnRightAxis = isMultiAxis && attrSettings.rightAxisAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; - const isFaint = attrSettings.faintAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; - const isSmooth = attrSettings.smoothAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; - const isStepped = attrSettings.steppedAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; - const isArea = attrSettings.areaAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; - const isExtended = attrSettings.extendedAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; return html` ${asset.name} ${attributeLabel} ${when(isOnRightAxis, () => html` `)} - ${when(isFaint, () => html` - - `)} - ${when(isSmooth, () => html` - - `)} - ${when(isStepped, () => html` - - `)} - ${when(isArea, () => html` - - `)} - ${when(isExtended, () => html` - - `)} ` } @@ -95,32 +75,6 @@ export class ReportSettings extends WidgetSettings { tooltip: i18next.t('dashboard.lineColor'), disabled: false }, - { - icon: 'chart-bell-curve-cumulative', - tooltip: i18next.t("dashboard.smooth"), - disabled: false - }, - { - icon: 'square-wave', - tooltip: i18next.t('dashboard.stepped'), - disabled: false - }, - { - icon: 'chart-areaspline-variant', - tooltip: i18next.t('dashboard.fill'), - disabled: false - }, - { - icon: 'arrange-send-backward', - tooltip: i18next.t('dashboard.faint'), - disabled: false - }, - { - icon: 'arrow-expand-right', - tooltip: i18next.t('dashboard.extendData'), - disabled: false - }, - { icon: this.widgetConfig.attributeSettings.rightAxisAttributes.includes(attributeRef) ? "arrow-right-bold" : "arrow-left-bold", tooltip: i18next.t('dashboard.toggleAxis'), @@ -178,13 +132,6 @@ export class ReportSettings extends WidgetSettings { @or-mwc-input-changed="${(ev: OrInputChangedEvent) => this.onShowLegendToggle(ev)}" >
- -
- - -
@@ -192,12 +139,6 @@ export class ReportSettings extends WidgetSettings { @or-mwc-input-changed="${(ev: OrInputChangedEvent) => this.onShowToolBoxToggle(ev)}" >
- -
- -
@@ -310,13 +251,6 @@ export class ReportSettings extends WidgetSettings { > `; } - case 'lttb': { - return html ` - - `; - } default: return html``; } @@ -359,21 +293,6 @@ export class ReportSettings extends WidgetSettings { case "arrow-left-bold": this.toggleAttributeSetting("rightAxisAttributes", attributeRef); break; - case "chart-bell-curve-cumulative": - this.toggleAttributeSetting("smoothAttributes", attributeRef); - break; - case "square-wave": - this.toggleAttributeSetting("steppedAttributes", attributeRef); - break; - case "chart-areaspline-variant": - this.toggleAttributeSetting("areaAttributes", attributeRef); - break; - case "arrange-send-backward": - this.toggleAttributeSetting("faintAttributes", attributeRef); - break; - case "arrow-expand-right": - this.toggleAttributeSetting("extendedAttributes", attributeRef); - break; default: console.warn('Unknown attribute panel action:', action); } @@ -444,11 +363,6 @@ export class ReportSettings extends WidgetSettings { this.notifyConfigUpdate(); } - protected onShowZoomBarToggle(ev: OrInputChangedEvent) { - this.widgetConfig.showZoomBar = ev.detail.value; - this.notifyConfigUpdate(); - } - protected onShowToolBoxToggle(ev: OrInputChangedEvent) { this.widgetConfig.showToolBox = ev.detail.value; this.notifyConfigUpdate(); @@ -484,13 +398,4 @@ export class ReportSettings extends WidgetSettings { this.notifyConfigUpdate(); } - protected onMaxConcurrentDatapointsValueChange(ev: OrInputChangedEvent) { - this.widgetConfig.maxConcurrentDatapoints = ev.detail.value; - this.notifyConfigUpdate(); - } - - protected onShowSymbolMaxDatapointsValueChange(ev: OrInputChangedEvent) { - this.widgetConfig.showSymbolMaxDatapoints = ev.detail.value; - this.notifyConfigUpdate(); - } } diff --git a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts index daa041b910..4feefed6d1 100644 --- a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts @@ -1,4 +1,4 @@ -import {AssetDatapointLTTBQuery, AssetDatapointQueryUnion, Attribute, AttributeRef} from "@openremote/model"; +import {AssetDatapointIntervalQuery, AssetDatapointQueryUnion, Attribute, AttributeRef} from "@openremote/model"; import {html, PropertyValues, TemplateResult } from "lit"; import { when } from "lit/directives/when.js"; import moment from "moment"; @@ -15,11 +15,6 @@ export interface ReportWidgetConfig extends WidgetConfig { colorPickedAttributes: Array<{ attributeRef: AttributeRef; color: string }>; attributeSettings: { rightAxisAttributes: AttributeRef[], - smoothAttributes: AttributeRef[], - steppedAttributes: AttributeRef[], - areaAttributes: AttributeRef[], - faintAttributes: AttributeRef[], - extendedAttributes: AttributeRef[], }, datapointQuery: AssetDatapointQueryUnion; chartOptions?: any; @@ -27,10 +22,8 @@ export interface ReportWidgetConfig extends WidgetConfig { defaultTimeWindowKey: string; defaultTimePrefixKey: string; showLegend: boolean; - showZoomBar: boolean; showToolBox: boolean; - showSymbolMaxDatapoints: number; - maxConcurrentDatapoints: number; + } function getDefaultTimeWindowOptions(): Map { @@ -60,7 +53,7 @@ function getDefaultSamplingOptions(): Map { } function getDefaultWidgetConfig(): ReportWidgetConfig { - const preset = "24Hours"; // Default time preset, "last" prefix is hardcoded in startDate and endDate below. + const preset = "30Days"; // Default time preset, "last" prefix is hardcoded in startDate and endDate below. const dateFunc = getDefaultTimeWindowOptions().get(preset); const startDate = moment().subtract(dateFunc![1], dateFunc![0]).startOf(dateFunc![0]); const endDate = dateFunc![1]== 1 ? moment().endOf(dateFunc![0]) : moment(); @@ -69,14 +62,9 @@ function getDefaultWidgetConfig(): ReportWidgetConfig { colorPickedAttributes: [], attributeSettings: { rightAxisAttributes: [], - smoothAttributes: [], - steppedAttributes: [], - areaAttributes: [], - faintAttributes: [], - extendedAttributes: [], }, datapointQuery: { - type: "lttb", + type: "interval", fromTimestamp: startDate.toDate().getTime(), toTimestamp: endDate.toDate().getTime(), }, @@ -98,11 +86,7 @@ function getDefaultWidgetConfig(): ReportWidgetConfig { defaultTimeWindowKey: preset, defaultTimePrefixKey: "last", showLegend: true, - showZoomBar: false, showToolBox: false, - showSymbolMaxDatapoints: 30, - maxConcurrentDatapoints: 100 - }; } @@ -224,7 +208,6 @@ export class ReportWidget extends OrAssetWidget { .colorPickedAttributes="${this.widgetConfig?.colorPickedAttributes != null ? this.widgetConfig?.colorPickedAttributes : []}" .attributeSettings="${this.widgetConfig?.attributeSettings != null ? this.widgetConfig.attributeSettings : {}}" .showLegend="${(this.widgetConfig?.showLegend != null) ? this.widgetConfig?.showLegend : true}" - .showZoomBar="${(this.widgetConfig?.showZoomBar != null) ? this.widgetConfig?.showZoomBar : true}" .showToolBox="${(this.widgetConfig?.showToolBox != null) ? this.widgetConfig?.showToolBox : true}" .attributeControls="${false}" .timestampControls="${!this.widgetConfig?.showTimestampControls}" .timeWindowOptions="${getDefaultTimeWindowOptions()}" @@ -232,8 +215,6 @@ export class ReportWidget extends OrAssetWidget { .timePrefixKey="${this.widgetConfig?.defaultTimePrefixKey}" .timeWindowKey="${this.widgetConfig?.defaultTimeWindowKey}" .datapointQuery="${this.datapointQuery}" .chartOptions="${this.widgetConfig?.chartOptions}" - .showSymbolMaxDatapoints="${this.widgetConfig?.showSymbolMaxDatapoints}" - .maxConcurrentDatapoints="${this.widgetConfig?.maxConcurrentDatapoints}" style="height: 100%" > `; @@ -241,11 +222,11 @@ export class ReportWidget extends OrAssetWidget { `; } - protected getDefaultQuery(): AssetDatapointLTTBQuery { + protected getDefaultQuery(): AssetDatapointIntervalQuery { return { - type: "lttb", - fromTimestamp: moment().set('minute', -60).toDate().getTime(), - toTimestamp: moment().set('minute', 60).toDate().getTime() + type: "interval", + fromTimestamp: moment().set('day', -30).toDate().getTime(), + toTimestamp: moment().toDate().getTime() } } } From 184e90db3adfa9597bc0bf2d23a154ac40025f72 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Wed, 26 Mar 2025 15:55:13 +0100 Subject: [PATCH 07/15] test using bar labels = chaos --- ui/component/or-attribute-report/src/index.ts | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 2d639885df..813f36998c 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -523,11 +523,13 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { left: 50,//'5%', // 5% padding right: 50,//'5%', top: this.showToolBox ? 28 : 10, - bottom: 55 + bottom: 55, + containLabel: true }, backgroundColor: this._style.getPropertyValue("--internal-or-asset-tree-background-color"), tooltip: { trigger: 'axis', + confine: true, //make tooltip not go outside frame bounds axisPointer: { type: 'shadow' }, @@ -541,11 +543,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")} }, splitLine: {show: true}, - //min: this._startOfPeriod, - //max: this._endOfPeriod, axisLabel: { - showMinLabel: true, - showMaxLabel: true, hideOverlap: true, rotate: 25, //fontSize: 10, @@ -1109,19 +1107,19 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { //This must be reworked for my own use case if(diffInHours <= 1) { - return [5, DatapointInterval.MINUTE,"h:mm:ss"]; + return [5, DatapointInterval.MINUTE,"h:mmA"]; } else if(diffInHours <= 3) { - return [10, DatapointInterval.MINUTE,"h:mm:ss"]; + return [20, DatapointInterval.MINUTE,"h:mmA"]; } else if(diffInHours <= 6) { - return [30, DatapointInterval.MINUTE,"h:mm:ss"]; + return [30, DatapointInterval.MINUTE,"h:mmA"]; } else if(diffInHours <= 24) { // one day return [1, DatapointInterval.HOUR,"h:mmA"]; } else if(diffInHours <= 48) { // two days - return [3, DatapointInterval.HOUR,"h:mmA"]; + return [6, DatapointInterval.HOUR,"h:mmA"]; } else if(diffInHours <= 96) { - return [12, DatapointInterval.HOUR,"MMM Mo hA"]; + return [12, DatapointInterval.HOUR,"MMM Do hA"]; } else if(diffInHours <= 744) { // one month - return [1, DatapointInterval.DAY,"ddd MMM Do"]; + return [1, DatapointInterval.DAY,"ddd | MMM Do"]; } else { return [1, DatapointInterval.MONTH,"MMM 'YY"]; } @@ -1185,6 +1183,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { const unit = Util.resolveUnits(Util.getAttributeUnits(attribute, descriptors[0], asset.type)); const colourIndex = index % this.colors.length; const options = { signal: this._dataAbortController?.signal }; + let formula = 'Max' //Load Historic Data let dataset = await this._loadAttributeData(asset, attribute, color ?? this.colors[colourIndex], this._startOfPeriod!, this._endOfPeriod!, false, asset.name + " " + label, options, unit); (dataset as any).assetId = asset.id; @@ -1192,6 +1191,16 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { (dataset as any).unit = unit; (dataset as any).yAxisIndex = shownOnRightAxis ? '1' : '0'; (dataset as any).color = color ?? this.colors[colourIndex]; + (dataset as any).label = { + show: true, + position: 'insideBottom', + distance: 15, + align: 'left', + verticalAlign: 'middle', + rotate: 90, + formatter: `{c1}${unit} ${formula}}`, + //fontSize: 16, + } data.push(dataset); //Load Comparison Data @@ -1206,6 +1215,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } this._data = data; + console.log("this._data",this._data); this._loading = false; @@ -1240,9 +1250,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { const dataset = { name: label, type: 'bar', - //showSymbol: false, data: [] as [any, any][], - //sampling: 'lttb', lineStyle: { color: color, // type: predicted ? [2, 4] : extended ? [0.8, 10] : undefined, @@ -1255,8 +1263,6 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { // @ts-ignore valueFormatter: value => value + unit }, - //smooth: smooth, - //step: stepped ? 'end' : undefined, //areaStyle: area ? {color: new graphic.LinearGradient(0, 0, 0, 1, [ // { // offset: 0, @@ -1298,10 +1304,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { .filter(value => value.y !== null && value.y !== undefined) .map(point => ({ x: point.x , y: point.y } as ValueDatapoint)) - - dataset.data = data.map(point => [moment(point.x).format(intervalArr[2]), point.y]); - //dataset.showSymbol = data.length <= this.showSymbolMaxDatapoints; } @@ -1314,27 +1317,20 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { protected _updateChartData(){ this._chart!.setOption({ + //legend: {data: this._data!.map((data) => `${data.name} ${data.unit}`)}, //xAxis: { // min: this._startOfPeriod, // max: this._endOfPeriod //}, series: this._data!.map(series => ({ ...series, - //markLine: { - // symbol: 'circle', - // silent: true, - // data: [{ name: '', xAxis: new Date().toISOString(), label: { formatter: '{b}' } }], - // lineStyle: { - // color: this._style.getPropertyValue("--internal-or-chart-text-color"), - // type: 'solid', - // width: 2, - // opacity: 1 - // } - //} + //markLine: KLOPT DIT NOG WEL NU ALLEEN SERIES ER NOG IS + // })) }); } + protected _toggleChartEventListeners(connect: boolean){ if (connect) { //Connect event listeners From 9f387e2855fec9b6fde2e78617cfac33998eeaca Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Thu, 27 Mar 2025 15:05:19 +0100 Subject: [PATCH 08/15] table and chart kind of working, quite messy and table requires css treatment --- ui/component/or-attribute-report/src/index.ts | 174 +++++++++--------- .../src/settings/report-settings.ts | 12 ++ .../src/widgets/report-widget.ts | 3 + 3 files changed, 106 insertions(+), 83 deletions(-) diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 813f36998c..66c1736aa4 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -21,7 +21,8 @@ import "@openremote/or-mwc-components/or-mwc-input"; import "@openremote/or-components/or-panel"; import "@openremote/or-translate"; import {ECharts, EChartsOption, init} from "echarts"; -import {TimeUnit,} from "chart.js"; +import {MDCDataTable} from "@material/data-table"; +import {OrMwcTableRowClickEvent, TableColumn, TableRow, TableConfig} from "@openremote/or-mwc-components/or-mwc-table"; import {InputType} from "@openremote/or-mwc-components/or-mwc-input"; import "@openremote/or-components/or-loading-indicator"; import moment from "moment"; @@ -297,6 +298,27 @@ const style = css` height: 100%; !important; } + #table-container { + height: 100%; + min-height: 250px; + } + + #table { + width: 100%; + margin-bottom: 10px; + } + + #table > table { + width: 100%; + table-layout: fixed; + } + + #table th, #table td { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + @media screen and (max-width: 1280px) { #chart-container { max-height: 330px; @@ -376,7 +398,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { @property() - public dataProvider?: (startOfPeriod: number, endOfPeriod: number, timeUnits: TimeUnit, stepSize: number) => Promise<[]> + public dataProvider?: () => Promise<[]> @property({type: Array}) public colors: string[] = ["#3869B1", "#DA7E30", "#3F9852", "#CC2428", "#6B4C9A", "#922427", "#958C3D", "#535055"]; @@ -426,25 +448,30 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { @property() public showToolBox: boolean = true; + @property() + public isChart: boolean = false; + @property() protected _loading: boolean = false; @property() - protected _data?: ValueDatapoint[]; + protected _data?: any[]; @property() protected _tableTemplate?: TemplateResult; @query("#chart") protected _chartElem!: HTMLDivElement; + @query("#table") + protected _tableElem!: HTMLDivElement; + protected _table?: MDCDataTable; + protected columns: TableColumn[] = []; + protected rows: TableRow[] = []; protected _chartOptions: EChartsOption = {}; protected _chart?: ECharts; protected _style!: CSSStyleDeclaration; protected _startOfPeriod?: number; protected _endOfPeriod?: number; - - protected _timeUnits?: TimeUnit; - protected _stepSize?: number; protected _latestError?: string; protected _dataAbortController?: AbortController; @@ -539,25 +566,12 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { xAxis: { type: 'category', axisLine: { - // onZero: false, lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")} }, splitLine: {show: true}, axisLabel: { hideOverlap: true, rotate: 25, - //fontSize: 10, - //formatter: { - // year: '{yyyy}-{MMM}', - // month: '{yy}-{MMM}', - // day: '{d}-{MMM}', - // hour: '{HH}:{mm}', - // minute: '{HH}:{mm}', - // second: '{HH}:{mm}:{ss}', - // millisecond: '{d}-{MMM} {HH}:{mm}', - // // @ts-ignore - // none: '{MMM}-{dd} {HH}:{mm}' - //} } }, yAxis: [ @@ -653,10 +667,22 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } render() { - const disabled = this._loading || this._latestError; + const disabled = !this.isChart || this._loading || this._latestError; + + const tableConfig: any = { + fullHeight: true, + pagination: { + enable: true, + options: [10, 25, 100], + } + } as TableConfig + + return html` + + +
-
${when(this._loading, () => html`
@@ -667,8 +693,22 @@ export class OrAttributeReport extends translate(i18next)(LitElement) {
`)} + ${when(this.isChart, () => html` +
+ + `, () => html ` +
+
+
+
+ +
+ `)} + + + ${(this.timestampControls || this.attributeControls || this.showLegend) ? html`
@@ -1083,6 +1123,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { return [startDate.toDate(), endDate.toDate()]; } + protected _shiftTimeframe(currentStart: Date, timeWindowSelected: string, direction: string) { const timeWindow = this.timeWindowOptions!.get(timeWindowSelected); @@ -1092,20 +1133,14 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { const [unit, value] = timeWindow; let newStart = moment(currentStart); - direction === "previous" ? newStart.subtract(value, unit as moment.unitOfTime.DurationConstructor) : newStart.add(value, unit as moment.unitOfTime.DurationConstructor); - let newEnd = moment(newStart).add(value, unit as moment.unitOfTime.DurationConstructor); - this.timeframe = [newStart.toDate(), newEnd.toDate()]; } protected _getInterval(diffInHours: number): [number, DatapointInterval, string] { //Returns amount of steps, interval size and moment.js time format - - //This must be reworked for my own use case - if(diffInHours <= 1) { return [5, DatapointInterval.MINUTE,"h:mmA"]; } else if(diffInHours <= 3) { @@ -1140,35 +1175,18 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } this._loading = true; - const dates: [Date, Date] = this._getTimeSelectionDates(this.timePrefixKey!, this.timeWindowKey!); - console.log("Selected prefix&window", dates); - //if(!this._startOfPeriod || !this._endOfPeriod) { //Above is commented to work BUT WHY IS IT LIKE THIS, OR CHART WORKS WITH IT - console.log("periods were empty"); - this._startOfPeriod = this.timeframe ? this.timeframe[0].getTime() : dates[0].getTime(); - this._endOfPeriod = this.timeframe ? this.timeframe[1].getTime() : dates[1].getTime(); + this._startOfPeriod = this.timeframe ? this.timeframe[0].getTime() : dates[0].getTime(); + this._endOfPeriod = this.timeframe ? this.timeframe[1].getTime() : dates[1].getTime(); //} - - const diffInHours = (this._endOfPeriod - this._startOfPeriod) / 1000 / 60 / 60; - console.log('diffinhours1',diffInHours); - const intervalArr = this._getInterval(diffInHours); - console.log("Interval selectione", intervalArr); - - const stepSize: number = intervalArr[0]; - const interval: DatapointInterval = intervalArr[1]; - - const lowerCaseInterval = interval.toLowerCase(); - this._timeUnits = lowerCaseInterval as TimeUnit; - this._stepSize = stepSize; - const data: any = []; let promises; try { if(this.dataProvider) { - await this.dataProvider(this._startOfPeriod, this._endOfPeriod, (interval.toString() as TimeUnit), stepSize).then((dataset) => { + await this.dataProvider().then((dataset) => { dataset.forEach((set) => { data.push(set); }); }); } else { @@ -1191,16 +1209,8 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { (dataset as any).unit = unit; (dataset as any).yAxisIndex = shownOnRightAxis ? '1' : '0'; (dataset as any).color = color ?? this.colors[colourIndex]; - (dataset as any).label = { - show: true, - position: 'insideBottom', - distance: 15, - align: 'left', - verticalAlign: 'middle', - rotate: 90, - formatter: `{c1}${unit} ${formula}}`, - //fontSize: 16, - } + //(dataset as any).label.formatter = `{@[1]}${unit} ${formula}`; this does not scale well + data.push(dataset); //Load Comparison Data @@ -1216,6 +1226,19 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { this._data = data; console.log("this._data",this._data); + + //Load data into table format + this.columns = ['Time', ...this._data!.map(entry => entry.name)]; + + this.rows= this._data![0].data.map((subArray: any[]) => ({ + content: [subArray[0], ...this._data!.map(entry => entry.data[subArray[0] === entry.data[0][0] ? 0 : 1][1])] + })); + + + console.log("columns:", this.columns); + console.log("rows:", this.rows); + + this._loading = false; @@ -1243,36 +1266,25 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { protected async _loadAttributeData(asset: Asset, attribute: Attribute, color: string, from: number, to: number, predicted: boolean, label?: string, options?: any, unit?: any) { - function rgba (color: string, alpha: number) { - return `rgba(${parseInt(color.slice(-6,-4), 16)}, ${parseInt(color.slice(-4,-2), 16)}, ${parseInt(color.slice(-2), 16)}, ${alpha})`; - } - const dataset = { name: label, type: 'bar', data: [] as [any, any][], lineStyle: { color: color, - // type: predicted ? [2, 4] : extended ? [0.8, 10] : undefined, - // opacity: faint ? 0.31 : 1, - }, - itemStyle: { - color: color }, + //label: { Does not scale well + // show: true, + // position: 'insideBottom', + // distance: 15, + // align: 'left', + // verticalAlign: 'middle', + // rotate: 90, + //}, tooltip: { // @ts-ignore valueFormatter: value => value + unit }, - //areaStyle: area ? {color: new graphic.LinearGradient(0, 0, 0, 1, [ - // { - // offset: 0, - // color: rgba(color, faint ? 0.1 : 0.5) - // }, - // { - // offset: 1, - // color: rgba(color, 0) - // } - // ])} as any : undefined, } @@ -1302,9 +1314,11 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { console.log("response:", response.data); data = response.data .filter(value => value.y !== null && value.y !== undefined) - .map(point => ({ x: point.x , y: point.y } as ValueDatapoint)) + .map(point => ({x: point.x, y: point.y} as ValueDatapoint)) dataset.data = data.map(point => [moment(point.x).format(intervalArr[2]), point.y]); + //dataset.label.show =data.length <= 12 // this does not scale well + } @@ -1318,14 +1332,8 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { protected _updateChartData(){ this._chart!.setOption({ //legend: {data: this._data!.map((data) => `${data.name} ${data.unit}`)}, - //xAxis: { - // min: this._startOfPeriod, - // max: this._endOfPeriod - //}, series: this._data!.map(series => ({ ...series, - //markLine: KLOPT DIT NOG WEL NU ALLEEN SERIES ER NOG IS - // })) }); } diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts index 0f88167497..c9a99af4d7 100644 --- a/ui/component/or-dashboard-builder/src/settings/report-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -139,6 +139,13 @@ export class ReportSettings extends WidgetSettings { @or-mwc-input-changed="${(ev: OrInputChangedEvent) => this.onShowToolBoxToggle(ev)}" >
+ +
+ + +
@@ -368,6 +375,11 @@ export class ReportSettings extends WidgetSettings { this.notifyConfigUpdate(); } + protected onisChartToggle(ev: OrInputChangedEvent) { + this.widgetConfig.isChart = ev.detail.value; + this.notifyConfigUpdate(); + } + protected setAxisMinMaxValue(axis: 'left' | 'right', type: 'min' | 'max', value?: number) { if(axis === 'left') { if(type === 'min') { diff --git a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts index 4feefed6d1..aa33cff3f9 100644 --- a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts @@ -23,6 +23,7 @@ export interface ReportWidgetConfig extends WidgetConfig { defaultTimePrefixKey: string; showLegend: boolean; showToolBox: boolean; + isChart: boolean; } @@ -87,6 +88,7 @@ function getDefaultWidgetConfig(): ReportWidgetConfig { defaultTimePrefixKey: "last", showLegend: true, showToolBox: false, + isChart: true }; } @@ -215,6 +217,7 @@ export class ReportWidget extends OrAssetWidget { .timePrefixKey="${this.widgetConfig?.defaultTimePrefixKey}" .timeWindowKey="${this.widgetConfig?.defaultTimeWindowKey}" .datapointQuery="${this.datapointQuery}" .chartOptions="${this.widgetConfig?.chartOptions}" + .isChart="${this.widgetConfig?.isChart}" style="height: 100%" > `; From 160a7c150167dcbd88ea4c614e0c04b9c7f8dab4 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Fri, 4 Apr 2025 10:09:17 +0200 Subject: [PATCH 09/15] some tunings to CSS and make to and from date always visible --- docker-compose.yml | 2 ++ ui/component/or-attribute-report/src/index.ts | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 779b2e8707..7691d52230 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,8 @@ services: volumes: - postgresql-data:/var/lib/postgresql/data - manager-data:/storage + ports: + - "5432:5432" keycloak: restart: always diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 66c1736aa4..7bcc6bb3bd 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -300,7 +300,8 @@ const style = css` #table-container { height: 100%; - min-height: 250px; + width: 100%; + overflow: hidden; } #table { @@ -549,7 +550,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { borderColor: this._style.getPropertyValue("--internal-or-chart-text-color"), left: 50,//'5%', // 5% padding right: 50,//'5%', - top: this.showToolBox ? 28 : 10, + //top: 28,//this.showToolBox ? 28 : 10, bottom: 55, containLabel: true }, @@ -569,9 +570,12 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { lineStyle: {color: this._style.getPropertyValue("--internal-or-chart-text-color")} }, splitLine: {show: true}, + axisTick: {alignWithLabel: true}, axisLabel: { hideOverlap: true, rotate: 25, + interval: 0, + fontSize: 10 } }, yAxis: [ @@ -670,14 +674,9 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { const disabled = !this.isChart || this._loading || this._latestError; const tableConfig: any = { - fullHeight: true, - pagination: { - enable: true, - options: [10, 25, 100], - } + fullHeight: true } as TableConfig - return html` @@ -699,11 +698,8 @@ export class OrAttributeReport extends translate(i18next)(LitElement) {
`, () => html ` -
-
-
- + `)} @@ -753,24 +749,22 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { `} ` : undefined}
- ${this.timeframe ? html`
- + - +
${i18next.t('from')}:${moment(this.timeframe[0]).format("L HH:mm")}${moment(this.timeframe?.[0] ?? this._startOfPeriod).format("lll")}
${i18next.t('to')}:${moment(this.timeframe[1]).format("L HH:mm")}${moment(this.timeframe?.[1] ?? this._endOfPeriod).format("lll")}
- ` : undefined} ${this.attributeControls ? html` ` : undefined} @@ -1285,6 +1279,10 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { // @ts-ignore valueFormatter: value => value + unit }, + //showBackground: true, + //backgroundStyle: { + // color: 'rgba(180, 180, 180, 0.2)' + //} } From c7d3550449984f704d883b3918b76439283886a3 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Tue, 8 Apr 2025 10:54:26 +0200 Subject: [PATCH 10/15] not working save with dialog list --- ui/app/shared/locales/en/or.json | 10 +- ui/component/or-attribute-report/src/index.ts | 48 ++++----- .../src/settings/report-settings.ts | 101 ++++++++++++++++-- .../src/widgets/report-widget.ts | 34 ++++-- 4 files changed, 153 insertions(+), 40 deletions(-) diff --git a/ui/app/shared/locales/en/or.json b/ui/app/shared/locales/en/or.json index 21e9765038..e5660fe161 100644 --- a/ui/app/shared/locales/en/or.json +++ b/ui/app/shared/locales/en/or.json @@ -761,7 +761,15 @@ "zoom": "Zoom", "center": "Center", "imageUrl": "Image URL", - "noImageSelected": "No image selected" + "noImageSelected": "No image selected", + "methodMaxAttributes": "Maximum", + "methodMinAttributes": "Minimum", + "methodAvgAttributes": "Average", + "methodDeltaAttributes": "Difference", + "methodMedianAttributes": "Median", + "methodModeAttributes": "Mode", + "methodSumAttributes": "Datapoints Sum", + "methodCountAttributes": "Datapoint Count" }, "south": "South", "east": "East", diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 7bcc6bb3bd..26e149dc8a 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -381,23 +381,15 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { public attributeQueryFormula: Array<{ attributeRef: AttributeRef; formula: AssetDatapointIntervalQueryFormula }> = []; @property({type: Object}) - public attributeSettings: { - rightAxisAttributes: AttributeRef[], - smoothAttributes: AttributeRef[], - steppedAttributes: AttributeRef[], - areaAttributes: AttributeRef[], - faintAttributes: AttributeRef[], - extendedAttributes: AttributeRef[], - } = { - rightAxisAttributes: [], - smoothAttributes: [], - steppedAttributes: [], - areaAttributes: [], - faintAttributes: [], - extendedAttributes: [], + public attributeSettings = { + rightAxisAttributes: [] as AttributeRef[], + smoothAttributes: [] as AttributeRef[], + steppedAttributes: [] as AttributeRef[], + areaAttributes: [] as AttributeRef[], + faintAttributes: [] as AttributeRef[], + extendedAttributes: [] as AttributeRef[], }; - @property() public dataProvider?: () => Promise<[]> @@ -412,6 +404,15 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { @property({type: Object}) public chartOptions?: any + public chartSettings: { + showLegend: boolean; + showToolBox: boolean; + defaultStacked: boolean; + } = { + showLegend: true, + showToolBox: true, + defaultStacked: false, + }; @property({type: String}) public realm?: string; @@ -440,15 +441,9 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { @property() public timeWindowKey?: string; - @property() - public showLegend: boolean = true; - @property() public denseLegend: boolean = false; - @property() - public showToolBox: boolean = true; - @property() public isChart: boolean = false; @@ -562,8 +557,8 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { type: 'shadow' }, }, - legend: {show: true}, - toolbox: {}, + legend: this.chartSettings.showLegend ? {show: true} : undefined, + toolbox: this.chartSettings.showToolBox ? {show:true, feature: {magicType: {type: ['bar', 'stack']}}} : undefined, xAxis: { type: 'category', axisLine: { @@ -608,7 +603,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { }; // Add toolbox if enabled - if(this.showToolBox) { + if(this.chartSettings.showToolBox) { this._chartOptions!.toolbox! = { right: 45, top: 0, @@ -706,7 +701,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { - ${(this.timestampControls || this.attributeControls || this.showLegend) ? html` + ${(this.timestampControls || this.attributeControls || this.chartSettings.showLegend) ? html`
@@ -769,7 +764,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { ` : undefined}
- ${cache(this.showLegend ? html` + ${cache(this.chartSettings.showLegend ? html`
${this.assetAttributes == null || this.assetAttributes.length == 0 ? html`
@@ -1264,6 +1259,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { name: label, type: 'bar', data: [] as [any, any][], + stack: this.chartSettings.defaultStacked ? 'stack' : undefined, lineStyle: { color: color, }, diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts index c9a99af4d7..1b6d6c15e8 100644 --- a/ui/component/or-dashboard-builder/src/settings/report-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -10,6 +10,8 @@ import {ReportWidgetConfig} from "../widgets/report-widget"; import {InputType, OrInputChangedEvent} from "@openremote/or-mwc-components/or-mwc-input"; import {when} from "lit/directives/when.js"; import moment from "moment/moment"; +import {ListItem, ListType, OrMwcList, OrMwcListChangedEvent} from "@openremote/or-mwc-components/or-mwc-list"; +import {showDialog, OrMwcDialog, DialogAction} from "@openremote/or-mwc-components/or-mwc-dialog"; const styling = css` .switch-container { @@ -75,6 +77,11 @@ export class ReportSettings extends WidgetSettings { tooltip: i18next.t('dashboard.lineColor'), disabled: false }, + { + icon: 'calculator-variant-outline', + tooltip: i18next.t('dashboard.algorithmMethod'), + disabled: false + }, { icon: this.widgetConfig.attributeSettings.rightAxisAttributes.includes(attributeRef) ? "arrow-right-bold" : "arrow-left-bold", tooltip: i18next.t('dashboard.toggleAxis'), @@ -128,22 +135,29 @@ export class ReportSettings extends WidgetSettings {
-
-
+ +
+ + +
@@ -300,6 +314,9 @@ export class ReportSettings extends WidgetSettings { case "arrow-left-bold": this.toggleAttributeSetting("rightAxisAttributes", attributeRef); break; + case "calculator-variant-outline": + this.algorithmMethodsDialog(attributeRef); + break; default: console.warn('Unknown attribute panel action:', action); } @@ -350,6 +367,73 @@ export class ReportSettings extends WidgetSettings { ); } + + protected algorithmMethodsDialog(attributeRef: AttributeRef) { + + const methodList: ListItem[] = Object.entries(this.widgetConfig.attributeSettings) + .map(([key, attributeRefs]) => { + return { + text: key, + value: attributeRefs.includes(attributeRef) ? key : '', + translate: false + } + } + ); + console.log('methodList:', methodList); + + let changes: ListItem[] = []; + + const dialog = showDialog(new OrMwcDialog() + .setContent(html` +
+ +
+ `) + .setStyles(html` + + `) + .setHeading(i18next.t("algorithmMethod")) + .setActions([ + { + actionName: "cancel", + content: "cancel" + }, + { + default: true, + actionName: "ok", + action: () => { + console.log('changes:', changes); + changes.forEach((item) => { + this.toggleAttributeSetting(item.value as keyof ReportWidgetConfig["attributeSettings"], attributeRef); + }); + }, + content: "ok" + } + ]) + .setDismissAction(null)); + } + protected onTimePreFixSelect(ev: OrInputChangedEvent) { this.widgetConfig.defaultTimePrefixKey = ev.detail.value.toString(); this.notifyConfigUpdate(); @@ -366,16 +450,21 @@ export class ReportSettings extends WidgetSettings { } protected onShowLegendToggle(ev: OrInputChangedEvent) { - this.widgetConfig.showLegend = ev.detail.value; + this.widgetConfig.chartSettings.showLegend = ev.detail.value; this.notifyConfigUpdate(); } protected onShowToolBoxToggle(ev: OrInputChangedEvent) { - this.widgetConfig.showToolBox = ev.detail.value; + this.widgetConfig.chartSettings.showToolBox = ev.detail.value; + this.notifyConfigUpdate(); + } + + protected onDefaultStackedToggle(ev: OrInputChangedEvent) { + this.widgetConfig.chartSettings.defaultStacked = ev.detail.value; this.notifyConfigUpdate(); } - protected onisChartToggle(ev: OrInputChangedEvent) { + protected onIsChartToggle(ev: OrInputChangedEvent) { this.widgetConfig.isChart = ev.detail.value; this.notifyConfigUpdate(); } diff --git a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts index aa33cff3f9..05550f4aa9 100644 --- a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts @@ -15,16 +15,26 @@ export interface ReportWidgetConfig extends WidgetConfig { colorPickedAttributes: Array<{ attributeRef: AttributeRef; color: string }>; attributeSettings: { rightAxisAttributes: AttributeRef[], + methodMaxAttributes: AttributeRef[], + methodMinAttributes: AttributeRef[], + methodAvgAttributes: AttributeRef[], + methodDeltaAttributes: AttributeRef[], + methodMedianAttributes: AttributeRef[], + methodModeAttributes: AttributeRef[], + methodSumAttributes: AttributeRef[], + methodCountAttributes: AttributeRef[], }, datapointQuery: AssetDatapointQueryUnion; chartOptions?: any; showTimestampControls: boolean; defaultTimeWindowKey: string; defaultTimePrefixKey: string; - showLegend: boolean; - showToolBox: boolean; + chartSettings: { + showLegend: boolean; + showToolBox: boolean; + defaultStacked: boolean; + }; isChart: boolean; - } function getDefaultTimeWindowOptions(): Map { @@ -63,6 +73,14 @@ function getDefaultWidgetConfig(): ReportWidgetConfig { colorPickedAttributes: [], attributeSettings: { rightAxisAttributes: [], + methodMaxAttributes: [], + methodMinAttributes: [], + methodAvgAttributes: [], + methodDeltaAttributes: [], + methodMedianAttributes: [], + methodModeAttributes: [], + methodSumAttributes: [], + methodCountAttributes: [] }, datapointQuery: { type: "interval", @@ -86,8 +104,11 @@ function getDefaultWidgetConfig(): ReportWidgetConfig { showTimestampControls: false, defaultTimeWindowKey: preset, defaultTimePrefixKey: "last", - showLegend: true, - showToolBox: false, + chartSettings: { + showLegend: true, + showToolBox: false, + defaultStacked: false, + }, isChart: true }; } @@ -209,8 +230,7 @@ export class ReportWidget extends OrAssetWidget { Date: Tue, 8 Apr 2025 16:28:52 +0200 Subject: [PATCH 11/15] method settings dialog working --- ui/app/shared/locales/en/or.json | 18 +-- ui/component/or-attribute-report/src/index.ts | 18 +-- .../src/settings/report-settings.ts | 124 +++++++++--------- 3 files changed, 79 insertions(+), 81 deletions(-) diff --git a/ui/app/shared/locales/en/or.json b/ui/app/shared/locales/en/or.json index e5660fe161..8cc94a9635 100644 --- a/ui/app/shared/locales/en/or.json +++ b/ui/app/shared/locales/en/or.json @@ -164,6 +164,14 @@ "dataSampling": "Data sampling", "algorithm": "Algorithm", "algorithmMethod": "Method per interval", + "methodMaxAttributes": "Maximum", + "methodMinAttributes": "Minimum", + "methodAvgAttributes": "Average", + "methodDeltaAttributes": "Difference", + "methodMedianAttributes": "Median", + "methodModeAttributes": "Mode", + "methodSumAttributes": "Datapoints Sum", + "methodCountAttributes": "Datapoint Count", "addAttribute": "Add attribute", "selectAttributes": "Select attributes", "selectAttribute": "Select attribute", @@ -761,15 +769,7 @@ "zoom": "Zoom", "center": "Center", "imageUrl": "Image URL", - "noImageSelected": "No image selected", - "methodMaxAttributes": "Maximum", - "methodMinAttributes": "Minimum", - "methodAvgAttributes": "Average", - "methodDeltaAttributes": "Difference", - "methodMedianAttributes": "Median", - "methodModeAttributes": "Mode", - "methodSumAttributes": "Datapoints Sum", - "methodCountAttributes": "Datapoint Count" + "noImageSelected": "No image selected" }, "south": "South", "east": "East", diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 26e149dc8a..d54ba93112 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -1214,7 +1214,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } this._data = data; - console.log("this._data",this._data); + //console.log("this._data",this._data); //Load data into table format this.columns = ['Time', ...this._data!.map(entry => entry.name)]; @@ -1224,8 +1224,8 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { })); - console.log("columns:", this.columns); - console.log("rows:", this.rows); + //console.log("columns:", this.columns); + //console.log("rows:", this.rows); this._loading = false; @@ -1293,19 +1293,19 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { //if(query.type === 'interval' && !query.interval) { const diffInHours = (this._endOfPeriod! - this._startOfPeriod!) / 1000 / 60 / 60; - console.log("DiffInHours", diffInHours); + // console.log("DiffInHours", diffInHours); const intervalArr = this._getInterval(diffInHours); - console.log("intervallArr2",intervalArr) + // console.log("intervallArr2",intervalArr) query.interval = (intervalArr[0].toString() + " " + intervalArr[1].toString()); // for example: "5 minute" - console.log('Start:', new Date(this._startOfPeriod!), 'End:', new Date(this._endOfPeriod!)); - console.log("datapointResoursequery:", query); + // console.log('Start:', new Date(this._startOfPeriod!), 'End:', new Date(this._endOfPeriod!)); + // console.log("datapointResoursequery:", query); response = await manager.rest.api.AssetDatapointResource.getDatapoints(asset.id, attribute.name, query, options); let data: ValueDatapoint[] = []; if (response.status === 200) { - console.log("response:", response.data); + // console.log("response:", response.data); data = response.data .filter(value => value.y !== null && value.y !== undefined) .map(point => ({x: point.x, y: point.y} as ValueDatapoint)) @@ -1318,7 +1318,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } - console.log("dataset", dataset); + // console.log("dataset", dataset); return dataset; } diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts index 1b6d6c15e8..eb9ef9ead8 100644 --- a/ui/component/or-dashboard-builder/src/settings/report-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -79,7 +79,7 @@ export class ReportSettings extends WidgetSettings { }, { icon: 'calculator-variant-outline', - tooltip: i18next.t('dashboard.algorithmMethod'), + tooltip: i18next.t('algorithmMethod'), disabled: false }, { @@ -287,40 +287,18 @@ export class ReportSettings extends WidgetSettings { switch (action.icon) { case "palette": // Change color - const colorInput = document.createElement('input'); - colorInput.type = 'color'; - colorInput.style.border = 'none'; - colorInput.style.height = '31px'; - colorInput.style.width = '31px'; - colorInput.style.padding = '1px 3px'; - colorInput.style.minHeight = '22px'; - colorInput.style.minWidth = '30px'; - colorInput.style.cursor = 'pointer'; - colorInput.addEventListener('change', (e: any) => { - const color = e.target.value; - const existingIndex = this.widgetConfig.colorPickedAttributes.findIndex(item => - item.attributeRef.id === attributeRef.id && item.attributeRef.name === attributeRef.name - ); - if (existingIndex >= 0) { - this.widgetConfig.colorPickedAttributes[existingIndex].color = color; - } else { - this.widgetConfig.colorPickedAttributes.push({ attributeRef, color }); - } - this.notifyConfigUpdate(); - }); - colorInput.click(); + this.openColorPickDialog(attributeRef); break; case "arrow-right-bold": case "arrow-left-bold": this.toggleAttributeSetting("rightAxisAttributes", attributeRef); break; case "calculator-variant-outline": - this.algorithmMethodsDialog(attributeRef); + this.openAlgorithmMethodsDialog(attributeRef); break; default: console.warn('Unknown attribute panel action:', action); } - console.log("end of onAttributeAction" + JSON.stringify(this.widgetConfig.attributeSettings)); } // When the list of attributeRefs is changed by the asset selector, @@ -361,6 +339,33 @@ export class ReportSettings extends WidgetSettings { this.notifyConfigUpdate(); } + protected openColorPickDialog(attributeRef: AttributeRef) { + const colorInput = document.createElement('input'); + colorInput.type = 'color'; + colorInput.style.border = 'none'; + colorInput.style.height = '31px'; + colorInput.style.width = '31px'; + colorInput.style.padding = '1px 3px'; + colorInput.style.minHeight = '22px'; + colorInput.style.minWidth = '30px'; + colorInput.style.cursor = 'pointer'; + colorInput.addEventListener('change', (e: any) => { + const color = e.target.value; + const existingIndex = this.widgetConfig.colorPickedAttributes.findIndex(item => + item.attributeRef.id === attributeRef.id && item.attributeRef.name === attributeRef.name + ); + if (existingIndex >= 0) { + this.widgetConfig.colorPickedAttributes[existingIndex].color = color; + } else { + this.widgetConfig.colorPickedAttributes.push({ attributeRef, color }); + } + this.notifyConfigUpdate(); + }); + colorInput.click(); + } + + + protected removeFromColorPickedAttributes(attributeRef: AttributeRef) { this.widgetConfig.colorPickedAttributes = this.widgetConfig.colorPickedAttributes.filter( item => item.attributeRef.id !== attributeRef.id || item.attributeRef.name !== attributeRef.name @@ -368,51 +373,32 @@ export class ReportSettings extends WidgetSettings { } - protected algorithmMethodsDialog(attributeRef: AttributeRef) { + protected openAlgorithmMethodsDialog(attributeRef: AttributeRef) { const methodList: ListItem[] = Object.entries(this.widgetConfig.attributeSettings) + .filter(([key]) => key.includes('method')) .map(([key, attributeRefs]) => { return { text: key, - value: attributeRefs.includes(attributeRef) ? key : '', - translate: false - } - } - ); - console.log('methodList:', methodList); - - let changes: ListItem[] = []; - - const dialog = showDialog(new OrMwcDialog() + value: key, + data: attributeRefs.includes(attributeRef) ? key : undefined, + translate: true + }; + }); + let selected: ListItem[] = []; + + showDialog(new OrMwcDialog() .setContent(html`
-
`) - .setStyles(html` - - `) .setHeading(i18next.t("algorithmMethod")) .setActions([ { @@ -423,9 +409,21 @@ export class ReportSettings extends WidgetSettings { default: true, actionName: "ok", action: () => { - console.log('changes:', changes); - changes.forEach((item) => { - this.toggleAttributeSetting(item.value as keyof ReportWidgetConfig["attributeSettings"], attributeRef); + // Check which settings need updating + const changedMethods = methodList.filter(input => { + const selectedItem = selected.find(selected => selected.value === input.value); + return (!selectedItem && input.data !== undefined) || + (selectedItem && selectedItem.data === undefined) || + (selectedItem && selectedItem.data !== input.data); + }); + //Update the settings + changedMethods.forEach((item: ListItem) => { + if (item.value) { + this.toggleAttributeSetting( + item.value as keyof ReportWidgetConfig["attributeSettings"], + attributeRef + ); + } }); }, content: "ok" From 73deea5613d125c9e7d2868590b96cc5bf5f1eca Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Thu, 10 Apr 2025 10:40:52 +0200 Subject: [PATCH 12/15] fix for current methods retrival, remove redundant code --- ui/app/shared/locales/en/or.json | 3 +- ui/component/or-attribute-report/src/index.ts | 157 +++++++++--------- .../src/settings/report-settings.ts | 52 ++---- .../src/widgets/report-widget.ts | 1 + 4 files changed, 89 insertions(+), 124 deletions(-) diff --git a/ui/app/shared/locales/en/or.json b/ui/app/shared/locales/en/or.json index 8cc94a9635..ce28da23df 100644 --- a/ui/app/shared/locales/en/or.json +++ b/ui/app/shared/locales/en/or.json @@ -769,7 +769,8 @@ "zoom": "Zoom", "center": "Center", "imageUrl": "Image URL", - "noImageSelected": "No image selected" + "noImageSelected": "No image selected", + "noData": "No data for selected timeperiod or no interval selected" }, "south": "South", "east": "East", diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index d54ba93112..40039525d1 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -22,7 +22,7 @@ import "@openremote/or-components/or-panel"; import "@openremote/or-translate"; import {ECharts, EChartsOption, init} from "echarts"; import {MDCDataTable} from "@material/data-table"; -import {OrMwcTableRowClickEvent, TableColumn, TableRow, TableConfig} from "@openremote/or-mwc-components/or-mwc-table"; +import {TableColumn, TableRow, TableConfig} from "@openremote/or-mwc-components/or-mwc-table"; import {InputType} from "@openremote/or-mwc-components/or-mwc-input"; import "@openremote/or-components/or-loading-indicator"; import moment from "moment"; @@ -300,7 +300,7 @@ const style = css` #table-container { height: 100%; - width: 100%; + max-width: 100%; overflow: hidden; } @@ -320,6 +320,7 @@ const style = css` text-overflow: ellipsis; } + @media screen and (max-width: 1280px) { #chart-container { max-height: 330px; @@ -377,17 +378,17 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { @property({type: Array}) public colorPickedAttributes: Array<{ attributeRef: AttributeRef; color: string }> = []; - @property({type: Array}) - public attributeQueryFormula: Array<{ attributeRef: AttributeRef; formula: AssetDatapointIntervalQueryFormula }> = []; - @property({type: Object}) public attributeSettings = { rightAxisAttributes: [] as AttributeRef[], - smoothAttributes: [] as AttributeRef[], - steppedAttributes: [] as AttributeRef[], - areaAttributes: [] as AttributeRef[], - faintAttributes: [] as AttributeRef[], - extendedAttributes: [] as AttributeRef[], + methodMaxAttributes:[] as AttributeRef[], + methodMinAttributes: [] as AttributeRef[], + methodAvgAttributes: [] as AttributeRef[], + methodDeltaAttributes: [] as AttributeRef[], + methodMedianAttributes: [] as AttributeRef[], + methodModeAttributes: [] as AttributeRef[], + methodSumAttributes: [] as AttributeRef[], + methodCountAttributes: [] as AttributeRef[] }; @property() @@ -523,28 +524,15 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { return; } - if (!this._chart) { - - let bgColor = this._style.getPropertyValue("--internal-or-chart-graph-fill-color").trim(); - const opacity = Number(this._style.getPropertyValue("--internal-or-chart-graph-fill-opacity").trim()); - if (!isNaN(opacity)) { - if (bgColor.startsWith("#") && (bgColor.length === 4 || bgColor.length === 7)) { - bgColor += (bgColor.length === 4 ? Math.round(opacity * 255).toString(16).substr(0, 1) : Math.round(opacity * 255).toString(16)); - } else if (bgColor.startsWith("rgb(")) { - bgColor = bgColor.substring(0, bgColor.length - 1) + opacity; - } - } - - - + if (!this._chart && this.isChart) { this._chartOptions = { //animation: false, grid: { show: true, backgroundColor: this._style.getPropertyValue("--internal-or-asset-tree-background-color"), borderColor: this._style.getPropertyValue("--internal-or-chart-text-color"), - left: 50,//'5%', // 5% padding - right: 50,//'5%', + left: 25,//'5%', // 5% padding + right: 25,//'5%', //top: 28,//this.showToolBox ? 28 : 10, bottom: 55, containLabel: true @@ -626,7 +614,9 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { if (changedProperties.has("_data")) { //Update chart to data from set period - this._updateChartData(); + if (this._chart) { + this._updateChartData(); + } } this.onCompleted().then(() => { @@ -687,6 +677,11 @@ export class OrAttributeReport extends translate(i18next)(LitElement) {
`)} + ${when(this._data?.every(entry => entry.data.length === 0), () => html` +
+ +
+ `)} ${when(this.isChart, () => html`
@@ -694,7 +689,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { `, () => html `
- `)} @@ -1165,8 +1160,6 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { this._loading = true; const dates: [Date, Date] = this._getTimeSelectionDates(this.timePrefixKey!, this.timeWindowKey!); - //if(!this._startOfPeriod || !this._endOfPeriod) { - //Above is commented to work BUT WHY IS IT LIKE THIS, OR CHART WORKS WITH IT this._startOfPeriod = this.timeframe ? this.timeframe[0].getTime() : dates[0].getTime(); this._endOfPeriod = this.timeframe ? this.timeframe[1].getTime() : dates[1].getTime(); //} @@ -1190,21 +1183,36 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { const unit = Util.resolveUnits(Util.getAttributeUnits(attribute, descriptors[0], asset.type)); const colourIndex = index % this.colors.length; const options = { signal: this._dataAbortController?.signal }; - let formula = 'Max' - //Load Historic Data - let dataset = await this._loadAttributeData(asset, attribute, color ?? this.colors[colourIndex], this._startOfPeriod!, this._endOfPeriod!, false, asset.name + " " + label, options, unit); - (dataset as any).assetId = asset.id; - (dataset as any).attrName = attribute.name; - (dataset as any).unit = unit; - (dataset as any).yAxisIndex = shownOnRightAxis ? '1' : '0'; - (dataset as any).color = color ?? this.colors[colourIndex]; - //(dataset as any).label.formatter = `{@[1]}${unit} ${formula}`; this does not scale well - - data.push(dataset); - - //Load Comparison Data - // this is predicted dataset = await this._loadAttributeData(this.assets[assetIndex], attribute, color ?? this.colors[colourIndex], predictedFromTimestamp, this._endOfPeriod!, true, smooth, stepped, area, faint, false , asset.name + " " + label + " " + i18next.t("predicted"), options, unit); - //data.push(dataset); + + + // Map calculation methods to their corresponding attribute arrays and formulas + const methodMapping: { [key: string]: { active: boolean; formula: AssetDatapointIntervalQueryFormula } } = { + AVG: { active: !!this.attributeSettings.methodAvgAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.AVG }, + MIN: { active: !!this.attributeSettings.methodMinAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MIN }, + MAX: { active: !!this.attributeSettings.methodMaxAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MAX }, + DELTA: { active: !!this.attributeSettings.methodDeltaAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.DELTA }, + //MEDIAN: { active: !!this.attributeSettings.methodMedianAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MEDIAN }, + //MODE: { active: !!this.attributeSettings.methodModeAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MODE }, + //SUM: { active: !!this.attributeSettings.methodSumAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.SUM }, + //COUNT: { active: !!this.attributeSettings.methodCountAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.COUNT } + }; + // Iterate over the mapping, make a dataset for every active method + for (const [key, value] of Object.entries(methodMapping)) { + console.log(`Key: ${key}, Active: ${value.active}, Formula: ${value.formula}`); + if (value.active) { + //Initiate query Attribute Data + let dataset = await this._loadAttributeData(asset, attribute, color ?? this.colors[colourIndex], this._startOfPeriod!, this._endOfPeriod!, value.formula, asset.name + " " + label + " | " + i18next.t(value.formula), options, unit); + (dataset as any).assetId = asset.id; + (dataset as any).attrName = attribute.name; + (dataset as any).unit = unit; + (dataset as any).yAxisIndex = shownOnRightAxis ? '1' : '0'; + (dataset as any).color = color ?? this.colors[colourIndex]; + + data.push(dataset); + + } + } + }); } @@ -1214,19 +1222,16 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } this._data = data; - //console.log("this._data",this._data); + console.log("this._data",this._data); //Load data into table format - this.columns = ['Time', ...this._data!.map(entry => entry.name)]; - - this.rows= this._data![0].data.map((subArray: any[]) => ({ - content: [subArray[0], ...this._data!.map(entry => entry.data[subArray[0] === entry.data[0][0] ? 0 : 1][1])] - })); - - - //console.log("columns:", this.columns); - //console.log("rows:", this.rows); + if (!this.isChart && this._data![0]) { + this.columns = ['Time', ...this._data!.map(entry => entry.name)]; + this.rows = this._data![0].data.map((subArray: any[]) => ({ + content: [subArray[0], ...this._data!.map(entry => entry.data[subArray[0] === entry.data[0][0] ? 0 : 1][1])] + })); + } this._loading = false; @@ -1253,32 +1258,31 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } - protected async _loadAttributeData(asset: Asset, attribute: Attribute, color: string, from: number, to: number, predicted: boolean, label?: string, options?: any, unit?: any) { + protected async _loadAttributeData(asset: Asset, attribute: Attribute, color: string, from: number, to: number, formula: AssetDatapointIntervalQueryFormula, label?: string, options?: any, unit?: any) { const dataset = { name: label, type: 'bar', data: [] as [any, any][], - stack: this.chartSettings.defaultStacked ? 'stack' : undefined, + stack: this.chartSettings.defaultStacked ? `${formula}` : undefined, lineStyle: { color: color, }, - //label: { Does not scale well - // show: true, - // position: 'insideBottom', - // distance: 15, - // align: 'left', - // verticalAlign: 'middle', - // rotate: 90, - //}, tooltip: { // @ts-ignore valueFormatter: value => value + unit }, - //showBackground: true, - //backgroundStyle: { - // color: 'rgba(180, 180, 180, 0.2)' - //} + label: { + show: true, + align: 'left', + verticalAlign: 'middle', + position: 'insideBottom', + rotate: '90', + distance: 15, + formatter: (params: { dataIndex: number; value: number }): string => { + // Show labels only for the first index (index 0) + return params.dataIndex === 0 ? `${formula}` : ''; //Or make it i18next.t(formula) to display longer text + }} } @@ -1289,43 +1293,33 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { query.fromTimestamp = this._startOfPeriod; query.toTimestamp = this._endOfPeriod; - - - //if(query.type === 'interval' && !query.interval) { + query.formula = formula; const diffInHours = (this._endOfPeriod! - this._startOfPeriod!) / 1000 / 60 / 60; - // console.log("DiffInHours", diffInHours); const intervalArr = this._getInterval(diffInHours); - // console.log("intervallArr2",intervalArr) query.interval = (intervalArr[0].toString() + " " + intervalArr[1].toString()); // for example: "5 minute" - // console.log('Start:', new Date(this._startOfPeriod!), 'End:', new Date(this._endOfPeriod!)); - // console.log("datapointResoursequery:", query); + response = await manager.rest.api.AssetDatapointResource.getDatapoints(asset.id, attribute.name, query, options); let data: ValueDatapoint[] = []; if (response.status === 200) { - // console.log("response:", response.data); data = response.data .filter(value => value.y !== null && value.y !== undefined) .map(point => ({x: point.x, y: point.y} as ValueDatapoint)) dataset.data = data.map(point => [moment(point.x).format(intervalArr[2]), point.y]); - //dataset.label.show =data.length <= 12 // this does not scale well - } } - // console.log("dataset", dataset); return dataset; } protected _updateChartData(){ this._chart!.setOption({ - //legend: {data: this._data!.map((data) => `${data.name} ${data.unit}`)}, series: this._data!.map(series => ({ ...series, })) @@ -1355,4 +1349,5 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } + } diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts index eb9ef9ead8..2e7d6307b0 100644 --- a/ui/component/or-dashboard-builder/src/settings/report-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -57,7 +57,6 @@ export class ReportSettings extends WidgetSettings { const min = this.widgetConfig.chartOptions.options?.scales?.y?.min; const max = this.widgetConfig.chartOptions.options?.scales?.y?.max; const isMultiAxis = attrSettings.rightAxisAttributes.length > 0; - const samplingValue = Array.from(this.samplingOptions.entries()).find((entry => entry[1] === this.widgetConfig.datapointQuery.type))![0] const attributeLabelCallback = (asset: Asset, attribute: Attribute, attributeLabel: string) => { const isOnRightAxis = isMultiAxis && attrSettings.rightAxisAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; return html` @@ -240,43 +239,10 @@ export class ReportSettings extends WidgetSettings { })}
- - - -
-
- -
-
- ${this.getSamplingOptionsTemplate(this.widgetConfig.datapointQuery.type)} -
-
-
`; } - protected getSamplingOptionsTemplate(type: any): TemplateResult { - switch (type) { - case 'interval': { - const intervalQuery = this.widgetConfig.datapointQuery as AssetDatapointIntervalQuery; - const formulaOptions = [AssetDatapointIntervalQueryFormula.AVG, AssetDatapointIntervalQueryFormula.MIN, AssetDatapointIntervalQueryFormula.MAX, AssetDatapointIntervalQueryFormula.DELTA]; - return html` - - `; - } - default: - return html``; - } - } - // Check which icon was pressed and act accordingly. protected onAttributeAction(ev: AttributeActionEvent) { const { asset ,attributeRef, action } = ev.detail; @@ -374,18 +340,24 @@ export class ReportSettings extends WidgetSettings { protected openAlgorithmMethodsDialog(attributeRef: AttributeRef) { - + console.log('attributeRef:', attributeRef); const methodList: ListItem[] = Object.entries(this.widgetConfig.attributeSettings) .filter(([key]) => key.includes('method')) .map(([key, attributeRefs]) => { + const isActive = attributeRefs.some( + (ref: AttributeRef) => + ref.id === attributeRef.id && ref.name === attributeRef.name + ); + return { text: key, value: key, - data: attributeRefs.includes(attributeRef) ? key : undefined, - translate: true + data: isActive ? key : undefined, + translate: true, }; }); let selected: ListItem[] = []; + console.log('InputList:', methodList); showDialog(new OrMwcDialog() .setContent(html` @@ -410,6 +382,7 @@ export class ReportSettings extends WidgetSettings { actionName: "ok", action: () => { // Check which settings need updating + console.log('Selected:', selected) const changedMethods = methodList.filter(input => { const selectedItem = selected.find(selected => selected.value === input.value); return (!selectedItem && input.data !== undefined) || @@ -492,9 +465,4 @@ export class ReportSettings extends WidgetSettings { this.setAxisMinMaxValue(axis, type, (ev.detail.value ? (type === 'min' ? 0 : 100) : undefined)); } - protected onSamplingQueryChange(ev: OrInputChangedEvent) { - this.widgetConfig.datapointQuery.type = this.samplingOptions.get(ev.detail.value)! as any; - this.notifyConfigUpdate(); - } - } diff --git a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts index 05550f4aa9..d8bea44f10 100644 --- a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts @@ -179,6 +179,7 @@ export class ReportWidget extends OrAssetWidget { if(changedProps.has('widgetConfig') && this.widgetConfig) { this.datapointQuery = this.widgetConfig.datapointQuery; + console.log("WidgetConfig:", this.widgetConfig) const attributeRefs = this.widgetConfig.attributeRefs; if(attributeRefs.length === 0) { From eb283823a866b9df4a7a4e2beecae693a5f3c5ff Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Fri, 18 Apr 2025 16:54:12 +0200 Subject: [PATCH 13/15] tweaks --- ui/app/shared/locales/en/or.json | 5 +- ui/component/or-attribute-report/src/index.ts | 204 ++++++++++-------- .../src/settings/chart-settings.ts | 1 - .../src/settings/report-settings.ts | 170 ++++++++------- 4 files changed, 207 insertions(+), 173 deletions(-) diff --git a/ui/app/shared/locales/en/or.json b/ui/app/shared/locales/en/or.json index ce28da23df..758f025d3a 100644 --- a/ui/app/shared/locales/en/or.json +++ b/ui/app/shared/locales/en/or.json @@ -756,6 +756,9 @@ "showLegend": "Show legend", "showGeoJson": "Show GeoJSON overlay", "showValueAs": "Show value as", + "defaultStacked": "Stack by default", + "showToolBox": "Show Toolbox", + "isChart": "Bar chart instead of table", "customLabel": "Custom label", "customTimeSpan": "Custom", "userCanEdit": "Allow user input", @@ -770,7 +773,7 @@ "center": "Center", "imageUrl": "Image URL", "noImageSelected": "No image selected", - "noData": "No data for selected timeperiod or no interval selected" + "noData": "No data" }, "south": "South", "east": "East", diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 40039525d1..d8bd10eaf2 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -23,19 +23,20 @@ import "@openremote/or-translate"; import {ECharts, EChartsOption, init} from "echarts"; import {MDCDataTable} from "@material/data-table"; import {TableColumn, TableRow, TableConfig} from "@openremote/or-mwc-components/or-mwc-table"; -import {InputType} from "@openremote/or-mwc-components/or-mwc-input"; +import {InputType, OrMwcInput} from "@openremote/or-mwc-components/or-mwc-input"; import "@openremote/or-components/or-loading-indicator"; import moment from "moment"; import {OrAssetTreeSelectionEvent} from "@openremote/or-asset-tree"; import {getAssetDescriptorIconTemplate} from "@openremote/or-icon"; import {GenericAxiosResponse, isAxiosError} from "@openremote/rest"; import {OrAttributePicker, OrAttributePickerPickedEvent} from "@openremote/or-attribute-picker"; -import {showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; +import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; import {cache} from "lit/directives/cache.js"; import {throttle} from "lodash"; import {getContentWithMenuTemplate} from "@openremote/or-mwc-components/or-mwc-menu"; import {ListItem} from "@openremote/or-mwc-components/or-mwc-list"; import {when} from "lit/directives/when.js"; +import {createRef, Ref, ref } from "lit/directives/ref.js"; export class OrAttributeReportEvent extends CustomEvent { @@ -193,6 +194,7 @@ const style = css` width: 100%; flex-direction: row; margin: 0; + border-top: 3px solid var(--or-app-color2); } #attribute-list { @@ -200,8 +202,9 @@ const style = css` min-height: 50px; flex: 1 1 0; width: 100%; - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; } .attribute-list-dense { flex-wrap: wrap; @@ -238,6 +241,7 @@ const style = css` --or-icon-fill: var(--or-app-color4); } + .attribute-list-item-label { display: flex; flex: 1 1 0; @@ -263,6 +267,7 @@ const style = css` #controls > * { margin-top: 5px; margin-bottom: 5px; + justify-content: center; } .dialog-container { @@ -302,6 +307,7 @@ const style = css` height: 100%; max-width: 100%; overflow: hidden; + flex: 1 1 0; } #table { @@ -533,8 +539,8 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { borderColor: this._style.getPropertyValue("--internal-or-chart-text-color"), left: 25,//'5%', // 5% padding right: 25,//'5%', - //top: 28,//this.showToolBox ? 28 : 10, - bottom: 55, + top: this.chartSettings.showToolBox ? 28 : 10, + bottom: 25, //55 containLabel: true }, backgroundColor: this._style.getPropertyValue("--internal-or-asset-tree-background-color"), @@ -545,7 +551,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { type: 'shadow' }, }, - legend: this.chartSettings.showLegend ? {show: true} : undefined, + //legend: this.chartSettings.showLegend ? {show: true} : undefined, toolbox: this.chartSettings.showToolBox ? {show:true, feature: {magicType: {type: ['bar', 'stack']}}} : undefined, xAxis: { type: 'category', @@ -678,7 +684,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) {
`)} ${when(this._data?.every(entry => entry.data.length === 0), () => html` -
+
`)} @@ -698,12 +704,55 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { ${(this.timestampControls || this.attributeControls || this.chartSettings.showLegend) ? html`
+ ${cache(this.chartSettings.showLegend ? html` +
+ ${this.assetAttributes == null || this.assetAttributes.length == 0 ? html` +
+ ${i18next.t('noAttributesConnected')} +
+ ` : undefined} + ${this.assetAttributes && this.assetAttributes.map(([assetIndex, attr], index) => { + const asset: Asset | undefined = this.assets[assetIndex]; + const colourIndex = index % this.colors.length; + const color = this.colorPickedAttributes.find(({ attributeRef }) => attributeRef.name === attr.name && attributeRef.id === asset.id)?.color; + const descriptors = AssetModelUtil.getAttributeAndValueDescriptors(asset!.type, attr.name, attr); + const label = Util.getAttributeLabel(attr, descriptors[0], asset!.type, true); + const axisNote = (this.attributeSettings.rightAxisAttributes.find(ar => asset!.id === ar.id && attr.name === ar.name)) ? i18next.t('right') : undefined; + const bgColor = ( color ?? this.colors[colourIndex] ) || ""; + //Find which calculation methods are active + const methodList: { data: string | undefined; }[] = Object.entries(this.attributeSettings) + .filter(([key]) => key.includes('method')) + .map(([key, attributeRefs]) => { + const isActive = attributeRefs.some( + (ref: AttributeRef) => + ref.id === asset!.id && ref.name === attr.name + ); + return { + data: isActive ? ` (${i18next.t(key)})` : undefined, + }; + }); + + + + return html` +
+ ${getAssetDescriptorIconTemplate(AssetModelUtil.getAssetDescriptor(this.assets[assetIndex]!.type!), undefined, undefined, bgColor.split('#')[1])} +
+
+ ${this.assets[assetIndex].name} + ${when(axisNote, () => html`(${axisNote})`)} +
+ ${label} ${methodList.map(item => item.data)} +
+
+ ` + })} +
+ ` : undefined)}
${this.timePrefixKey && this.timePrefixOptions && this.timeWindowKey && this.timeWindowOptions ? html` ${this.timestampControls ? html` - - ${getContentWithMenuTemplate( html``, @@ -720,7 +769,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { )} ${getContentWithMenuTemplate( - html``, + html``, Array.from(this.timeWindowOptions!.keys()).map((key) => ({ value: key } as ListItem)), this.timeWindowKey, (value: string | string[]) => { @@ -732,8 +781,12 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { undefined, true )} + + - + + + ` : html` `} @@ -759,36 +812,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { ` : undefined}
- ${cache(this.chartSettings.showLegend ? html` -
- ${this.assetAttributes == null || this.assetAttributes.length == 0 ? html` -
- ${i18next.t('noAttributesConnected')} -
- ` : undefined} - ${this.assetAttributes && this.assetAttributes.map(([assetIndex, attr], index) => { - const asset: Asset | undefined = this.assets[assetIndex]; - const colourIndex = index % this.colors.length; - const color = this.colorPickedAttributes.find(({ attributeRef }) => attributeRef.name === attr.name && attributeRef.id === asset.id)?.color; - const descriptors = AssetModelUtil.getAttributeAndValueDescriptors(asset!.type, attr.name, attr); - const label = Util.getAttributeLabel(attr, descriptors[0], asset!.type, true); - const axisNote = (this.attributeSettings.rightAxisAttributes.find(ar => asset!.id === ar.id && attr.name === ar.name)) ? i18next.t('right') : undefined; - const bgColor = ( color ?? this.colors[colourIndex] ) || ""; - return html` -
- ${getAssetDescriptorIconTemplate(AssetModelUtil.getAssetDescriptor(this.assets[assetIndex]!.type!), undefined, undefined, bgColor.split('#')[1])} -
-
- ${this.assets[assetIndex].name} - ${when(axisNote, () => html`(${axisNote})`)} -
- ${label} -
-
- ` - })} -
- ` : undefined)} +
` : undefined}
@@ -815,42 +839,6 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } } - removeDatasetHighlight() { - if(this._chart){ - let options = this._chart.getOption(); - if (options.series && Array.isArray(options.series)) { - options.series.forEach(function (series) { - if (series.lineStyle.opacity == 0.2 || series.lineStyle.opacity == 0.99) { - series.lineStyle.opacity = 0.31; - } else { - series.lineStyle.opacity = 1; - } - }); - } - this._chart.setOption(options); - } - } - - addDatasetHighlight(assetId?:string, attrName?:string) { - if (this._chart) { - let options = this._chart.getOption(); - if (options.series && Array.isArray(options.series)) { - options.series.forEach(function (series) { - if (series.assetId != assetId || series.attrName != attrName) { - if (series.lineStyle.opacity == 0.31) { // 0.31 is faint setting, 1 is normal - series.lineStyle.opacity = 0.2; - } else { - series.lineStyle.opacity = 0.3; - } - } else if (series.lineStyle.opacity == 0.31) { // extra highlight if selected is faint - series.lineStyle.opacity = 0.99; - } - }); - } - this._chart.setOption(options) - } - }; - @@ -992,6 +980,32 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { dialog.addEventListener(OrAttributePickerPickedEvent.NAME, (ev: any) => this._addAttribute(ev.detail)); } + protected _openTimeDialog(startTimestamp?: number, endTimestamp?: number) { + const startRef: Ref = createRef(); + const endRef: Ref = createRef(); + const dialog = showDialog(new OrMwcDialog() + .setHeading(i18next.t('timeframe')) + .setContent(() => html` +
+ + +
+ `) + .setActions([{ + actionName: "cancel", + content: "cancel" + }, { + actionName: "ok", + content: "ok", + action: () => { + if(startRef.value?.value && endRef.value?.value) { + this.timeframe = [new Date(startRef.value.value), new Date(endRef.value.value)]; + } + } + }]) + ) + } + protected async _addAttribute(selectedAttrs?: AttributeRef[]) { if (!selectedAttrs) return; @@ -1131,15 +1145,15 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { return [20, DatapointInterval.MINUTE,"h:mmA"]; } else if(diffInHours <= 6) { return [30, DatapointInterval.MINUTE,"h:mmA"]; - } else if(diffInHours <= 24) { // one day + } else if(diffInHours <= 24) { // hour if up to one day return [1, DatapointInterval.HOUR,"h:mmA"]; - } else if(diffInHours <= 48) { // two days + } else if(diffInHours <= 48) { // hour if up to two days return [6, DatapointInterval.HOUR,"h:mmA"]; - } else if(diffInHours <= 96) { - return [12, DatapointInterval.HOUR,"MMM Do hA"]; - } else if(diffInHours <= 744) { // one month + } else if(diffInHours <= 744) { // one day if up to one month return [1, DatapointInterval.DAY,"ddd | MMM Do"]; - } else { + } else if(diffInHours <= 8760) { // one week if up to 1 year + return [1, DatapointInterval.WEEK,"[Week] w 'YY"]; + } else { // one month if more than a year return [1, DatapointInterval.MONTH,"MMM 'YY"]; } } @@ -1272,17 +1286,17 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { // @ts-ignore valueFormatter: value => value + unit }, - label: { - show: true, - align: 'left', - verticalAlign: 'middle', - position: 'insideBottom', - rotate: '90', - distance: 15, - formatter: (params: { dataIndex: number; value: number }): string => { - // Show labels only for the first index (index 0) - return params.dataIndex === 0 ? `${formula}` : ''; //Or make it i18next.t(formula) to display longer text - }} + //label: { + // show: true, + // align: 'left', + // verticalAlign: 'middle', + // position: 'insideBottom', + // rotate: '90', + // distance: 15, + // formatter: (params: { dataIndex: number; value: number }): string => { + // // Show labels only for the first index (index 0) + // return params.dataIndex === 0 ? `${formula}` : ''; //Or make it i18next.t(formula) to display longer text + // }} } diff --git a/ui/component/or-dashboard-builder/src/settings/chart-settings.ts b/ui/component/or-dashboard-builder/src/settings/chart-settings.ts index 8da97fe582..f0c7cb4350 100644 --- a/ui/component/or-dashboard-builder/src/settings/chart-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/chart-settings.ts @@ -107,7 +107,6 @@ export class ChartSettings extends WidgetSettings {
-
${when(isMultiAxis, () => html` diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts index 2e7d6307b0..09135c0e7e 100644 --- a/ui/component/or-dashboard-builder/src/settings/report-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -59,12 +59,27 @@ export class ReportSettings extends WidgetSettings { const isMultiAxis = attrSettings.rightAxisAttributes.length > 0; const attributeLabelCallback = (asset: Asset, attribute: Attribute, attributeLabel: string) => { const isOnRightAxis = isMultiAxis && attrSettings.rightAxisAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name) !== undefined; + //Find which calculation methods are active + const methodList = Object.entries(this.widgetConfig.attributeSettings) + .filter(([key]) => key.includes('method')) + .reduce((activeKeys, [key, attributeRefs]) => { + const isActive = attributeRefs.some( + (ref: AttributeRef) => ref.id === asset?.id && ref.name === attribute.name + ); + if (isActive) { + activeKeys.push(i18next.t(key)); + } + console.log('activeKeys:', activeKeys) + return activeKeys; + }, [] as any[]); + return html` ${asset.name} ${attributeLabel} ${when(isOnRightAxis, () => html` `)} + item.data)}> ` } @@ -152,9 +167,9 @@ export class ReportSettings extends WidgetSettings { @or-mwc-input-changed="${(ev: OrInputChangedEvent) => this.onDefaultStackedToggle(ev)}" >
- +
- + @@ -163,82 +178,84 @@ export class ReportSettings extends WidgetSettings { - -
- - -
- ${when(isMultiAxis, () => html` -
- -
- `)} -
- ${max !== undefined ? html` - - ` : html` - - `} - -
-
- ${min !== undefined ? html` - - ` : html` - - `} - -
-
- - - ${when(isMultiAxis, () => { + ${when(this.widgetConfig.isChart, () => html` + +
+ + +
+ ${when(isMultiAxis, () => html` +
+ +
+ `)} +
+ ${max !== undefined ? html` + + ` : html` + + `} + +
+
+ ${min !== undefined ? html` + + ` : html` + + `} + +
+
+ + + ${when(isMultiAxis, () => { const rightMin = this.widgetConfig.chartOptions.options?.scales?.y1?.min; const rightMax = this.widgetConfig.chartOptions.options?.scales?.y1?.max; return html` -
-
- -
-
- ${rightMax !== undefined ? html` - - ` : html` - - `} - -
-
- ${rightMin !== undefined ? html` - - ` : html` - - `} - -
-
- ` - })} -
-
+
+
+ +
+
+ ${rightMax !== undefined ? html` + + ` : html` + + `} + +
+
+ ${rightMin !== undefined ? html` + + ` : html` + + `} + +
+
+ ` + }) } +
+
+ `)}
`; } @@ -272,7 +289,8 @@ export class ReportSettings extends WidgetSettings { // Also update the WidgetConfig attributeRefs field as usual protected onAttributesSelect(ev: AttributesSelectEvent) { const removedAttributeRefs = this.widgetConfig.attributeRefs.filter(ar => !ev.detail.attributeRefs.includes(ar)); - + console.log('evdetail:', ev.detail.attributeRefs) + console.log('removedAttributeRefs:', removedAttributeRefs) removedAttributeRefs.forEach(raf => { this.removeFromAttributeSettings(raf); this.removeFromColorPickedAttributes(raf); From f65f027e3c6e3aca90eda3096a3005659a849602 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Tue, 22 Apr 2025 16:17:36 +0200 Subject: [PATCH 14/15] revised timecontrols, decimals added --- ui/component/or-attribute-report/src/index.ts | 26 ++++++++++++------- .../src/settings/report-settings.ts | 15 +++++++++-- .../src/widgets/report-widget.ts | 6 +++-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index d8bd10eaf2..55e1925389 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -448,12 +448,18 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { @property() public timeWindowKey?: string; + @property() + public isCustomWindow?: boolean = false; + @property() public denseLegend: boolean = false; @property() public isChart: boolean = false; + @property() + public decimals: number = 2; + @property() protected _loading: boolean = false; @@ -755,7 +761,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { ${this.timestampControls ? html` ${getContentWithMenuTemplate( - html``, + html``, this.timePrefixOptions.map((option) => ({ value: option } as ListItem)), this.timePrefixKey, (value: string | string[]) => { @@ -769,7 +775,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { )} ${getContentWithMenuTemplate( - html``, + html``, Array.from(this.timeWindowOptions!.keys()).map((key) => ({ value: key } as ListItem)), this.timeWindowKey, (value: string | string[]) => { @@ -782,11 +788,11 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { true )} - + - - - + + + ` : html` `} @@ -981,6 +987,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } protected _openTimeDialog(startTimestamp?: number, endTimestamp?: number) { + this.isCustomWindow = true; const startRef: Ref = createRef(); const endRef: Ref = createRef(); const dialog = showDialog(new OrMwcDialog() @@ -1122,7 +1129,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } - protected _shiftTimeframe(currentStart: Date, timeWindowSelected: string, direction: string) { + protected _shiftTimeframe(currentStart: Date, currentEnd: Date, timeWindowSelected: string, direction: string) { const timeWindow = this.timeWindowOptions!.get(timeWindowSelected); if (!timeWindow) { @@ -1132,7 +1139,8 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { const [unit, value] = timeWindow; let newStart = moment(currentStart); direction === "previous" ? newStart.subtract(value, unit as moment.unitOfTime.DurationConstructor) : newStart.add(value, unit as moment.unitOfTime.DurationConstructor); - let newEnd = moment(newStart).add(value, unit as moment.unitOfTime.DurationConstructor); + let newEnd = moment(currentEnd) + direction === "previous" ? newEnd.subtract(value, unit as moment.unitOfTime.DurationConstructor) : newEnd.add(value, unit as moment.unitOfTime.DurationConstructor); this.timeframe = [newStart.toDate(), newEnd.toDate()]; } @@ -1322,7 +1330,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { .filter(value => value.y !== null && value.y !== undefined) .map(point => ({x: point.x, y: point.y} as ValueDatapoint)) - dataset.data = data.map(point => [moment(point.x).format(intervalArr[2]), point.y]); + dataset.data = data.map(point => [moment(point.x).format(intervalArr[2]), +point.y.toFixed(this.decimals)]); } diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts index 09135c0e7e..5b1d9ebf4c 100644 --- a/ui/component/or-dashboard-builder/src/settings/report-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -79,7 +79,7 @@ export class ReportSettings extends WidgetSettings { ${when(isOnRightAxis, () => html` `)} - item.data)}> + ` } @@ -174,6 +174,12 @@ export class ReportSettings extends WidgetSettings { @or-mwc-input-changed="${(ev: OrInputChangedEvent) => this.onIsChartToggle(ev)}" >
+ +
+ +
@@ -303,7 +309,7 @@ export class ReportSettings extends WidgetSettings { protected removeFromAttributeSettings(attributeRef: AttributeRef) { const settings = this.widgetConfig.attributeSettings; (Object.keys(settings) as (keyof typeof settings)[]).forEach(key => { - settings[key] = settings[key].filter((ar: AttributeRef) => ar.id !== attributeRef.id || ar.name !== attributeRef.name); + settings[key] = settings[key].filter((ar: AttributeRef) => !(ar.id === attributeRef.id && ar.name === attributeRef.name)); }); } @@ -483,4 +489,9 @@ export class ReportSettings extends WidgetSettings { this.setAxisMinMaxValue(axis, type, (ev.detail.value ? (type === 'min' ? 0 : 100) : undefined)); } + protected onDecimalsChange(ev: OrInputChangedEvent) { + this.widgetConfig.decimals = ev.detail.value; + this.notifyConfigUpdate(); + } + } diff --git a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts index d8bea44f10..530c3d72cc 100644 --- a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts @@ -35,6 +35,7 @@ export interface ReportWidgetConfig extends WidgetConfig { defaultStacked: boolean; }; isChart: boolean; + decimals: number; } function getDefaultTimeWindowOptions(): Map { @@ -109,7 +110,8 @@ function getDefaultWidgetConfig(): ReportWidgetConfig { showToolBox: false, defaultStacked: false, }, - isChart: true + isChart: true, + decimals: 2 }; } @@ -238,7 +240,7 @@ export class ReportWidget extends OrAssetWidget { .timePrefixKey="${this.widgetConfig?.defaultTimePrefixKey}" .timeWindowKey="${this.widgetConfig?.defaultTimeWindowKey}" .datapointQuery="${this.datapointQuery}" .chartOptions="${this.widgetConfig?.chartOptions}" - .isChart="${this.widgetConfig?.isChart}" + .isChart="${this.widgetConfig?.isChart}" .decimals="${this.widgetConfig?.decimals}" style="height: 100%" > `; From 7e0e9cc23435453ceb64ef32a79a4adb692368bf Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Thu, 24 Apr 2025 13:38:15 +0200 Subject: [PATCH 15/15] set ordering of methods, css changes, removed dataprovider, removed console logs --- ui/app/shared/locales/en/or.json | 12 +++- ui/component/or-attribute-report/src/index.ts | 68 +++++++++---------- .../src/settings/report-settings.ts | 10 +-- .../src/widgets/report-widget.ts | 9 ++- 4 files changed, 47 insertions(+), 52 deletions(-) diff --git a/ui/app/shared/locales/en/or.json b/ui/app/shared/locales/en/or.json index 758f025d3a..ab90d03bb5 100644 --- a/ui/app/shared/locales/en/or.json +++ b/ui/app/shared/locales/en/or.json @@ -170,8 +170,8 @@ "methodDeltaAttributes": "Difference", "methodMedianAttributes": "Median", "methodModeAttributes": "Mode", - "methodSumAttributes": "Datapoints Sum", - "methodCountAttributes": "Datapoint Count", + "methodSumAttributes": "Sum", + "methodCountAttributes": "Count", "addAttribute": "Add attribute", "selectAttributes": "Select attributes", "selectAttribute": "Select attribute", @@ -773,7 +773,8 @@ "center": "Center", "imageUrl": "Image URL", "noImageSelected": "No image selected", - "noData": "No data" + "noData": "No data", + "noDataOrMethod": "No data or no method per interval selected for selected attributes" }, "south": "South", "east": "East", @@ -845,6 +846,11 @@ "AVG": "Average (mean)", "MIN": "Minimum", "MAX": "Maximum", + "DELTA":"Difference", + "COUNT": "Count", + "MEDIAN": "Median", + "MODE": "Mode", + "SUM": "Sum", "alarm": { "": "Alarm", "alarm_plural": "Alarms", diff --git a/ui/component/or-attribute-report/src/index.ts b/ui/component/or-attribute-report/src/index.ts index 55e1925389..54cf0a2b52 100644 --- a/ui/component/or-attribute-report/src/index.ts +++ b/ui/component/or-attribute-report/src/index.ts @@ -195,12 +195,13 @@ const style = css` flex-direction: row; margin: 0; border-top: 3px solid var(--or-app-color2); + justify-content: center; } #attribute-list { overflow: hidden auto; min-height: 50px; - flex: 1 1 0; + max-height: 120px; width: 100%; display: grid; grid-template-columns: repeat(2, 1fr); @@ -329,7 +330,6 @@ const style = css` @media screen and (max-width: 1280px) { #chart-container { - max-height: 330px; } } @@ -397,9 +397,6 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { methodCountAttributes: [] as AttributeRef[] }; - @property() - public dataProvider?: () => Promise<[]> - @property({type: Array}) public colors: string[] = ["#3869B1", "#DA7E30", "#3F9852", "#CC2428", "#6B4C9A", "#922427", "#958C3D", "#535055"]; @@ -519,7 +516,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } const reloadData = changedProperties.has('colorPickedAttributes') || changedProperties.has("datapointQuery") || changedProperties.has("timeframe") || changedProperties.has("timePrefixKey") || changedProperties.has("timeWindowKey")|| - changedProperties.has("attributeSettings") || changedProperties.has("assetAttributes") || changedProperties.has("realm") || changedProperties.has("dataProvider"); + changedProperties.has("attributeSettings") || changedProperties.has("assetAttributes") || changedProperties.has("realm"); if (reloadData) { this._data = undefined; @@ -691,7 +688,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { `)} ${when(this._data?.every(entry => entry.data.length === 0), () => html`
- +
`)} ${when(this.isChart, () => html` @@ -725,9 +722,10 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { const label = Util.getAttributeLabel(attr, descriptors[0], asset!.type, true); const axisNote = (this.attributeSettings.rightAxisAttributes.find(ar => asset!.id === ar.id && attr.name === ar.name)) ? i18next.t('right') : undefined; const bgColor = ( color ?? this.colors[colourIndex] ) || ""; - //Find which calculation methods are active + //Find which aggregation methods are active const methodList: { data: string | undefined; }[] = Object.entries(this.attributeSettings) .filter(([key]) => key.includes('method')) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) .map(([key, attributeRefs]) => { const isActive = attributeRefs.some( (ref: AttributeRef) => @@ -1167,7 +1165,7 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { } protected async _loadData() { - if ( this._data || !this.assetAttributes || !this.assets || (this.assets.length === 0 && !this.dataProvider) || (this.assetAttributes.length === 0 && !this.dataProvider) || !this.datapointQuery) { + if ( this._data || !this.assetAttributes || !this.assets || (this.assets.length === 0) || (this.assetAttributes.length === 0) || !this.datapointQuery) { return; } @@ -1189,11 +1187,6 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { let promises; try { - if(this.dataProvider) { - await this.dataProvider().then((dataset) => { - dataset.forEach((set) => { data.push(set); }); - }); - } else { this._dataAbortController = new AbortController(); promises = this.assetAttributes.map(async ([assetIndex, attribute], index) => { @@ -1209,18 +1202,18 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { // Map calculation methods to their corresponding attribute arrays and formulas const methodMapping: { [key: string]: { active: boolean; formula: AssetDatapointIntervalQueryFormula } } = { - AVG: { active: !!this.attributeSettings.methodAvgAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.AVG }, - MIN: { active: !!this.attributeSettings.methodMinAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MIN }, - MAX: { active: !!this.attributeSettings.methodMaxAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MAX }, - DELTA: { active: !!this.attributeSettings.methodDeltaAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.DELTA }, - //MEDIAN: { active: !!this.attributeSettings.methodMedianAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MEDIAN }, - //MODE: { active: !!this.attributeSettings.methodModeAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MODE }, - //SUM: { active: !!this.attributeSettings.methodSumAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.SUM }, - //COUNT: { active: !!this.attributeSettings.methodCountAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.COUNT } + AVG: { active: !!this.attributeSettings.methodAvgAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.AVG }, + //COUNT: { active: !!this.attributeSettings.methodCountAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.COUNT }, + DELTA: { active: !!this.attributeSettings.methodDeltaAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.DELTA }, + MAX: { active: !!this.attributeSettings.methodMaxAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MAX }, + //MEDIAN: { active: !!this.attributeSettings.methodMedianAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MEDIAN }, + MIN: { active: !!this.attributeSettings.methodMinAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MIN }, + //MODE: { active: !!this.attributeSettings.methodModeAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.MODE }, + //SUM: { active: !!this.attributeSettings.methodSumAttributes.find(ar => ar.id === asset.id && ar.name === attribute.name), formula: AssetDatapointIntervalQueryFormula.SUM } }; + // Iterate over the mapping, make a dataset for every active method - for (const [key, value] of Object.entries(methodMapping)) { - console.log(`Key: ${key}, Active: ${value.active}, Formula: ${value.formula}`); + for (const [key, value] of (Object.entries(methodMapping)).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))) { if (value.active) { //Initiate query Attribute Data let dataset = await this._loadAttributeData(asset, attribute, color ?? this.colors[colourIndex], this._startOfPeriod!, this._endOfPeriod!, value.formula, asset.name + " " + label + " | " + i18next.t(value.formula), options, unit); @@ -1237,14 +1230,13 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { }); - } + if(promises) { await Promise.all(promises); } this._data = data; - console.log("this._data",this._data); //Load data into table format if (!this.isChart && this._data![0]) { @@ -1294,17 +1286,19 @@ export class OrAttributeReport extends translate(i18next)(LitElement) { // @ts-ignore valueFormatter: value => value + unit }, - //label: { - // show: true, - // align: 'left', - // verticalAlign: 'middle', - // position: 'insideBottom', - // rotate: '90', - // distance: 15, - // formatter: (params: { dataIndex: number; value: number }): string => { - // // Show labels only for the first index (index 0) - // return params.dataIndex === 0 ? `${formula}` : ''; //Or make it i18next.t(formula) to display longer text - // }} + label: { + show: true, + align: 'left', + verticalAlign: 'middle', + position: 'top', + fontStyle: 'italic', + fontSize: 10, + rotate: '90', + distance: 15, + formatter: (params: { dataIndex: number; value: number }): string => { + // Show labels only for the first index (index 0) + return params.dataIndex === 0 ? `${formula}` : ''; //Or make it i18next.t(formula) to display longer text + }} } diff --git a/ui/component/or-dashboard-builder/src/settings/report-settings.ts b/ui/component/or-dashboard-builder/src/settings/report-settings.ts index 5b1d9ebf4c..7f2e3fe13d 100644 --- a/ui/component/or-dashboard-builder/src/settings/report-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/report-settings.ts @@ -62,6 +62,7 @@ export class ReportSettings extends WidgetSettings { //Find which calculation methods are active const methodList = Object.entries(this.widgetConfig.attributeSettings) .filter(([key]) => key.includes('method')) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) .reduce((activeKeys, [key, attributeRefs]) => { const isActive = attributeRefs.some( (ref: AttributeRef) => ref.id === asset?.id && ref.name === attribute.name @@ -69,7 +70,6 @@ export class ReportSettings extends WidgetSettings { if (isActive) { activeKeys.push(i18next.t(key)); } - console.log('activeKeys:', activeKeys) return activeKeys; }, [] as any[]); @@ -295,8 +295,6 @@ export class ReportSettings extends WidgetSettings { // Also update the WidgetConfig attributeRefs field as usual protected onAttributesSelect(ev: AttributesSelectEvent) { const removedAttributeRefs = this.widgetConfig.attributeRefs.filter(ar => !ev.detail.attributeRefs.includes(ar)); - console.log('evdetail:', ev.detail.attributeRefs) - console.log('removedAttributeRefs:', removedAttributeRefs) removedAttributeRefs.forEach(raf => { this.removeFromAttributeSettings(raf); this.removeFromColorPickedAttributes(raf); @@ -309,7 +307,7 @@ export class ReportSettings extends WidgetSettings { protected removeFromAttributeSettings(attributeRef: AttributeRef) { const settings = this.widgetConfig.attributeSettings; (Object.keys(settings) as (keyof typeof settings)[]).forEach(key => { - settings[key] = settings[key].filter((ar: AttributeRef) => !(ar.id === attributeRef.id && ar.name === attributeRef.name)); + settings[key] = settings[key].filter((ar: AttributeRef) => (ar.id !== attributeRef.id && ar.name !== attributeRef.name)); }); } @@ -364,9 +362,9 @@ export class ReportSettings extends WidgetSettings { protected openAlgorithmMethodsDialog(attributeRef: AttributeRef) { - console.log('attributeRef:', attributeRef); const methodList: ListItem[] = Object.entries(this.widgetConfig.attributeSettings) .filter(([key]) => key.includes('method')) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) .map(([key, attributeRefs]) => { const isActive = attributeRefs.some( (ref: AttributeRef) => @@ -381,7 +379,6 @@ export class ReportSettings extends WidgetSettings { }; }); let selected: ListItem[] = []; - console.log('InputList:', methodList); showDialog(new OrMwcDialog() .setContent(html` @@ -406,7 +403,6 @@ export class ReportSettings extends WidgetSettings { actionName: "ok", action: () => { // Check which settings need updating - console.log('Selected:', selected) const changedMethods = methodList.filter(input => { const selectedItem = selected.find(selected => selected.value === input.value); return (!selectedItem && input.data !== undefined) || diff --git a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts index 530c3d72cc..5d285dd8c1 100644 --- a/ui/component/or-dashboard-builder/src/widgets/report-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/report-widget.ts @@ -15,9 +15,9 @@ export interface ReportWidgetConfig extends WidgetConfig { colorPickedAttributes: Array<{ attributeRef: AttributeRef; color: string }>; attributeSettings: { rightAxisAttributes: AttributeRef[], - methodMaxAttributes: AttributeRef[], - methodMinAttributes: AttributeRef[], methodAvgAttributes: AttributeRef[], + methodMinAttributes: AttributeRef[], + methodMaxAttributes: AttributeRef[], methodDeltaAttributes: AttributeRef[], methodMedianAttributes: AttributeRef[], methodModeAttributes: AttributeRef[], @@ -74,9 +74,9 @@ function getDefaultWidgetConfig(): ReportWidgetConfig { colorPickedAttributes: [], attributeSettings: { rightAxisAttributes: [], - methodMaxAttributes: [], - methodMinAttributes: [], methodAvgAttributes: [], + methodMinAttributes: [], + methodMaxAttributes: [], methodDeltaAttributes: [], methodMedianAttributes: [], methodModeAttributes: [], @@ -181,7 +181,6 @@ export class ReportWidget extends OrAssetWidget { if(changedProps.has('widgetConfig') && this.widgetConfig) { this.datapointQuery = this.widgetConfig.datapointQuery; - console.log("WidgetConfig:", this.widgetConfig) const attributeRefs = this.widgetConfig.attributeRefs; if(attributeRefs.length === 0) {