diff --git a/core/backend/src/main/java/io/dataease/service/chart/ChartViewService.java b/core/backend/src/main/java/io/dataease/service/chart/ChartViewService.java index 44528a8f51..6fb2639c6b 100644 --- a/core/backend/src/main/java/io/dataease/service/chart/ChartViewService.java +++ b/core/backend/src/main/java/io/dataease/service/chart/ChartViewService.java @@ -1368,7 +1368,7 @@ public class ChartViewService { // forecast List extends ForecastDataVO, ?>> forecastData = Collections.emptyList(); JSONObject senior = JSONObject.parseObject(view.getSenior()); - JSONObject forecastObj = senior.getJSONObject("forecast"); + JSONObject forecastObj = senior.getJSONObject("forecastCfg"); if (forecastObj != null) { ChartSeniorForecastDTO forecastCfg = forecastObj.toJavaObject(ChartSeniorForecastDTO.class); if (forecastCfg.isEnable()) { diff --git a/core/frontend/src/lang/en.js b/core/frontend/src/lang/en.js index da7c203da2..965787806c 100644 --- a/core/frontend/src/lang/en.js +++ b/core/frontend/src/lang/en.js @@ -1825,7 +1825,16 @@ export default { trend_line: 'Trend Line', field_enum: 'Enumeration values', main_axis_label: 'Main Axis Label', - sub_axis_label: 'Sub Axis Label' + sub_axis_label: 'Sub Axis Label', + forecast_enable: 'Enable forecast', + forecast_all_period: 'Use full data', + forecast_all_period_tip: 'Whether to use all data as training data for predictions', + forecast_training_period: 'Training data', + forecast_training_period_tip: 'Intercept the most recent data from all the data as the training data', + forecast_period: 'Forecast period', + forecast_confidence_interval: 'Confidence interval', + forecast_model: 'Forecast model', + forecast_degree: 'Degree' }, dataset: { scope_edit: 'Effective only when editing', diff --git a/core/frontend/src/lang/tw.js b/core/frontend/src/lang/tw.js index 10fafb8b86..68c52af545 100644 --- a/core/frontend/src/lang/tw.js +++ b/core/frontend/src/lang/tw.js @@ -1818,7 +1818,16 @@ export default { trend_line: '趨勢線', field_enum: '枚舉值', main_axis_label: '主軸標籤', - sub_axis_label: '副軸標籤' + sub_axis_label: '副軸標籤', + forecast_enable: '啟用預測', + forecast_all_period: '全量數據', + forecast_all_period_tip: '是否使用所有數據作為馴良數據進行預測', + forecast_training_period: '訓練數據', + forecast_training_period_tip: '從所有數據中截取最近的數據作為訓練數據', + forecast_period: '預測週期', + forecast_confidence_interval: '置信區間', + forecast_model: '預測模型', + forecast_degree: '階數' }, dataset: { scope_edit: '僅編輯時生效', diff --git a/core/frontend/src/lang/zh.js b/core/frontend/src/lang/zh.js index caec1525d3..8234cc151a 100644 --- a/core/frontend/src/lang/zh.js +++ b/core/frontend/src/lang/zh.js @@ -1815,7 +1815,16 @@ export default { trend_line: '趋势线', field_enum: '枚举值', main_axis_label: '主轴标签', - sub_axis_label: '副轴标签' + sub_axis_label: '副轴标签', + forecast_enable: '启用预测', + forecast_all_period: '全量数据', + forecast_all_period_tip: '是否使用所有数据作为训练数据进行预测', + forecast_training_period: '训练数据', + forecast_training_period_tip: '从所有数据中截取最近的数据作为训练数据', + forecast_period: '预测周期', + forecast_confidence_interval: '置信区间', + forecast_model: '预测模型', + forecast_degree: '阶数' }, dataset: { goto: ', 前往 ', diff --git a/core/frontend/src/views/chart/chart/bar/bar_antv.js b/core/frontend/src/views/chart/chart/bar/bar_antv.js index 46f83c3d34..5a6e7aa0b5 100644 --- a/core/frontend/src/views/chart/chart/bar/bar_antv.js +++ b/core/frontend/src/views/chart/chart/bar/bar_antv.js @@ -114,6 +114,33 @@ export function baseBarOptionAntV(container, chart, action, isGroup, isStack) { } else { delete options.groupField } + // forecast + if (chart.data?.forecastData?.length) { + const { forecastData } = chart.data + const templateData = data?.[data.length - 1] + forecastData.forEach(item => { + data.push({ + ...templateData, + field: item.dimension, + name: item.dimension, + value: item.quota, + forecast: true + }) + }) + analyse.push({ + type: 'region', + start: xScale => { + const ratio = xScale.ticks ? 1 / xScale.ticks.length : 1 + const x = xScale.scale(forecastData[0].dimension) - ratio / 2 + return [`${x * 100}%`, '0%'] + }, + end: (xScale) => { + const ratio = xScale.ticks ? 1 / xScale.ticks.length : 1 + const x = xScale.scale(forecastData[forecastData.length - 1].dimension) + ratio / 2 + return [`${x * 100}%`, '100%'] + } + }) + } // 目前只有百分比堆叠柱状图需要这个属性,先直接在这边判断而不作为参数传过来 options.isPercent = chart.type === 'percentage-bar-stack' // custom color diff --git a/core/frontend/src/views/chart/chart/line/line_antv.js b/core/frontend/src/views/chart/chart/line/line_antv.js index 088d701a98..fd7007d4bf 100644 --- a/core/frontend/src/views/chart/chart/line/line_antv.js +++ b/core/frontend/src/views/chart/chart/line/line_antv.js @@ -94,6 +94,39 @@ export function baseLineOptionAntV(container, chart, action) { } } } + // forecast + if (chart.data?.forecastData?.length) { + const { forecastData } = chart.data + const templateData = data?.[data.length - 1] + forecastData.forEach(item => { + data.push({ + ...templateData, + field: item.dimension, + name: item.dimension, + value: item.quota, + forecast: true + }) + }) + analyse.push({ + type: 'region', + start: (xScale) => { + if (forecastData.length > 1) { + return [forecastData[0].dimension, 'min'] + } + const ratio = xScale.ticks ? 1 / xScale.ticks.length : 1 + const x = xScale.scale(forecastData[0].dimension) - ratio / 2 + return [`${x * 100}%`, '0%'] + }, + end: (xScale) => { + if (forecastData.length > 1) { + return [forecastData[forecastData.length - 1].dimension, 'max'] + } + const ratio = xScale.ticks ? 1 / xScale.ticks.length : 1 + const x = xScale.scale(forecastData[forecastData.length - 1].dimension) + ratio / 2 + return [`${x * 100}%`, '100%'] + } + }) + } // custom color options.color = antVCustomColor(chart) // 处理空值 diff --git a/core/frontend/src/views/chart/components/senior/DataForecast.vue b/core/frontend/src/views/chart/components/senior/DataForecast.vue new file mode 100644 index 0000000000..c8f844fe23 --- /dev/null +++ b/core/frontend/src/views/chart/components/senior/DataForecast.vue @@ -0,0 +1,246 @@ + + + + + + + + + + + {{ $t('chart.forecast_all_period_tip') }} + + + + + + + + {{ $t('chart.forecast_training_period_tip') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/frontend/src/views/chart/view/ChartEdit.vue b/core/frontend/src/views/chart/view/ChartEdit.vue index 4141ffed15..6e6e38e176 100644 --- a/core/frontend/src/views/chart/view/ChartEdit.vue +++ b/core/frontend/src/views/chart/view/ChartEdit.vue @@ -1361,6 +1361,18 @@ @onTrendLineChange="onTrendLineChange" /> + + + @@ -1937,9 +1949,11 @@ import PositionAdjust from '@/views/chart/view/PositionAdjust' import MarkMapDataEditor from '@/views/chart/components/map/MarkMapDataEditor' import TrendLine from '@/views/chart/components/senior/TrendLine' import ChartTitleUpdate from './ChartTitleUpdate' +import DataForecast from '@/views/chart/components/senior/DataForecast' export default { name: 'ChartEdit', components: { + DataForecast, PositionAdjust, ScrollCfg, CalcChartFieldEdit, @@ -2191,6 +2205,9 @@ export default { showTrendLineCfg() { return this.view.render === 'antv' && equalsAny(this.view.type, 'line') }, + showDataForecastCfg() { + return this.view.render === 'antv' && equalsAny(this.view.type, 'line', 'bar') + }, showThresholdCfg() { if (this.view.type === 'bidirectional-bar') { return false @@ -3077,7 +3094,10 @@ export default { this.view.senior.trendLine = val this.calcData() }, - + onForecastChange(val) { + this.view.senior.forecastCfg = val + this.calcData() + }, onThresholdChange(val) { this.view.senior.threshold = val this.calcData()