diff --git a/libs/api-client-tiger/api/api-client-tiger.api.md b/libs/api-client-tiger/api/api-client-tiger.api.md index 60ffd6e2196..8f06822498e 100644 --- a/libs/api-client-tiger/api/api-client-tiger.api.md +++ b/libs/api-client-tiger/api/api-client-tiger.api.md @@ -6819,6 +6819,8 @@ export interface ITigerClient { explain: ReturnType; // (undocumented) export: ReturnType; + // @beta (undocumented) + forecast: ReturnType; // (undocumented) labelElements: ReturnType; // (undocumented) @@ -14189,6 +14191,9 @@ export const tigerExecutionResultClientFactory: (axios: AxiosInstance) => Pick ExportActionsApiInterface; +// @beta +export const tigerForecastClientFactory: (axios: AxiosInstance) => Pick; + // @public (undocumented) export const tigerLabelElementsClientFactory: (axios: AxiosInstance) => Pick; diff --git a/libs/api-client-tiger/src/client.ts b/libs/api-client-tiger/src/client.ts index 4e70101418c..0914ed1bcc1 100644 --- a/libs/api-client-tiger/src/client.ts +++ b/libs/api-client-tiger/src/client.ts @@ -50,6 +50,7 @@ import { import { tigerValidDescendantsClientFactory } from "./validDescendants.js"; import { tigerResultClientFactory, ResultActionsApiInterface } from "./result.js"; import { tigerUserManagementClientFactory } from "./userManagement.js"; +import { tigerForecastClientFactory } from "./forecast.js"; export { tigerExecutionClientFactory, @@ -67,6 +68,7 @@ export { tigerExportClientFactory, tigerResultClientFactory, tigerUserManagementClientFactory, + tigerForecastClientFactory, MetadataConfiguration, MetadataConfigurationParameters, MetadataBaseApi, @@ -109,6 +111,10 @@ export interface ITigerClient { export: ReturnType; result: ReturnType; userManagement: ReturnType; + /** + * @beta + */ + forecast: ReturnType; /** * Updates tiger client to send the provided API TOKEN in `Authorization` header of all @@ -140,6 +146,7 @@ export const tigerClientFactory = (axios: AxiosInstance): ITigerClient => { const exportFactory = tigerExportClientFactory(axios); const result = tigerResultClientFactory(axios); const userManagement = tigerUserManagementClientFactory(axios); + const forecast = tigerForecastClientFactory(axios); return { axios, @@ -161,5 +168,6 @@ export const tigerClientFactory = (axios: AxiosInstance): ITigerClient => { setAxiosAuthorizationToken(axios, token); }, export: exportFactory, + forecast, }; }; diff --git a/libs/api-client-tiger/src/forecast.ts b/libs/api-client-tiger/src/forecast.ts new file mode 100644 index 00000000000..24c4b080fcd --- /dev/null +++ b/libs/api-client-tiger/src/forecast.ts @@ -0,0 +1,11 @@ +// (C) 2019-2024 GoodData Corporation +import { AxiosInstance } from "axios"; +import { ActionsApi, ActionsApiInterface } from "./generated/afm-rest-api/index.js"; + +/** + * Tiger forecast client factory + * @beta + */ +export const tigerForecastClientFactory = ( + axios: AxiosInstance, +): Pick => new ActionsApi(undefined, "", axios); diff --git a/libs/api-client-tiger/src/index.ts b/libs/api-client-tiger/src/index.ts index ac303c851d8..55e9b0f30e6 100644 --- a/libs/api-client-tiger/src/index.ts +++ b/libs/api-client-tiger/src/index.ts @@ -284,10 +284,10 @@ export { GdStorageFile, GdStorageFileTypeEnum, ImportCsvRequest, + ImportCsvResponse, ImportCsvRequestTable, ImportCsvRequestTableSource, ImportCsvRequestTableSourceConfig, - ImportCsvResponse, OrganizationCacheSettings, OrganizationCacheUsage, OrganizationCurrentCacheUsage, diff --git a/libs/sdk-backend-base/api/sdk-backend-base.api.md b/libs/sdk-backend-base/api/sdk-backend-base.api.md index 4b4bb440d2d..724b0f351e1 100644 --- a/libs/sdk-backend-base/api/sdk-backend-base.api.md +++ b/libs/sdk-backend-base/api/sdk-backend-base.api.md @@ -56,6 +56,8 @@ import { IExportResult } from '@gooddata/sdk-backend-spi'; import { IFactMetadataObject } from '@gooddata/sdk-model'; import { IFilter } from '@gooddata/sdk-model'; import { IFilterContextDefinition } from '@gooddata/sdk-model'; +import { IForecastConfig } from '@gooddata/sdk-backend-spi'; +import { IForecastResult } from '@gooddata/sdk-backend-spi'; import { IGetDashboardOptions } from '@gooddata/sdk-backend-spi'; import { IGetDashboardPluginOptions } from '@gooddata/sdk-backend-spi'; import { IGetScheduledMailOptions } from '@gooddata/sdk-backend-spi'; @@ -136,6 +138,8 @@ export type AnalyticalBackendCallbacks = { failedResultReadAll?: (error: any, executionId: string) => void; successfulResultReadWindow?: (offset: number[], size: number[], dataView: IDataView, executionId: string) => void; failedResultReadWindow?: (offset: number[], size: number[], error: any, executionId: string) => void; + successfulForecastResultReadAll?: (forecastResult: IForecastResult, executionId: string) => void; + failedForecastResultReadAll?: (error: any, executionId: string) => void; }; // @public @@ -398,6 +402,8 @@ export abstract class DecoratedExecutionResult implements IExecutionResult { // (undocumented) readAll(): Promise; // (undocumented) + readForecastAll(config: IForecastConfig): Promise; + // (undocumented) readWindow(offset: number[], size: number[]): Promise; // (undocumented) transform(): IPreparedExecution; diff --git a/libs/sdk-backend-base/src/cachingBackend/index.ts b/libs/sdk-backend-base/src/cachingBackend/index.ts index 24616cdc6bf..abb3382ee71 100644 --- a/libs/sdk-backend-base/src/cachingBackend/index.ts +++ b/libs/sdk-backend-base/src/cachingBackend/index.ts @@ -10,6 +10,8 @@ import { IElementsQueryResult, IExecutionFactory, IExecutionResult, + IForecastConfig, + IForecastResult, IPreparedExecution, ISecuritySettingsService, IUserWorkspaceSettings, @@ -199,6 +201,8 @@ function windowKey(offset: number[], size: number[]): string { class WithExecutionResultCaching extends DecoratedExecutionResult { private allData: Promise | undefined; + private allForecastConfig: IForecastConfig | undefined; + private allForecastData: Promise | undefined; private windows: LRUCache> | undefined; constructor( @@ -224,6 +228,19 @@ class WithExecutionResultCaching extends DecoratedExecutionResult { return this.allData; }; + public readForecastAll = (config: IForecastConfig): Promise => { + // TODO: enable forecasting caching size configuration + if (!this.allForecastData || this.allForecastConfig !== config) { + this.allForecastConfig = config; + this.allForecastData = super.readForecastAll(config).catch((e) => { + this.allForecastData = undefined; + throw e; + }); + } + + return this.allForecastData; + }; + public readWindow = (offset: number[], size: number[]): Promise => { if (!this.windows) { return super.readWindow(offset, size); diff --git a/libs/sdk-backend-base/src/customBackend/execution.ts b/libs/sdk-backend-base/src/customBackend/execution.ts index 80271395dde..cb3426221eb 100644 --- a/libs/sdk-backend-base/src/customBackend/execution.ts +++ b/libs/sdk-backend-base/src/customBackend/execution.ts @@ -1,4 +1,4 @@ -// (C) 2019-2023 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import { AbstractExecutionFactory } from "../toolkit/execution.js"; import { @@ -27,6 +27,7 @@ import { NotImplemented, IExplainProvider, ExplainType, + IForecastResult, } from "@gooddata/sdk-backend-spi"; import isEqual from "lodash/isEqual.js"; import { @@ -160,6 +161,10 @@ class CustomExecutionResult implements IExecutionResult { }); }; + public readForecastAll(): Promise { + throw new NotSupported("Forecasting is not supported by the custom backend."); + } + public readWindow = (offset: number[], size: number[]): Promise => { return this.state.authApiCall((client) => { if (!this.config.dataProvider) { diff --git a/libs/sdk-backend-base/src/decoratedBackend/execution.ts b/libs/sdk-backend-base/src/decoratedBackend/execution.ts index 904140c4f57..8fd0f5acbdc 100644 --- a/libs/sdk-backend-base/src/decoratedBackend/execution.ts +++ b/libs/sdk-backend-base/src/decoratedBackend/execution.ts @@ -1,6 +1,7 @@ -// (C) 2019-2023 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import { IDataView, + IForecastView, IExecutionFactory, IExecutionResult, IExportConfig, @@ -9,6 +10,8 @@ import { ExplainConfig, IExplainProvider, ExplainType, + IForecastResult, + IForecastConfig, } from "@gooddata/sdk-backend-spi"; import { IAttributeOrMeasure, @@ -171,6 +174,10 @@ export abstract class DecoratedExecutionResult implements IExecutionResult { return this.decorated.readAll(); } + public readForecastAll(config: IForecastConfig): Promise { + return this.decorated.readForecastAll(config); + } + public readWindow(offset: number[], size: number[]): Promise { return this.decorated.readWindow(offset, size); } @@ -205,9 +212,13 @@ export abstract class DecoratedDataView implements IDataView { public definition: IExecutionDefinition; public result: IExecutionResult; public warnings?: IResultWarning[]; + public forecastConfig?: IForecastConfig; + public forecastResult?: IForecastResult; constructor(private readonly decorated: IDataView, result?: IExecutionResult) { this.result = result ?? decorated.result; + this.forecastConfig = decorated.forecastConfig; + this.forecastResult = decorated.forecastResult; this.count = decorated.count; this.data = decorated.data; @@ -227,4 +238,12 @@ export abstract class DecoratedDataView implements IDataView { public fingerprint(): string { return this.decorated.fingerprint(); } + + public withForecast(config: IForecastConfig, result: IForecastResult): IDataView { + return this.decorated.withForecast(config, result); + } + + public forecast(): IForecastView { + return this.decorated.forecast(); + } } diff --git a/libs/sdk-backend-base/src/dummyBackend/index.ts b/libs/sdk-backend-base/src/dummyBackend/index.ts index c6b08041d6f..895a946e392 100644 --- a/libs/sdk-backend-base/src/dummyBackend/index.ts +++ b/libs/sdk-backend-base/src/dummyBackend/index.ts @@ -63,6 +63,9 @@ import { IWorkspaceExportDefinitionsService, IDataFiltersService, IWorkspaceLogicalModelService, + IForecastResult, + IForecastConfig, + IForecastView, } from "@gooddata/sdk-backend-spi"; import { defFingerprint, @@ -256,6 +259,17 @@ export function dummyDataView( equals(other: IDataView): boolean { return fp === other.fingerprint(); }, + forecast(): IForecastView { + return { + headerItems: [], + prediction: [], + low: [], + high: [], + }; + }, + withForecast(_config: IForecastConfig, _result: IForecastResult): IDataView { + throw new NotSupported("not supported"); + }, }; } @@ -385,6 +399,9 @@ function dummyExecutionResult( readWindow(_1: number[], _2: number[]): Promise { return dummyRead(); }, + readForecastAll(): Promise { + throw new NotSupported("Forecasting is not supported in dummy backend."); + }, fingerprint(): string { return fp; }, diff --git a/libs/sdk-backend-base/src/eventingBackend/index.ts b/libs/sdk-backend-base/src/eventingBackend/index.ts index be85ef4405e..718e85422c1 100644 --- a/libs/sdk-backend-base/src/eventingBackend/index.ts +++ b/libs/sdk-backend-base/src/eventingBackend/index.ts @@ -1,11 +1,13 @@ -// (C) 2007-2021 GoodData Corporation +// (C) 2007-2024 GoodData Corporation import isEmpty from "lodash/isEmpty.js"; import { v4 as uuid } from "uuid"; import { IAnalyticalBackend, IDataView, IExecutionResult, + IForecastResult, IPreparedExecution, + IForecastConfig, } from "@gooddata/sdk-backend-spi"; import { IExecutionDefinition } from "@gooddata/sdk-model"; @@ -75,6 +77,24 @@ class WithExecutionResultEventing extends DecoratedExecutionResult { }); }; + public readForecastAll(config: IForecastConfig): Promise { + const { successfulForecastResultReadAll, failedForecastResultReadAll } = this.callbacks; + + const promisedDataView = super.readForecastAll(config); + + return promisedDataView + .then((res) => { + successfulForecastResultReadAll?.(res, this.executionId); + + return res; + }) + .catch((e) => { + failedForecastResultReadAll?.(e, this.executionId); + + throw e; + }); + } + public readWindow = (offset: number[], size: number[]): Promise => { const { successfulResultReadWindow, failedResultReadWindow } = this.callbacks; @@ -166,6 +186,22 @@ export type AnalyticalBackendCallbacks = { * @param executionId - unique ID assigned to each execution that can be used to correlate individual events that "belong" to the same execution */ failedResultReadWindow?: (offset: number[], size: number[], error: any, executionId: string) => void; + + /** + * Called success when forecast results read + * + * @param forecastResult - forecast result + * @param executionId - unique ID assigned to each execution that can be used to correlate individual events that "belong" to the same execution + */ + successfulForecastResultReadAll?: (forecastResult: IForecastResult, executionId: string) => void; + + /** + * Called when forecast results read failed + * + * @param error - error from the underlying backend, contractually this should be an instance of AnalyticalBackendError + * @param executionId - unique ID assigned to each execution that can be used to correlate individual events that "belong" to the same execution + */ + failedForecastResultReadAll?: (error: any, executionId: string) => void; }; /** diff --git a/libs/sdk-backend-base/src/normalizingBackend/index.ts b/libs/sdk-backend-base/src/normalizingBackend/index.ts index 781c0d9b6f7..aa73e1ebd44 100644 --- a/libs/sdk-backend-base/src/normalizingBackend/index.ts +++ b/libs/sdk-backend-base/src/normalizingBackend/index.ts @@ -1,8 +1,9 @@ -// (C) 2007-2023 GoodData Corporation +// (C) 2007-2024 GoodData Corporation import { IAnalyticalBackend, IDataView, + IForecastView, IExecutionFactory, IExecutionResult, IExportConfig, @@ -11,6 +12,8 @@ import { NotSupported, isNoDataError, NoDataError, + IForecastResult, + IForecastConfig, } from "@gooddata/sdk-backend-spi"; import { decoratedBackend } from "../decoratedBackend/index.js"; import { DecoratedExecutionFactory, DecoratedPreparedExecution } from "../decoratedBackend/execution.js"; @@ -151,6 +154,10 @@ class DenormalizingExecutionResult implements IExecutionResult { .catch(this.handleDataViewError); }; + public readForecastAll(config: IForecastConfig): Promise { + return this.normalizedResult.readForecastAll(config); + } + public equals = (other: IExecutionResult): boolean => { return this._fingerprint === other.fingerprint(); }; @@ -181,6 +188,8 @@ class DenormalizingExecutionResult implements IExecutionResult { class DenormalizedDataView implements IDataView { public readonly definition: IExecutionDefinition; public readonly result: IExecutionResult; + public readonly forecastConfig?: IForecastConfig; + public readonly forecastResult?: IForecastResult; public readonly data: DataValue[][] | DataValue[]; public readonly headerItems: IResultHeader[][][]; @@ -191,13 +200,21 @@ class DenormalizedDataView implements IDataView { public readonly totalTotals: DataValue[][][] | undefined; private readonly _fingerprint: string; + private readonly _denormalizer: Denormalizer; constructor( result: DenormalizingExecutionResult, private readonly normalizedDataView: IDataView, denormalizer: Denormalizer, + forecastConfig?: IForecastConfig, + forecastResult?: IForecastResult, ) { + this._denormalizer = denormalizer; + this.result = result; + this.forecastConfig = forecastConfig; + this.forecastResult = forecastResult; + this.definition = this.result.definition; this.count = cloneDeep(this.normalizedDataView.count); this.data = cloneDeep(this.normalizedDataView.data); @@ -217,6 +234,25 @@ class DenormalizedDataView implements IDataView { public fingerprint = (): string => { return this._fingerprint; }; + + public forecast(): IForecastView { + const data = this.normalizedDataView.forecast(); + + return { + ...data, + headerItems: this._denormalizer.denormalizeHeaders(data.headerItems), + }; + } + + public withForecast(config: IForecastConfig, result: IForecastResult): IDataView { + return new DenormalizedDataView( + this.result as DenormalizingExecutionResult, + this.normalizedDataView.withForecast(config, result), + this._denormalizer, + config, + result, + ); + } } /** diff --git a/libs/sdk-backend-mockingbird/src/legacyRecordedBackend/index.ts b/libs/sdk-backend-mockingbird/src/legacyRecordedBackend/index.ts index 1b8e0aedc2e..79d869b8e59 100644 --- a/libs/sdk-backend-mockingbird/src/legacyRecordedBackend/index.ts +++ b/libs/sdk-backend-mockingbird/src/legacyRecordedBackend/index.ts @@ -47,6 +47,8 @@ import { IWorkspaceExportDefinitionsService, IDataFiltersService, IWorkspaceLogicalModelService, + IForecastResult, + IForecastView, } from "@gooddata/sdk-backend-spi"; import { defFingerprint, @@ -303,6 +305,17 @@ function recordedDataView( equals(other: IDataView): boolean { return fp === other.fingerprint(); }, + withForecast(): IDataView { + throw new NotSupported("not supported"); + }, + forecast(): IForecastView { + return { + headerItems: [], + low: [], + high: [], + prediction: [], + }; + }, }; } @@ -360,6 +373,9 @@ function recordedExecutionResult( readWindow(_1: number[], _2: number[]): Promise { return Promise.resolve(recordedDataView(definition, result, recording)); }, + readForecastAll(): Promise { + throw new NotSupported("Forecasting is not supported by the recorded backend."); + }, fingerprint(): string { return fp; }, diff --git a/libs/sdk-backend-mockingbird/src/recordedBackend/execution.ts b/libs/sdk-backend-mockingbird/src/recordedBackend/execution.ts index 537808afb1d..56c8b348498 100644 --- a/libs/sdk-backend-mockingbird/src/recordedBackend/execution.ts +++ b/libs/sdk-backend-mockingbird/src/recordedBackend/execution.ts @@ -11,6 +11,9 @@ import { NotSupported, IExplainProvider, ExplainType, + IForecastResult, + IForecastConfig, + IForecastView, } from "@gooddata/sdk-backend-spi"; import { defFingerprint, @@ -285,6 +288,10 @@ class RecordedExecutionResult implements IExecutionResult { return Promise.resolve(new RecordedDataView(this, this.definition, windowData, this.denormalizer)); }; + public readForecastAll(): Promise { + throw new NotSupported("Forecasting is not supported by the recorded backend."); + } + public transform = (): IPreparedExecution => { return this.executionFactory.forDefinition(this.definition); }; @@ -312,8 +319,10 @@ class RecordedDataView implements IDataView { constructor( public readonly result: IExecutionResult, public readonly definition: IExecutionDefinition, - recordedDataView: any, - denormalizer?: Denormalizer, + private readonly recordedDataView: any, + private readonly denormalizer?: Denormalizer, + public readonly forecastConfig?: IForecastConfig, + public readonly forecastResult?: IForecastResult, ) { this.data = recordedDataView.data; this.headerItems = denormalizer @@ -338,6 +347,28 @@ class RecordedDataView implements IDataView { public fingerprint = (): string => { return this._fp; }; + + public withForecast(config: IForecastConfig, result: IForecastResult): IDataView { + return new RecordedDataView( + this.result, + this.definition, + this.recordedDataView, + this.denormalizer, + config, + result, + ); + } + + forecast(): IForecastView { + return ( + this.recordedDataView.forecast ?? { + headerItems: [], + low: [], + high: [], + prediction: [], + } + ); + } } // diff --git a/libs/sdk-backend-spi/api/sdk-backend-spi.api.md b/libs/sdk-backend-spi/api/sdk-backend-spi.api.md index f59aad3b95e..95629dcbf26 100644 --- a/libs/sdk-backend-spi/api/sdk-backend-spi.api.md +++ b/libs/sdk-backend-spi/api/sdk-backend-spi.api.md @@ -390,6 +390,12 @@ export interface IDataView { readonly definition: IExecutionDefinition; equals(other: IDataView): boolean; fingerprint(): string; + // @beta + forecast(): IForecastView; + // @beta + readonly forecastConfig?: IForecastConfig; + // @beta + readonly forecastResult?: IForecastResult; readonly headerItems: IResultHeader[][][]; readonly offset: number[]; readonly result: IExecutionResult; @@ -397,6 +403,8 @@ export interface IDataView { readonly totals?: DataValue[][][]; readonly totalTotals?: DataValue[][][]; readonly warnings?: IResultWarning[]; + // @beta + withForecast(config: IForecastConfig, result?: IForecastResult): IDataView; } // @internal @@ -496,6 +504,8 @@ export interface IExecutionResult { export(options: IExportConfig): Promise; fingerprint(): string; readAll(): Promise; + // @beta + readForecastAll(config: IForecastConfig): Promise; readWindow(offset: number[], size: number[]): Promise; transform(): IPreparedExecution; } @@ -573,6 +583,39 @@ export interface IFilterElementsQuery { withOffset(offset: number): IFilterElementsQuery; } +// @beta (undocumented) +export interface IForecastConfig { + confidenceLevel: number; + forecastPeriod: number; + seasonal: boolean; +} + +// @beta (undocumented) +export interface IForecastResult { + // (undocumented) + attribute: string[]; + // (undocumented) + lowerBound: number[]; + // (undocumented) + origin: number[]; + // (undocumented) + prediction: number[]; + // (undocumented) + upperBound: number[]; +} + +// @beta +export interface IForecastView { + // (undocumented) + headerItems: IResultHeader[][][]; + // (undocumented) + high: DataValue[][]; + // (undocumented) + low: DataValue[][]; + // (undocumented) + prediction: DataValue[][]; +} + // @alpha export interface IGetDashboardOptions { exportId?: string; diff --git a/libs/sdk-backend-spi/src/index.ts b/libs/sdk-backend-spi/src/index.ts index 4f5848b5a4b..6be0deaa113 100644 --- a/libs/sdk-backend-spi/src/index.ts +++ b/libs/sdk-backend-spi/src/index.ts @@ -31,10 +31,13 @@ export { IPreparedExecution, IExecutionResult, IDataView, + IForecastView, ExplainConfig, IExplainResult, IExplainProvider, ExplainType, + IForecastResult, + IForecastConfig, } from "./workspace/execution/index.js"; export { IWorkspaceSettingsService } from "./workspace/settings/index.js"; diff --git a/libs/sdk-backend-spi/src/workspace/execution/index.ts b/libs/sdk-backend-spi/src/workspace/execution/index.ts index 18982c8171d..c973ef2c42a 100644 --- a/libs/sdk-backend-spi/src/workspace/execution/index.ts +++ b/libs/sdk-backend-spi/src/workspace/execution/index.ts @@ -1,4 +1,4 @@ -// (C) 2019-2023 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import { IAttributeOrMeasure, IBucket, @@ -17,6 +17,35 @@ import { } from "@gooddata/sdk-model"; import { IExportConfig, IExportResult } from "./export.js"; +/** + * @beta + */ +export interface IForecastConfig { + /** + * Forecast period in number of periods - e.g. 3 + */ + forecastPeriod: number; + /** + * Confidence level of the forecast in percents - e.g. 0.95 + */ + confidenceLevel: number; + /** + * Defines, whether the forecast should be seasonal. + */ + seasonal: boolean; +} + +/** + * @beta + */ +export interface IForecastResult { + attribute: string[]; + origin: number[]; + prediction: number[]; + lowerBound: number[]; + upperBound: number[]; +} + /** * Execution factory provides several methods to create a prepared execution from different types * of inputs. @@ -293,6 +322,12 @@ export interface IExecutionResult { */ readWindow(offset: number[], size: number[]): Promise; + /** + * Reads forecast for the execution result. + * @beta + */ + readForecastAll(config: IForecastConfig): Promise; + /** * Transforms this execution result - changing the result sorting, dimensionality and available * totals is possible through transformation. @@ -427,6 +462,18 @@ export interface IDataView { */ readonly result: IExecutionResult; + /** + * Configuration for the forecasting, if available. + * @beta + */ + readonly forecastConfig?: IForecastConfig; + + /** + * Forecasting result, if available. + * @beta + */ + readonly forecastResult?: IForecastResult; + /** * Result warnings. * @@ -456,4 +503,32 @@ export interface IDataView { * Thus, two data views on the same result, with same offset and limit will have the same fingerprint. */ fingerprint(): string; + + /** + * Return forecast data view. This object is empty if not `withForecast` was called + * @see IDataView.withForecast + * @beta + */ + forecast(): IForecastView; + + /** + * Adds forecast for this data view. + * + * @beta + * @param config - forecast configuration + * @param result - forecast result + * @returns new data view with forecasting enabled + */ + withForecast(config: IForecastConfig, result?: IForecastResult): IDataView; +} + +/** + * Represents a prediction, lower bound and upper bound for a forecast. + * @beta + */ +export interface IForecastView { + headerItems: IResultHeader[][][]; + prediction: DataValue[][]; + low: DataValue[][]; + high: DataValue[][]; } diff --git a/libs/sdk-backend-tiger/src/backend/features/feature.ts b/libs/sdk-backend-tiger/src/backend/features/feature.ts index 972a4fc1d08..a20050ad4cd 100644 --- a/libs/sdk-backend-tiger/src/backend/features/feature.ts +++ b/libs/sdk-backend-tiger/src/backend/features/feature.ts @@ -303,6 +303,13 @@ export function mapFeatures(features: FeaturesMap): Partial "BOOLEAN", FeatureFlagsValues.enableLabsSmartFunctions, ), + ...loadFeature( + features, + TigerFeaturesNames.EnableSmartFunctions, + "enableSmartFunctions", + "BOOLEAN", + FeatureFlagsValues.enableSmartFunctions, + ), ...loadFeature( features, TigerFeaturesNames.EnableKeyDriverAnalysis, diff --git a/libs/sdk-backend-tiger/src/backend/uiFeatures.ts b/libs/sdk-backend-tiger/src/backend/uiFeatures.ts index 41cfd3c447b..ce5b6f69e71 100644 --- a/libs/sdk-backend-tiger/src/backend/uiFeatures.ts +++ b/libs/sdk-backend-tiger/src/backend/uiFeatures.ts @@ -79,6 +79,7 @@ export enum TigerFeaturesNames { EnableOracleDataSource = "enableOracleDataSource", EnableAnalyticalCatalog = "enableAnalyticalCatalog", EnableAlerting = "enableAlerting", + EnableSmartFunctions = "enableSmartFunctions", EnableLabsSmartFunctions = "enableLabsSmartFunctions", EnableKeyDriverAnalysis = "enableKeyDriverAnalysis", EnableDataProfiling = "enableDataProfiling", @@ -131,6 +132,7 @@ export type ITigerFeatureFlags = { enableAnalyticalCatalog: typeof FeatureFlagsValues["enableAnalyticalCatalog"][number]; enableAlerting: typeof FeatureFlagsValues["enableAlerting"][number]; enableLabsSmartFunctions: typeof FeatureFlagsValues["enableLabsSmartFunctions"][number]; + enableSmartFunctions: typeof FeatureFlagsValues["enableSmartFunctions"][number]; enableKeyDriverAnalysis: typeof FeatureFlagsValues["enableKeyDriverAnalysis"][number]; enableDataProfiling: typeof FeatureFlagsValues["enableDataProfiling"][number]; enableFlexAi: typeof FeatureFlagsValues["enableFlexAi"][number]; @@ -182,6 +184,7 @@ export const DefaultFeatureFlags: ITigerFeatureFlags = { enableAnalyticalCatalog: false, enableAlerting: false, enableLabsSmartFunctions: false, + enableSmartFunctions: false, enableKeyDriverAnalysis: false, enableDataProfiling: false, enableFlexAi: false, @@ -237,6 +240,7 @@ export const FeatureFlagsValues = { enableAnalyticalCatalog: [true, false] as const, enableAlerting: [true, false] as const, enableLabsSmartFunctions: [true, false] as const, + enableSmartFunctions: [true, false] as const, enableKeyDriverAnalysis: [true, false] as const, enableDataProfiling: [true, false] as const, enableFlexAi: [true, false] as const, diff --git a/libs/sdk-backend-tiger/src/backend/workspace/execution/executionResult.ts b/libs/sdk-backend-tiger/src/backend/workspace/execution/executionResult.ts index 48a662f1dfd..756eeb937ea 100644 --- a/libs/sdk-backend-tiger/src/backend/workspace/execution/executionResult.ts +++ b/libs/sdk-backend-tiger/src/backend/workspace/execution/executionResult.ts @@ -18,6 +18,9 @@ import { NoDataError, UnexpectedError, TimeoutError, + IForecastConfig, + IForecastResult, + IForecastView, } from "@gooddata/sdk-backend-spi"; import { IExecutionDefinition, DataValue, IDimensionDescriptor, IResultHeader } from "@gooddata/sdk-model"; import SparkMD5 from "spark-md5"; @@ -26,9 +29,13 @@ import { transformExecutionResult } from "../../../convertors/fromBackend/afm/re import { DateFormatter } from "../../../convertors/fromBackend/dateFormatting/types.js"; import { TigerAuthenticatedCallGuard } from "../../../types/index.js"; import { transformGrandTotalData } from "../../../convertors/fromBackend/afm/GrandTotalsConverter.js"; -import { getTransformDimensionHeaders } from "../../../convertors/fromBackend/afm/DimensionHeaderConverter.js"; +import { + getTransformDimensionHeaders, + getTransformForecastHeaders, +} from "../../../convertors/fromBackend/afm/DimensionHeaderConverter.js"; import { resolveCustomOverride } from "./utils.js"; import { parseNameFromContentDisposition } from "../../../utils/downloadFile.js"; +import { transformForecastResult } from "../../../convertors/fromBackend/afm/forecast.js"; const TIGER_PAGE_SIZE_LIMIT = 1000; const DEFAULT_POLL_DELAY = 5000; @@ -88,6 +95,30 @@ export class TigerExecutionResult implements IExecutionResult { return this.asDataView(executionResultPromise); } + public async readForecastAll(forecastConfig: IForecastConfig): Promise { + const workspace = this.workspace; + const resultId = this.resultId; + + const forecast = await this.authCall((client) => + client.forecast + .forecast({ + forecastRequest: forecastConfig, + workspaceId: workspace, + resultId: resultId, + }) + .then(({ data }) => data), + ); + + return this.authCall((client) => + client.forecast + .forecastResult({ + workspaceId: workspace, + resultId: forecast.links.executionResult, + }) + .then(({ data }) => data), + ); + } + public async readWindow(offset: number[], size: number[]): Promise { const saneOffset = sanitizeOffset(offset); const saneSize = sanitizeSize(size); @@ -217,12 +248,27 @@ class TigerDataView implements IDataView { public readonly offset: number[]; public readonly result: IExecutionResult; public readonly totals?: DataValue[][][]; + public readonly forecastConfig?: IForecastConfig; + public readonly forecastResult?: IForecastResult; public readonly totalTotals?: DataValue[][][]; private readonly _fingerprint: string; + private readonly _execResult: ExecutionResult; + private readonly _dateFormatter: DateFormatter; - constructor(result: IExecutionResult, execResult: ExecutionResult, dateFormatter: DateFormatter) { + constructor( + result: IExecutionResult, + execResult: ExecutionResult, + dateFormatter: DateFormatter, + forecastConfig?: IForecastConfig, + forecastResult?: IForecastResult, + ) { this.result = result; this.definition = result.definition; + this.forecastConfig = forecastConfig; + this.forecastResult = forecastResult; + + this._execResult = execResult; + this._dateFormatter = dateFormatter; const transformDimensionHeaders = getTransformDimensionHeaders( result.dimensions, @@ -249,7 +295,9 @@ class TigerDataView implements IDataView { const totalTotals = grandTotalItem?.data as DataValue[][]; this.totalTotals = totalTotals ? [totalTotals] : undefined; - this._fingerprint = `${result.fingerprint()}/${this.offset.join(",")}-${this.count.join(",")}`; + this._fingerprint = `${result.fingerprint()}/${this.offset.join(",")}-${this.count.join(",")}/f:${ + this.forecastConfig?.forecastPeriod + },${this.forecastConfig?.confidenceLevel},${this.forecastConfig?.seasonal}`; } public fingerprint(): string { @@ -259,6 +307,25 @@ class TigerDataView implements IDataView { public equals(other: IDataView): boolean { return this.fingerprint() === other.fingerprint(); } + + public forecast(): IForecastView { + const transformForecastHeaders = getTransformForecastHeaders( + this.result.dimensions, + this._dateFormatter, + this.forecastConfig, + ); + + return transformForecastResult( + this._execResult, + this.forecastResult, + this.forecastConfig, + transformForecastHeaders, + ); + } + + public withForecast(config: IForecastConfig, result: IForecastResult): IDataView { + return new TigerDataView(this.result, this._execResult, this._dateFormatter, config, result); + } } function hasEmptyData(result: ExecutionResult): boolean { diff --git a/libs/sdk-backend-tiger/src/convertors/fromBackend/afm/DimensionHeaderConverter.ts b/libs/sdk-backend-tiger/src/convertors/fromBackend/afm/DimensionHeaderConverter.ts index c879008782c..e89fcd3f822 100644 --- a/libs/sdk-backend-tiger/src/convertors/fromBackend/afm/DimensionHeaderConverter.ts +++ b/libs/sdk-backend-tiger/src/convertors/fromBackend/afm/DimensionHeaderConverter.ts @@ -1,4 +1,4 @@ -// (C) 2022 GoodData Corporation +// (C) 2022-2024 GoodData Corporation import { DateAttributeGranularity, IDimensionDescriptor, @@ -28,6 +28,7 @@ import { import { createDateValueFormatter } from "../dateFormatting/dateValueFormatter.js"; import { toSdkGranularity } from "../dateGranularityConversions.js"; import { FormattingLocale } from "../dateFormatting/defaultDateFormatter.js"; +import { IForecastConfig, IForecastResult } from "@gooddata/sdk-backend-spi"; type DateAttributeFormatProps = { granularity: DateAttributeGranularity; @@ -205,6 +206,63 @@ export function getTransformDimensionHeaders( }); } +export function getTransformForecastHeaders( + dimensions: IDimensionDescriptor[], + dateFormatter: DateFormatter, + forecastConfig?: IForecastConfig, +): ( + dimensionHeaders: DimensionHeader[], + forecastResults: IForecastResult | undefined, +) => IResultHeader[][][] { + if (!forecastConfig) { + return () => []; + } + + const dateValueFormatter = createDateValueFormatter(dateFormatter); + + return (dimensionHeaders: DimensionHeader[], forecastResults: IForecastResult | undefined) => { + let used = false; + return dimensionHeaders.map((dimensionHeader, dimensionIndex) => { + if (!forecastResults) { + return []; + } + return dimensionHeader.headerGroups.map((headerGroup, headerGroupIndex) => { + const dateFormatProps = getDateFormatProps( + dimensions[dimensionIndex].headers[headerGroupIndex], + ); + + if ( + !used && + headerGroup.headers.length > 1 && + isResultAttributeHeader(headerGroup.headers[0]) + ) { + used = true; + + const length = forecastResults.attribute.length; + const data = forecastResults.attribute.slice( + length - forecastConfig.forecastPeriod, + length, + ); + return data.map((header): IResultAttributeHeader => { + return attributeHeaderItem( + { + attributeHeader: { + labelValue: header, + primaryLabelValue: header, + }, + }, + dateFormatProps, + dateValueFormatter, + ); + }); + } + + return []; + }); + }); + }; +} + function attributeHeaderItem( header: AttributeExecutionResultHeader, dateFormatProps: DateAttributeFormatProps | undefined, diff --git a/libs/sdk-backend-tiger/src/convertors/fromBackend/afm/forecast.ts b/libs/sdk-backend-tiger/src/convertors/fromBackend/afm/forecast.ts new file mode 100644 index 00000000000..7c50d5121a6 --- /dev/null +++ b/libs/sdk-backend-tiger/src/convertors/fromBackend/afm/forecast.ts @@ -0,0 +1,49 @@ +// (C) 2019-2024 GoodData Corporation +import { DataValue, IResultHeader } from "@gooddata/sdk-model"; +import { IForecastResult, IForecastConfig } from "@gooddata/sdk-backend-spi"; +import { ExecutionResult, DimensionHeader } from "@gooddata/api-client-tiger"; + +export type Data = DataValue[][]; + +export type TransformedForecastResult = { + readonly headerItems: IResultHeader[][][]; + readonly prediction: Data; + readonly low: Data; + readonly high: Data; +}; + +export function transformForecastResult( + result: ExecutionResult, + forecastResults: IForecastResult | undefined, + forecastConfig: IForecastConfig | undefined, + transformDimensionHeaders: ( + dimensionHeaders: DimensionHeader[], + forecastResults: IForecastResult | undefined, + ) => IResultHeader[][][], +): TransformedForecastResult { + const period = forecastConfig?.forecastPeriod ?? 0; + const prediction = + forecastResults?.prediction.slice( + forecastResults.prediction.length - period, + forecastResults.prediction.length, + ) ?? []; + const low = + forecastResults?.lowerBound.slice( + forecastResults.prediction.length - period, + forecastResults.prediction.length, + ) ?? []; + const high = + forecastResults?.upperBound.slice( + forecastResults.prediction.length - period, + forecastResults.prediction.length, + ) ?? []; + + return { + // in API is data typed as Array + //data: result. as unknown as Data, + low: [low], + high: [high], + prediction: [prediction], + headerItems: transformDimensionHeaders(result.dimensionHeaders, forecastResults), + }; +} diff --git a/libs/sdk-model/api/sdk-model.api.md b/libs/sdk-model/api/sdk-model.api.md index b8819847d64..745190d1e97 100644 --- a/libs/sdk-model/api/sdk-model.api.md +++ b/libs/sdk-model/api/sdk-model.api.md @@ -484,6 +484,13 @@ export function filterObjRef(filter: IAbsoluteDateFilter | IRelativeDateFilter | // @public export function filterObjRef(filter: IFilter): ObjRef | undefined; +// @beta +export type ForecastDataValue = { + low: DataValue; + high: DataValue; + prediction: DataValue; +}; + // @internal export function getAttributeElementsItems(attributeElements: IAttributeElements): Array; diff --git a/libs/sdk-model/src/execution/results/index.ts b/libs/sdk-model/src/execution/results/index.ts index e8804458465..02d40c66f6a 100644 --- a/libs/sdk-model/src/execution/results/index.ts +++ b/libs/sdk-model/src/execution/results/index.ts @@ -15,6 +15,13 @@ import { AttributeDisplayFormType } from "../../ldm/metadata/attributeDisplayFor */ export type DataValue = null | string | number; +/** + * Forecast single data value + * + * @beta + */ +export type ForecastDataValue = { low: DataValue; high: DataValue; prediction: DataValue }; + /** * Descriptor of the measure and its contents. * diff --git a/libs/sdk-model/src/index.ts b/libs/sdk-model/src/index.ts index 7febbe0ccb9..c81cd95298c 100644 --- a/libs/sdk-model/src/index.ts +++ b/libs/sdk-model/src/index.ts @@ -779,6 +779,7 @@ export { IWorkspacePermissions, WorkspacePermission } from "./permissions/index. export { DataValue, + ForecastDataValue, IMeasureDescriptor, IMeasureDescriptorObject, IMeasureDescriptorItem, diff --git a/libs/sdk-ui-charts/api/sdk-ui-charts.api.md b/libs/sdk-ui-charts/api/sdk-ui-charts.api.md index aae33f216b0..f19b0fff3b9 100644 --- a/libs/sdk-ui-charts/api/sdk-ui-charts.api.md +++ b/libs/sdk-ui-charts/api/sdk-ui-charts.api.md @@ -30,6 +30,8 @@ import { IDrillEventIntersectionElement } from '@gooddata/sdk-ui'; import { IExecutionConfig } from '@gooddata/sdk-model'; import { IExecutionFactory } from '@gooddata/sdk-backend-spi'; import { IFilter } from '@gooddata/sdk-model'; +import { IForecastConfig } from '@gooddata/sdk-backend-spi'; +import { IInsightDefinition } from '@gooddata/sdk-model'; import { IPreparedExecution } from '@gooddata/sdk-backend-spi'; import { IRgbColorValue } from '@gooddata/sdk-model'; import { ISeparators } from '@gooddata/sdk-model'; @@ -310,6 +312,8 @@ export interface IChartConfig { // (undocumented) enableSeparateTotalLabels?: boolean; forceDisableDrillOnAxes?: boolean; + // @beta + forecast?: IForecast; grid?: IGridConfig; // @beta hyperLinks?: IDisplayFormHyperlinksConfig; @@ -428,6 +432,7 @@ export interface IContinuousLineConfig { // @internal export interface ICoreChartProps extends ICommonChartProps { execution: IPreparedExecution; + forecastConfig?: IForecastConfig; } // @internal (undocumented) @@ -524,6 +529,14 @@ export interface IDonutChartBucketProps { export interface IDonutChartProps extends IBucketChartProps, IDonutChartBucketProps { } +// @beta +export interface IForecast { + confidence: number; + enabled: boolean; + period: number | string; + seasonal: boolean; +} + // @public (undocumented) export interface IFunnelChartBucketProps { filters?: NullableFiltersOrPlaceholders; @@ -642,6 +655,8 @@ export interface ILineChartBucketProps { // @public (undocumented) export interface ILineChartProps extends IBucketChartProps, ILineChartBucketProps { + // @beta + forecastConfig?: IForecastConfig; } // @public @@ -815,6 +830,12 @@ export const isDependencyWheel: LodashIsEqual1x1; // @internal (undocumented) export const isDonutChart: LodashIsEqual1x1; +// @beta +export function isForecastEnabled(insight: IInsightDefinition, type: ChartType): { + enabled: boolean; + visible: boolean; +}; + // @internal (undocumented) export const isFunnel: LodashIsEqual1x1; @@ -949,6 +970,11 @@ export const Treemap: (props: ITreemapProps) => React_2.JSX.Element; // @internal (undocumented) export function updateConfigWithSettings(config: IChartConfig, settings: ISettings): IChartConfig; +// @internal (undocumented) +export function updateForecastWithSettings(config: IChartConfig, settings: ISettings, { enabled }: { + enabled: boolean; +}): IForecastConfig | undefined; + // @public export const ViewByAttributesLimit = 2; diff --git a/libs/sdk-ui-charts/src/charts/_base/BaseChart.tsx b/libs/sdk-ui-charts/src/charts/_base/BaseChart.tsx index a10d3317751..10d16fb81d1 100644 --- a/libs/sdk-ui-charts/src/charts/_base/BaseChart.tsx +++ b/libs/sdk-ui-charts/src/charts/_base/BaseChart.tsx @@ -1,4 +1,4 @@ -// (C) 2007-2022 GoodData Corporation +// (C) 2007-2024 GoodData Corporation import React from "react"; import { ICoreChartProps, OnLegendReady } from "../../interfaces/index.js"; import { getValidColorPalette, ChartTransformation } from "../../highcharts/index.js"; diff --git a/libs/sdk-ui-charts/src/charts/lineChart/LineChart.tsx b/libs/sdk-ui-charts/src/charts/lineChart/LineChart.tsx index a37a2e8863b..4abd2656685 100644 --- a/libs/sdk-ui-charts/src/charts/lineChart/LineChart.tsx +++ b/libs/sdk-ui-charts/src/charts/lineChart/LineChart.tsx @@ -1,4 +1,4 @@ -// (C) 2007-2023 GoodData Corporation +// (C) 2007-2024 GoodData Corporation import React from "react"; import { IAttribute, @@ -22,6 +22,7 @@ import { IBucketChartProps } from "../../interfaces/index.js"; import { CoreLineChart } from "./CoreLineChart.js"; import { IChartDefinition } from "../_commons/chartDefinition.js"; import { withChart } from "../_base/withChart.js"; +import { IForecastConfig } from "@gooddata/sdk-backend-spi"; // // Internals @@ -105,7 +106,13 @@ export interface ILineChartBucketProps { /** * @public */ -export interface ILineChartProps extends IBucketChartProps, ILineChartBucketProps {} +export interface ILineChartProps extends IBucketChartProps, ILineChartBucketProps { + /** + * Enter the forecast configuration to apply to the chart data. + * @beta + */ + forecastConfig?: IForecastConfig; +} const WrappedLineChart = withChart(lineChartDefinition)(CoreLineChart); diff --git a/libs/sdk-ui-charts/src/forecast.ts b/libs/sdk-ui-charts/src/forecast.ts new file mode 100644 index 00000000000..e6503f183c8 --- /dev/null +++ b/libs/sdk-ui-charts/src/forecast.ts @@ -0,0 +1,36 @@ +// (C) 2022-2024 GoodData Corporation + +import { insightBuckets, bucketsFind, IInsightDefinition } from "@gooddata/sdk-model"; +import { BucketNames, ChartType } from "@gooddata/sdk-ui"; + +/** + * @beta + * Check if the forecast is enabled for the insight and chart type + */ +export function isForecastEnabled( + insight: IInsightDefinition, + type: ChartType, +): { + enabled: boolean; + visible: boolean; +} { + switch (type) { + case "line": { + const buckets = insightBuckets(insight); + const measures = bucketsFind(buckets, (b) => b.localIdentifier === BucketNames.MEASURES); + const trends = bucketsFind(buckets, (b) => b.localIdentifier === BucketNames.TREND); + + //TODO: s.hacker check if the trend is date attribute somehow + return { + enabled: measures?.items.length === 1 && trends?.items.length === 1, + visible: true, + }; + } + case "column": + default: + return { + enabled: false, + visible: false, + }; + } +} diff --git a/libs/sdk-ui-charts/src/highcharts/adapter/HighChartsRenderer.tsx b/libs/sdk-ui-charts/src/highcharts/adapter/HighChartsRenderer.tsx index 0234f4752bf..4b0551fd9e5 100644 --- a/libs/sdk-ui-charts/src/highcharts/adapter/HighChartsRenderer.tsx +++ b/libs/sdk-ui-charts/src/highcharts/adapter/HighChartsRenderer.tsx @@ -266,6 +266,7 @@ export class HighChartsRenderer extends React.PureComponent< if (isFunnel(chart.type)) { items = this.skipLeadingZeros(items).filter((i) => !isNil(i.y)); } + const updatedItems = items.map((item: any) => { const itemIndex = item.legendIndex; const visible = diff --git a/libs/sdk-ui-charts/src/highcharts/adapter/legendBuilder.ts b/libs/sdk-ui-charts/src/highcharts/adapter/legendBuilder.ts index fc00b062ed6..863b18f258f 100644 --- a/libs/sdk-ui-charts/src/highcharts/adapter/legendBuilder.ts +++ b/libs/sdk-ui-charts/src/highcharts/adapter/legendBuilder.ts @@ -22,7 +22,7 @@ import { createWaterfallLegendItems, } from "./legendHelpers.js"; import { supportedDualAxesChartTypes } from "../chartTypes/_chartOptions/chartCapabilities.js"; -import { IChartOptions, ISeriesNodeItem } from "../typings/unsafe.js"; +import { IChartOptions, ISeriesNodeItem, ISeriesItem } from "../typings/unsafe.js"; import { LegendOptionsItemType, ILegendOptions, @@ -39,10 +39,16 @@ function isHeatmapWithMultipleValues(chartOptions: IChartOptions) { } export function shouldLegendBeEnabled(chartOptions: IChartOptions): boolean { - const seriesLength = chartOptions?.data?.series?.length; + const legendItemsLength = chartOptions?.data?.series?.reduce((prev, cur: ISeriesItem) => { + if (prev.includes(cur.legendIndex)) { + return prev; + } + return [...prev, cur.legendIndex]; + }, []).length; + const { type, hasStackByAttribute, hasViewByAttribute } = chartOptions; - const hasMoreThanOneSeries = seriesLength > 1; + const hasMoreThanOneLegend = legendItemsLength > 1; const isLineChartStacked = isLineChart(type) && hasStackByAttribute; const isStacked = isStackedChart(chartOptions); const sliceTypes = [ @@ -63,7 +69,7 @@ export function shouldLegendBeEnabled(chartOptions: IChartOptions): boolean { const isWaterfallChart = isWaterfall(type); return ( - hasMoreThanOneSeries || + hasMoreThanOneLegend || isSliceChartWithViewByAttributeOrMultipleMeasures || isStacked || isLineChartStacked || diff --git a/libs/sdk-ui-charts/src/highcharts/chartTypes/_chartOptions/chartForecast.ts b/libs/sdk-ui-charts/src/highcharts/chartTypes/_chartOptions/chartForecast.ts index 6a3f38026a1..61d7b90f770 100644 --- a/libs/sdk-ui-charts/src/highcharts/chartTypes/_chartOptions/chartForecast.ts +++ b/libs/sdk-ui-charts/src/highcharts/chartTypes/_chartOptions/chartForecast.ts @@ -1,23 +1,25 @@ // (C) 2022-2024 GoodData Corporation import { VisType } from "@gooddata/sdk-ui"; +import { ISettings, ForecastDataValue } from "@gooddata/sdk-model"; import { getLighterColor } from "@gooddata/sdk-ui-vis-commons"; +import { IForecastConfig } from "@gooddata/sdk-backend-spi"; -import { ISeriesItem, IForecastData, ISeriesDataItem } from "../../typings/unsafe.js"; +import { ISeriesItem, ISeriesDataItem } from "../../typings/unsafe.js"; +import { IChartConfig, IForecast } from "../../../interfaces/index.js"; export function assignForecastAxes( type: VisType, - categories: any[], series: ISeriesItem[], - forecastData: IForecastData, -): { series: ISeriesItem[]; categories: any[] } { + forecastValues: ForecastDataValue[][], +): ISeriesItem[] { //in case of line chart, we need to add forecast axis if (type !== "line") { - return { series, categories }; + return series; } //if there is only one series, we need to add forecast axis if (series.length !== 1) { - return { series, categories }; + return series; } const [firstSeries] = series; @@ -25,63 +27,107 @@ export function assignForecastAxes( //if there is no data, we don't need to add forecast axis if (data.length === 0) { - return { series, categories }; + return series; + } + + //if there is no forecast data, we don't need to add forecast axis + const forecastData = forecastValues[0]; + if (!forecastData || forecastData.length === 0) { + return series; } const last = data[data.length - 1]; - const seriesData = [...data, ...forecastData.forecast.data.map(() => null)]; + const seriesData = [...data, ...forecastData.map(() => null)]; const rangeData = [ ...data.slice(0, -1).map(() => null), { - low: last.low, - high: last.high, + low: last.y, + high: last.y, format: last.format, + name: last.name, }, - ...forecastData.dispersion.data.map((item) => { + ...forecastData.map((item) => { return { - name: forecastData.dispersion.name, + name: last.name, format: last.format, - low: item[0], - high: item[1], + low: item.low, + high: item.high, } as ISeriesDataItem; }), ]; const predictedData = [ ...data.slice(0, -1).map(() => null), last, - ...forecastData.forecast.data.map((item) => { + ...forecastData.map((item) => { return { name: last.name, format: last.format, - y: item, + y: item.prediction, } as ISeriesDataItem; }), ]; - return { - series: [ - { ...firstSeries, data: seriesData }, - { - ...firstSeries, - data: rangeData, - name: forecastData.dispersion.name, - type: "areasplinerange", - marker: { - enabled: false, - }, - color: getLighterColor(firstSeries.color, 0.8), - lineWidth: 2, - lineColor: getLighterColor(firstSeries.color, 0.5), - showInLegend: false, - }, - { - ...firstSeries, - data: predictedData, - dashStyle: "dash", - showInLegend: false, + return [ + { ...firstSeries, data: seriesData }, + { + ...firstSeries, + data: rangeData, + type: "arearange", + marker: { + enabled: false, }, - ], - categories: [...categories, ...forecastData.categories], + color: getLighterColor(firstSeries.color, 0.8), + lineWidth: 2, + lineColor: getLighterColor(firstSeries.color, 0.5), + showInLegend: false, + }, + { + ...firstSeries, + data: predictedData, + dashStyle: "dash", + showInLegend: false, + }, + ]; +} + +/** + * @internal + */ +export function updateForecastWithSettings( + config: IChartConfig, + settings: ISettings, + { enabled }: { enabled: boolean }, +): IForecastConfig | undefined { + //no forecast setting + if (!config.forecast?.enabled || !enabled) { + return undefined; + } + + //check if confidence is set and is valid + const confidenceLevel = normalizeConfidence(config.forecast.confidence ?? 0.95); + const forecastPeriod = normalizePeriod(config.forecast.period ?? 3); + if (isNaN(confidenceLevel) || isNaN(forecastPeriod) || !settings.enableSmartFunctions) { + return undefined; + } + + return { + confidenceLevel, + forecastPeriod, + seasonal: config.forecast.seasonal ?? false, }; } + +/** + * Normalizes forecast confidence to be a number between 0 and 1 + */ +function normalizeConfidence(confidence: IForecast["confidence"]): number { + return confidence < 1 ? Math.max(confidence, 0) : Math.min(confidence, 100) / 100; +} + +/** + * Normalizes forecast period to be a number + */ +function normalizePeriod(period: IForecast["period"]): number { + return parseInt(period.toString(), 10); +} diff --git a/libs/sdk-ui-charts/src/highcharts/chartTypes/_chartOptions/chartOptionsBuilder.ts b/libs/sdk-ui-charts/src/highcharts/chartTypes/_chartOptions/chartOptionsBuilder.ts index 0381592cf86..f09316b5014 100644 --- a/libs/sdk-ui-charts/src/highcharts/chartTypes/_chartOptions/chartOptionsBuilder.ts +++ b/libs/sdk-ui-charts/src/highcharts/chartTypes/_chartOptions/chartOptionsBuilder.ts @@ -459,23 +459,7 @@ export function getChartOptions( } //Forecast - const forecastedData = assignForecastAxes(type, categories, series, { - dispersion: { - data: [ - [865190, 905190], - [402595, 482595], - [161297, 281297], - [30648, 190648], - ], - name: "Dispersion", - }, - forecast: { - data: [885190, 442595, 221297, 110648], - }, - categories: ["2026", "2027", "2028", "2029"], - }); - series = forecastedData.series; - categories = forecastedData.categories; + series = assignForecastAxes(type, series, dv.rawData().forecastTwoDimData()); const colorAssignments = colorStrategy.getColorAssignment(); const { colorPalette } = config; diff --git a/libs/sdk-ui-charts/src/highcharts/index.ts b/libs/sdk-ui-charts/src/highcharts/index.ts index 4028ca90814..ad90d171531 100644 --- a/libs/sdk-ui-charts/src/highcharts/index.ts +++ b/libs/sdk-ui-charts/src/highcharts/index.ts @@ -12,6 +12,7 @@ export { Chart, ChartTransformation, IChartTransformationProps, IChartProps }; export { FLUID_LEGEND_THRESHOLD } from "./adapter/HighChartsRenderer.js"; export { COMBO_SUPPORTED_CHARTS } from "./chartTypes/comboChart/comboChartOptions.js"; export { updateConfigWithSettings } from "./chartTypes/_chartOptions/chartOptionsForSettings.js"; +export { updateForecastWithSettings } from "./chartTypes/_chartOptions/chartForecast.js"; export { isLineChart, diff --git a/libs/sdk-ui-charts/src/highcharts/typings/unsafe.ts b/libs/sdk-ui-charts/src/highcharts/typings/unsafe.ts index f5c57f660fc..2291d93a43c 100644 --- a/libs/sdk-ui-charts/src/highcharts/typings/unsafe.ts +++ b/libs/sdk-ui-charts/src/highcharts/typings/unsafe.ts @@ -298,14 +298,3 @@ export interface IAxis { opposite?: boolean; seriesIndices?: number[]; } - -export interface IForecastData { - dispersion: { - name: string; - data: [any, any][]; - }; - forecast: { - data: any[]; - }; - categories: any[]; -} diff --git a/libs/sdk-ui-charts/src/index.ts b/libs/sdk-ui-charts/src/index.ts index 8c0b6064d54..3022fcabc31 100644 --- a/libs/sdk-ui-charts/src/index.ts +++ b/libs/sdk-ui-charts/src/index.ts @@ -1,4 +1,4 @@ -// (C) 2007-2023 GoodData Corporation +// (C) 2007-2024 GoodData Corporation /** * This package provides a set of React-based chart visualizations that you can use to visualize your data. * @@ -11,6 +11,7 @@ */ export * from "./interfaces/index.js"; export * from "./charts/index.js"; +export * from "./forecast.js"; export { ColorUtils, TOP, @@ -36,6 +37,7 @@ export { isSankeyOrDependencyWheel, isWaterfall, updateConfigWithSettings, + updateForecastWithSettings, } from "./highcharts/index.js"; // export the getColorMappingPredicate so that users can import it directly without having to explicitly install vis-commons diff --git a/libs/sdk-ui-charts/src/interfaces/chartConfig.ts b/libs/sdk-ui-charts/src/interfaces/chartConfig.ts index 08056b5a10c..2b48b623add 100644 --- a/libs/sdk-ui-charts/src/interfaces/chartConfig.ts +++ b/libs/sdk-ui-charts/src/interfaces/chartConfig.ts @@ -337,6 +337,43 @@ export interface IChartConfig { * @beta */ inlineVisualizations?: IInlineVisualizationsConfig; + + /** + * Configuration of the forecast. + * @beta + */ + forecast?: IForecast; +} + +/** + * @beta + * Forecast configuration + */ +export interface IForecast { + /** + * @beta + * Indicates whether the forecast is enabled or not. + */ + enabled: boolean; + + /** + * @beta + * Indicates the confidence level of the forecast. + * The value should be between (0, 1). + */ + confidence: number; + + /** + * @beta + * Indicates the number of periods to forecast. + */ + period: number | string; + + /** + * @beta + * Indicates whether the forecast is seasonal or not. + */ + seasonal: boolean; } /** diff --git a/libs/sdk-ui-charts/src/interfaces/chartProps.ts b/libs/sdk-ui-charts/src/interfaces/chartProps.ts index bb7bd4701cc..5a79794d8ac 100644 --- a/libs/sdk-ui-charts/src/interfaces/chartProps.ts +++ b/libs/sdk-ui-charts/src/interfaces/chartProps.ts @@ -1,5 +1,5 @@ -// (C) 2019-2022 GoodData Corporation -import { IAnalyticalBackend, IPreparedExecution } from "@gooddata/sdk-backend-spi"; +// (C) 2019-2024 GoodData Corporation +import { IAnalyticalBackend, IForecastConfig, IPreparedExecution } from "@gooddata/sdk-backend-spi"; import { IVisualizationCallbacks, IVisualizationProps } from "@gooddata/sdk-ui"; import { IExecutionConfig } from "@gooddata/sdk-model"; import { IChartConfig } from "./chartConfig.js"; @@ -109,4 +109,9 @@ export interface ICoreChartProps extends ICommonChartProps { * Prepared execution, which when executed, will provide data to visualize in the chart. */ execution: IPreparedExecution; + + /** + * Forecast configuration to apply to the chart data. + */ + forecastConfig?: IForecastConfig; } diff --git a/libs/sdk-ui-charts/src/interfaces/index.ts b/libs/sdk-ui-charts/src/interfaces/index.ts index 06e8550c668..9ac065dba76 100644 --- a/libs/sdk-ui-charts/src/interfaces/index.ts +++ b/libs/sdk-ui-charts/src/interfaces/index.ts @@ -1,6 +1,7 @@ // (C) 2007-2024 GoodData Corporation export { + IForecast, IChartConfig, PositionType, ILegendConfig, diff --git a/libs/sdk-ui-ext/src/internal/components/configurationControls/DropdownControl.tsx b/libs/sdk-ui-ext/src/internal/components/configurationControls/DropdownControl.tsx index fab3b841f44..1cb54441319 100644 --- a/libs/sdk-ui-ext/src/internal/components/configurationControls/DropdownControl.tsx +++ b/libs/sdk-ui-ext/src/internal/components/configurationControls/DropdownControl.tsx @@ -1,4 +1,4 @@ -// (C) 2019 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import React from "react"; import { WrappedComponentProps, injectIntl } from "react-intl"; import { @@ -21,7 +21,7 @@ export interface IDropdownControlProps { valuePath: string; properties: IVisualizationProperties; labelText?: string; - value?: string; + value?: string | number; items?: IDropdownItem[]; disabled?: boolean; width?: number; @@ -141,7 +141,7 @@ class DropdownControl extends React.PureComponent item.value === value); } diff --git a/libs/sdk-ui-ext/src/internal/components/configurationControls/forecast/ForecastConfidenceControl.tsx b/libs/sdk-ui-ext/src/internal/components/configurationControls/forecast/ForecastConfidenceControl.tsx index c6cd0c74946..881a038423e 100644 --- a/libs/sdk-ui-ext/src/internal/components/configurationControls/forecast/ForecastConfidenceControl.tsx +++ b/libs/sdk-ui-ext/src/internal/components/configurationControls/forecast/ForecastConfidenceControl.tsx @@ -10,7 +10,7 @@ import { messages } from "../../../../locales.js"; export interface IForecastConfidenceControl { disabled: boolean; - value: string; + value: number; showDisabledMessage: boolean; properties: IVisualizationProperties; pushData: (data: any) => any; diff --git a/libs/sdk-ui-ext/src/internal/components/configurationControls/forecast/ForecastSection.tsx b/libs/sdk-ui-ext/src/internal/components/configurationControls/forecast/ForecastSection.tsx index 8ef43e1b4a8..02ce4f5d57f 100644 --- a/libs/sdk-ui-ext/src/internal/components/configurationControls/forecast/ForecastSection.tsx +++ b/libs/sdk-ui-ext/src/internal/components/configurationControls/forecast/ForecastSection.tsx @@ -23,7 +23,7 @@ class ForecastSection extends React.PureComponent { enabled: false, properties: {}, propertiesMeta: {}, - defaultForecastEnabled: true, + defaultForecastEnabled: false, pushData: noop, }; @@ -31,7 +31,7 @@ class ForecastSection extends React.PureComponent { const { controlsDisabled, properties, pushData, defaultForecastEnabled, enabled } = this.props; const forecastEnabled = this.props.properties?.controls?.forecast?.enabled ?? defaultForecastEnabled; - const forecastConfidence = this.props.properties?.controls?.forecast?.confidence ?? "95"; + const forecastConfidence = this.props.properties?.controls?.forecast?.confidence ?? 0.95; const forecastPeriod = this.props.properties?.controls?.forecast?.period ?? 3; const forecastSeasonal = this.props.properties?.controls?.forecast?.seasonal ?? false; const forecastToggleDisabledByVisualization = !(this.props.propertiesMeta?.forecast_enabled ?? true); diff --git a/libs/sdk-ui-ext/src/internal/components/configurationPanels/ConfigurationPanelContent.tsx b/libs/sdk-ui-ext/src/internal/components/configurationPanels/ConfigurationPanelContent.tsx index 49583c28a50..ec64122bda6 100644 --- a/libs/sdk-ui-ext/src/internal/components/configurationPanels/ConfigurationPanelContent.tsx +++ b/libs/sdk-ui-ext/src/internal/components/configurationPanels/ConfigurationPanelContent.tsx @@ -1,14 +1,9 @@ // (C) 2019-2024 GoodData Corporation import React from "react"; import noop from "lodash/noop.js"; -import { ChartType, DefaultLocale, BucketNames } from "@gooddata/sdk-ui"; -import { - IInsightDefinition, - ISettings, - insightHasMeasures, - insightBuckets, - bucketsFind, -} from "@gooddata/sdk-model"; +import { ChartType, DefaultLocale } from "@gooddata/sdk-ui"; +import { isForecastEnabled } from "@gooddata/sdk-ui-charts"; +import { IInsightDefinition, ISettings, insightHasMeasures } from "@gooddata/sdk-model"; import { IReferences, @@ -134,25 +129,19 @@ export default abstract class ConfigurationPanelContent< return null; } - //line chart only now - if (type === "line") { - const buckets = insightBuckets(insight); - const measures = bucketsFind(buckets, (b) => b.localIdentifier === BucketNames.MEASURES); - const trends = bucketsFind(buckets, (b) => b.localIdentifier === BucketNames.TREND); - //TODO: check if the trend is date attribute somehow - const enabled = measures?.items.length === 1 && trends?.items.length === 1; - - return ( - - ); + const { enabled, visible } = isForecastEnabled(insight, type); + if (!visible) { + return null; } - return null; + return ( + + ); } } diff --git a/libs/sdk-ui-ext/src/internal/components/pluggableVisualizations/baseChart/PluggableBaseChart.tsx b/libs/sdk-ui-ext/src/internal/components/pluggableVisualizations/baseChart/PluggableBaseChart.tsx index 8d3ab4c0f44..e50e7c4a5fb 100644 --- a/libs/sdk-ui-ext/src/internal/components/pluggableVisualizations/baseChart/PluggableBaseChart.tsx +++ b/libs/sdk-ui-ext/src/internal/components/pluggableVisualizations/baseChart/PluggableBaseChart.tsx @@ -1,4 +1,4 @@ -// (C) 2019-2022 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import { IBackendCapabilities, IExecutionFactory } from "@gooddata/sdk-backend-spi"; import { IColorMappingItem, @@ -16,6 +16,8 @@ import { IChartConfig, IColorMapping, updateConfigWithSettings, + updateForecastWithSettings, + isForecastEnabled, } from "@gooddata/sdk-ui-charts"; import React from "react"; import compact from "lodash/compact.js"; @@ -275,6 +277,11 @@ export class PluggableBaseChart extends AbstractPluggableVisualization { height={resultingHeight} type={this.type} locale={locale} + forecastConfig={updateForecastWithSettings( + fullConfig, + this.featureFlags, + isForecastEnabled(insight, this.type), + )} config={updateConfigWithSettings(fullConfig, this.featureFlags)} LoadingComponent={null} ErrorComponent={null} diff --git a/libs/sdk-ui-ext/src/internal/constants/dropdowns.ts b/libs/sdk-ui-ext/src/internal/constants/dropdowns.ts index a84ed269b1f..cc30d14e3ba 100644 --- a/libs/sdk-ui-ext/src/internal/constants/dropdowns.ts +++ b/libs/sdk-ui-ext/src/internal/constants/dropdowns.ts @@ -46,12 +46,12 @@ export const formatDropdownItems: IDropdownItem[] = [ ]; export const confidenceDropdownItems: IDropdownItem[] = [ - { title: messages.forecastConfidence70.id, value: "70" }, - { title: messages.forecastConfidence75.id, value: "75" }, - { title: messages.forecastConfidence80.id, value: "80" }, - { title: messages.forecastConfidence85.id, value: "85" }, - { title: messages.forecastConfidence90.id, value: "90" }, - { title: messages.forecastConfidence95.id, value: "95" }, + { title: messages.forecastConfidence70.id, value: 0.7 }, + { title: messages.forecastConfidence75.id, value: 0.75 }, + { title: messages.forecastConfidence80.id, value: 0.8 }, + { title: messages.forecastConfidence85.id, value: 0.85 }, + { title: messages.forecastConfidence90.id, value: 0.9 }, + { title: messages.forecastConfidence95.id, value: 0.95 }, ]; export const legendPositionDropdownItems: IDropdownItem[] = [ diff --git a/libs/sdk-ui/api/sdk-ui.api.md b/libs/sdk-ui/api/sdk-ui.api.md index 52e35f629db..67a7fccdffe 100644 --- a/libs/sdk-ui/api/sdk-ui.api.md +++ b/libs/sdk-ui/api/sdk-ui.api.md @@ -10,6 +10,7 @@ import { AuthenticationFlow } from '@gooddata/sdk-backend-spi'; import { ComponentType } from 'react'; import { DataValue } from '@gooddata/sdk-model'; import { DependencyList } from 'react'; +import { ForecastDataValue } from '@gooddata/sdk-model'; import { IAbsoluteDateFilter } from '@gooddata/sdk-model'; import { IAnalyticalBackend } from '@gooddata/sdk-backend-spi'; import { IAttribute } from '@gooddata/sdk-model'; @@ -30,6 +31,7 @@ import { IExecutionResult } from '@gooddata/sdk-backend-spi'; import { IExportConfig } from '@gooddata/sdk-backend-spi'; import { IExportResult } from '@gooddata/sdk-backend-spi'; import { IFilter } from '@gooddata/sdk-model'; +import { IForecastConfig } from '@gooddata/sdk-backend-spi'; import { IInsightDefinition } from '@gooddata/sdk-model'; import { IMeasure } from '@gooddata/sdk-model'; import { IMeasureDefinitionType } from '@gooddata/sdk-model'; @@ -654,6 +656,8 @@ export interface IDataSliceCollection extends Iterable { // @public export interface IDataVisualizationProps extends IVisualizationProps, IVisualizationCallbacks { execution: IPreparedExecution; + // @beta + forecastConfig?: IForecastConfig; } // @public @@ -1119,6 +1123,7 @@ export interface IResultDataMethods { dataAt(index: number): DataValue | DataValue[]; // (undocumented) firstDimSize(): number; + forecastTwoDimData(): ForecastDataValue[][]; hasColumnTotals(): boolean; hasRowTotals(): boolean; hasTotals(): boolean; diff --git a/libs/sdk-ui/src/base/react/legacy/withEntireDataView.tsx b/libs/sdk-ui/src/base/react/legacy/withEntireDataView.tsx index 255976aa662..7dc4c3dcf3f 100644 --- a/libs/sdk-ui/src/base/react/legacy/withEntireDataView.tsx +++ b/libs/sdk-ui/src/base/react/legacy/withEntireDataView.tsx @@ -1,8 +1,9 @@ -// (C) 2019 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import { IDataView, IExecutionResult, + IForecastConfig, IPreparedExecution, isNoDataError, isUnexpectedResponseError, @@ -12,6 +13,7 @@ import React from "react"; import { injectIntl, IntlShape } from "react-intl"; import noop from "lodash/noop.js"; import omit from "lodash/omit.js"; +import isEqual from "lodash/isEqual.js"; import { IExportFunction, ILoadingState } from "../../vis/Events.js"; import { @@ -114,7 +116,7 @@ export function withEntireDataView( } public componentDidMount() { - this.initDataLoading(this.props.execution); + this.initDataLoading(this.props.execution, this.props.forecastConfig); } public render() { @@ -139,8 +141,11 @@ export function withEntireDataView( public UNSAFE_componentWillReceiveProps(nextProps: Readonly) { // we need strict equality here in case only the buckets changed (not measures or attributes) - if (!this.props.execution.equals(nextProps.execution)) { - this.initDataLoading(nextProps.execution); + if ( + !this.props.execution.equals(nextProps.execution) || + !isEqual(this.props.forecastConfig, nextProps.forecastConfig) + ) { + this.initDataLoading(nextProps.execution, nextProps.forecastConfig); } } @@ -187,7 +192,7 @@ export function withEntireDataView( this.onError(new NegativeValuesSdkError()); } - private async initDataLoading(execution: IPreparedExecution) { + private async initDataLoading(execution: IPreparedExecution, forecastConfig?: IForecastConfig) { const { onExportReady, pushData, exportTitle } = this.props; this.onLoadingChanged({ isLoading: true }); this.setState({ dataView: null }); @@ -243,6 +248,26 @@ export function withEntireDataView( pushData({ dataView, availableDrillTargets }); } + + if (this.hasUnmounted) { + return; + } + + if (forecastConfig) { + const normalizedForecastConfig = { + ...forecastConfig, + forecastPeriod: Math.min( + forecastConfig.forecastPeriod, + Math.max((dataView.count[1] ?? 0) - 1, 0), + ), + }; + const forecastResult = await executionResult.readForecastAll(normalizedForecastConfig); + const updatedDataView = dataView.withForecast(normalizedForecastConfig, forecastResult); + this.setState((s) => ({ ...s, dataView: updatedDataView })); + if (pushData) { + pushData({ dataView: updatedDataView }); + } + } } catch (error) { if (this.lastInitRequestFingerprint !== defFingerprint(execution.definition)) { return; diff --git a/libs/sdk-ui/src/base/results/facade.ts b/libs/sdk-ui/src/base/results/facade.ts index c97ed7059a7..e721b4efc59 100644 --- a/libs/sdk-ui/src/base/results/facade.ts +++ b/libs/sdk-ui/src/base/results/facade.ts @@ -1,6 +1,12 @@ -// (C) 2019-2022 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import { defFingerprint, IExecutionDefinition, IResultWarning } from "@gooddata/sdk-model"; -import { IDataView, IExecutionResult } from "@gooddata/sdk-backend-spi"; +import { + IDataView, + IExecutionResult, + IForecastConfig, + IForecastResult, + IForecastView, +} from "@gooddata/sdk-backend-spi"; import { DataAccessConfig } from "./dataAccessConfig.js"; import { IExecutionDefinitionMethods, newExecutionDefinitonMethods } from "./internal/definitionMethods.js"; import { IResultMetaMethods, newResultMetaMethods } from "./internal/resultMetaMethods.js"; @@ -159,16 +165,24 @@ export class DataViewFacade { * Constructs an empty data view with given execution result. * * @param result - execution result + * @param forecastConfig - forecast configuration + * @param forecastResult - forecast result * @returns data view * @public */ -export function emptyDataViewForResult(result: IExecutionResult): IDataView { +export function emptyDataViewForResult( + result: IExecutionResult, + forecastConfig?: IForecastConfig, + forecastResult?: IForecastResult, +): IDataView { const { definition } = result; const fp = defFingerprint(definition) + "/emptyView"; return { definition, result, + forecastConfig, + forecastResult, headerItems: [], data: [], offset: [0, 0], @@ -180,5 +194,16 @@ export function emptyDataViewForResult(result: IExecutionResult): IDataView { equals(other: IDataView): boolean { return fp === other.fingerprint(); }, + withForecast(forecastConfig: IForecastConfig, forecastResult: IForecastResult): IDataView { + return emptyDataViewForResult(result, forecastConfig, forecastResult); + }, + forecast(): IForecastView { + return { + headerItems: [], + high: [], + low: [], + prediction: [], + }; + }, }; } diff --git a/libs/sdk-ui/src/base/results/internal/resultDataMethods.ts b/libs/sdk-ui/src/base/results/internal/resultDataMethods.ts index 1eb78a5de08..47a0e9442cc 100644 --- a/libs/sdk-ui/src/base/results/internal/resultDataMethods.ts +++ b/libs/sdk-ui/src/base/results/internal/resultDataMethods.ts @@ -1,6 +1,6 @@ -// (C) 2019-2022 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import { IDataView } from "@gooddata/sdk-backend-spi"; -import { DataValue } from "@gooddata/sdk-model"; +import { DataValue, ForecastDataValue } from "@gooddata/sdk-model"; import { invariant } from "ts-invariant"; import isArray from "lodash/isArray.js"; @@ -58,6 +58,15 @@ export interface IResultDataMethods { */ twoDimData(): DataValue[][]; + /** + * This is a convenience method that determines whether the data in the data view is two dimension; if it + * is then data is returned as-is. If the data is single dimension, this method will up-cast the data to + * two dimensions. + * + * @returns two dimensional data; if data is empty, returns array with single empty array in + */ + forecastTwoDimData(): ForecastDataValue[][]; + /** * @returns grand totals in the data view, undefined if there are no grand totals */ @@ -156,6 +165,28 @@ class ResultDataMethods implements IResultDataMethods { return isArray(e) ? (d as DataValue[][]) : ([d] as DataValue[][]); } + public forecastTwoDimData(): ForecastDataValue[][] { + const { prediction, low, high } = this.dataView.forecast(); + + if (prediction === null) { + return []; + } + + const e = prediction[0]; + + if (!e) { + return []; + } + + return prediction.map((p, id) => + p.map((_, ii) => ({ + prediction: prediction[id][ii], + low: low[id][ii], + high: high[id][ii], + })), + ); + } + public totals(): DataValue[][][] | undefined { return this.dataView.totals; } diff --git a/libs/sdk-ui/src/base/results/internal/resultMetaMethods.ts b/libs/sdk-ui/src/base/results/internal/resultMetaMethods.ts index 6bdd72850ad..9f083bd9133 100644 --- a/libs/sdk-ui/src/base/results/internal/resultMetaMethods.ts +++ b/libs/sdk-ui/src/base/results/internal/resultMetaMethods.ts @@ -1,4 +1,4 @@ -// (C) 2019-2022 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import flatMap from "lodash/flatMap.js"; import { IDataView } from "@gooddata/sdk-backend-spi"; import { @@ -238,14 +238,31 @@ class ResultMetaMethods implements IResultMetaMethods { } public allHeaders(): IResultHeader[][][] { - return this.dataView.headerItems; + const data = this.dataView.forecast(); + return this.dataView.headerItems.map((dimension: IResultHeader[][], dim) => { + const forecastHeaders = (data.headerItems[dim]?.filter((headerList) => + isResultAttributeHeader(headerList[0]), + ) ?? []) as IResultAttributeHeader[][]; + return dimension.map((normalHeader, idx) => { + return [...normalHeader, ...(forecastHeaders[idx] ?? [])]; + }) as IResultHeader[][]; + }); } public attributeHeaders(): IResultAttributeHeader[][][] { - return this.dataView.headerItems.map((dimension: IResultHeader[][]) => { - return dimension.filter((headerList) => + const data = this.dataView.forecast(); + return this.dataView.headerItems.map((dimension: IResultHeader[][], dim) => { + const normalHeaders = dimension.filter((headerList) => isResultAttributeHeader(headerList[0]), ) as IResultAttributeHeader[][]; + + const forecastHeaders = (data.headerItems[dim]?.filter((headerList) => + isResultAttributeHeader(headerList[0]), + ) ?? []) as IResultAttributeHeader[][]; + + return normalHeaders.map((normalHeader, idx) => { + return [...normalHeader, ...(forecastHeaders[idx] ?? [])]; + }) as IResultAttributeHeader[][]; }); } @@ -254,9 +271,18 @@ class ResultMetaMethods implements IResultMetaMethods { return []; } - return this.dataView.headerItems[dim].filter((headerList) => + const data = this.dataView.forecast(); + const normalHeaders = this.dataView.headerItems[dim].filter((headerList) => isResultAttributeHeader(headerList[0]), ) as IResultAttributeHeader[][]; + + const forecastHeaders = (data.headerItems[dim]?.filter((headerList) => + isResultAttributeHeader(headerList[0]), + ) ?? []) as IResultAttributeHeader[][]; + + return normalHeaders.map((normalHeader, idx) => { + return [...normalHeader, ...(forecastHeaders[idx] ?? [])]; + }) as IResultAttributeHeader[][]; } public isDerivedMeasure(measureDescriptor: IMeasureDescriptor): boolean { diff --git a/libs/sdk-ui/src/base/vis/VisualizationProps.ts b/libs/sdk-ui/src/base/vis/VisualizationProps.ts index 723e487ca1c..40728e3b59b 100644 --- a/libs/sdk-ui/src/base/vis/VisualizationProps.ts +++ b/libs/sdk-ui/src/base/vis/VisualizationProps.ts @@ -1,11 +1,11 @@ -// (C) 2019-2022 GoodData Corporation +// (C) 2019-2024 GoodData Corporation import { ExplicitDrill, OnFiredDrillEvent } from "./DrillEvents.js"; import React from "react"; import { IErrorProps } from "../react/ErrorComponent.js"; import { ILoadingProps } from "../react/LoadingComponent.js"; import { IPushData, OnError, OnExportReady, OnLoadingChanged } from "./Events.js"; -import { IPreparedExecution } from "@gooddata/sdk-backend-spi"; +import { IForecastConfig, IPreparedExecution } from "@gooddata/sdk-backend-spi"; /** * Super-interface for all visualization props. @@ -104,4 +104,10 @@ export interface IDataVisualizationProps extends IVisualizationProps, IVisualiza * Prepared execution - running this will compute data to visualize. */ execution: IPreparedExecution; + + /** + * Configuration for forecasting. + * @beta + */ + forecastConfig?: IForecastConfig; }