Merge pull request #9767 from dataease/pr@dev@feat_data_forecast

feat(视图): AntV 数据预测
This commit is contained in:
wisonic-s 2024-05-22 11:50:14 +08:00 committed by GitHub
commit ca4784e1ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 358 additions and 5 deletions

View File

@ -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()) {

View File

@ -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',

View File

@ -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: '僅編輯時生效',

View File

@ -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: ', 前往 ',

View File

@ -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

View File

@ -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)
// 处理空值

View File

@ -0,0 +1,246 @@
<template>
<div>
<el-form
:model="forecastCfg"
label-width="80px"
size="mini"
@submit.native.prevent
>
<el-form-item
class="form-item"
:label="$t('chart.forecast_enable')"
>
<el-checkbox
v-model="forecastCfg.enable"
@change="onForecastChange"
/>
</el-form-item>
<el-form-item
class="form-item"
:label="$t('chart.forecast_all_period')"
>
<el-checkbox
v-model="forecastCfg.allPeriod"
:disabled="!forecastCfg.enable"
@change="onForecastChange"
/>
<el-tooltip
class="item"
effect="dark"
placement="bottom"
>
<div
slot="content"
>
{{ $t('chart.forecast_all_period_tip') }}
</div>
<i
class="el-icon-info"
style="cursor: pointer;color: #606266;margin-left: 4px;"
/>
</el-tooltip>
</el-form-item>
<el-form-item
v-if="!forecastCfg.allPeriod"
class="form-item"
:label="$t('chart.forecast_training_period')"
>
<el-input-number
v-model="forecastCfg.trainingPeriod"
:disabled="!forecastCfg.enable"
:min="5"
size="mini"
@change="onForecastChange"
/>
<el-tooltip
class="item"
effect="dark"
placement="bottom"
>
<div
slot="content"
>{{ $t('chart.forecast_training_period_tip') }}
</div>
<i
class="el-icon-info"
style="cursor: pointer;color: #606266;margin-left: 4px;"
/>
</el-tooltip>
</el-form-item>
<el-form-item
class="form-item"
:label="$t('chart.forecast_period')"
>
<el-input-number
v-model="forecastCfg.period"
:disabled="!forecastCfg.enable"
:min="1"
size="mini"
@change="onForecastChange"
/>
</el-form-item>
<el-form-item
class="form-item"
:label="$t('chart.forecast_confidence_interval')"
>
<el-select
v-model="forecastCfg.ciType"
:disabled="!forecastCfg.enable"
@change="onForecastChange"
>
<el-option
v-for="item in ciOptions"
:key="item.name"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="forecastCfg.ciType === 'custom'"
class="form-item"
:label="$t('chart.custom_case')"
>
<el-input-number
v-model="forecastCfg.confidenceInterval"
:disabled="!forecastCfg.enable"
:max="0.99"
:min="0.75"
:step="0.01"
size="mini"
@change="onForecastChange"
/>
</el-form-item>
<el-form-item
class="form-item"
:label="$t('chart.forecast_model')"
>
<el-select
v-model="forecastCfg.algorithm"
:disabled="!forecastCfg.enable"
@change="onForecastChange"
>
<el-option
v-for="item in algorithmOptions"
:key="item.name"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="forecastCfg.algorithm === 'polynomial-regression'"
class="form-item"
:label="$t('chart.forecast_degree')"
>
<el-input-number
v-model="forecastCfg.degree"
:disabled="!forecastCfg.enable"
:max="10"
:min="1"
size="mini"
@change="onForecastChange"
/>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'DataForecast',
props: {
chart: {
required: true,
type: Object
}
},
data() {
return {
forecastCfg: {
enable: false,
period: 3,
allPeriod: true,
trainingPeriod: 120,
confidenceInterval: 0.95,
ciType: 0.95,
algorithm: 'linear-regression',
customCi: 0.95,
degree: 3
},
algorithmOptions: [
{ name: '线性回归', value: 'linear-regression' },
{ name: '多项式拟合', value: 'polynomial-regression' }
],
ciOptions: [
{ name: '90%', value: 0.90 },
{ name: '95%', value: 0.95 },
{ name: '99%', value: 0.99 },
{ name: '自定义', value: 'custom' }
]
}
},
watch: {
chart: {
handler: function() {
this.init()
}
}
},
mounted() {
this.init()
},
methods: {
init() {
const chart = JSON.parse(JSON.stringify(this.chart))
if (chart.senior) {
let senior = null
if (Object.prototype.toString.call(chart.senior) === '[object Object]') {
senior = JSON.parse(JSON.stringify(chart.senior))
} else {
senior = JSON.parse(chart.senior)
}
if (senior.forecastCfg) {
this.forecastCfg = senior.forecastCfg
}
}
},
onForecastChange() {
if (this.forecastCfg.ciType !== 'custom') {
this.forecastCfg.confidenceInterval = this.forecastCfg.ciType
}
this.$emit('onForecastChange', this.forecastCfg)
}
}
}
</script>
<style lang="scss" scoped>
.form-item-slider ::v-deep .el-form-item__label {
font-size: 12px;
line-height: 38px;
}
.form-item-range-slider ::v-deep .el-form-item__content {
padding-right: 6px
}
.form-item ::v-deep .el-form-item__label {
font-size: 12px;
}
.form-item ::v-deep .el-checkbox__label {
font-size: 12px;
}
.form-item ::v-deep .el-radio__label {
font-size: 12px;
}
.el-select-dropdown__item {
padding: 0 20px;
}
span {
font-size: 12px
}
</style>

View File

@ -1361,6 +1361,18 @@
@onTrendLineChange="onTrendLineChange"
/>
</el-collapse-item>
<el-collapse-item
v-if="showDataForecastCfg"
name="data-forecast"
title="数据预测"
>
<data-forecast
class="attr-selector"
:chart="chart"
:quota-data="view.yaxis"
@onForecastChange="onForecastChange"
/>
</el-collapse-item>
</el-collapse>
</el-row>
@ -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()