forked from github/dataease
Merge pull request #9767 from dataease/pr@dev@feat_data_forecast
feat(视图): AntV 数据预测
This commit is contained in:
commit
ca4784e1ce
@ -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()) {
|
||||
|
@ -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',
|
||||
|
@ -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: '僅編輯時生效',
|
||||
|
@ -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: ', 前往 ',
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
// 处理空值
|
||||
|
246
core/frontend/src/views/chart/components/senior/DataForecast.vue
Normal file
246
core/frontend/src/views/chart/components/senior/DataForecast.vue
Normal 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>
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user