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 414025dfac..2e9d20e30c 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 @@ -636,7 +636,9 @@ public class ChartViewService { if (StringUtils.equalsIgnoreCase(view.getType(), "table-pivot") || StringUtils.containsIgnoreCase(view.getType(), "group") || ("antv".equalsIgnoreCase(view.getRender()) && "line".equalsIgnoreCase(view.getType())) - || StringUtils.equalsIgnoreCase(view.getType(), "flow-map")) { + || StringUtils.equalsIgnoreCase(view.getType(), "flow-map") + || StringUtils.equalsIgnoreCase(view.getType(), "bar-time-range") + ) { xAxis.addAll(xAxisExt); } List yAxis = gson.fromJson(view.getYAxis(), tokenType); @@ -1421,6 +1423,8 @@ public class ChartViewService { mapChart = ChartDataBuild.transBaseGroupDataAntV(xAxisBase, xAxis, xAxisExt, yAxis, view, data, isDrill); } else if (StringUtils.equalsIgnoreCase(view.getType(), "bar-group-stack")) { mapChart = ChartDataBuild.transGroupStackDataAntV(xAxisBase, xAxis, xAxisExt, yAxis, extStack, data, view, isDrill); + } else if (StringUtils.equalsIgnoreCase(view.getType(), "bar-time-range")) { + mapChart = ChartDataBuild.transTimeBarDataAntV(xAxisBase, xAxis, xAxisExt, yAxis, extStack, data, view, isDrill); } else if (StringUtils.containsIgnoreCase(view.getType(), "bar-stack")) { mapChart = ChartDataBuild.transStackChartDataAntV(xAxis, yAxis, view, data, extStack, isDrill); } else if (StringUtils.containsIgnoreCase(view.getType(), "line-stack")) { diff --git a/core/backend/src/main/java/io/dataease/service/chart/util/ChartDataBuild.java b/core/backend/src/main/java/io/dataease/service/chart/util/ChartDataBuild.java index 786ef57552..239b8cdd07 100644 --- a/core/backend/src/main/java/io/dataease/service/chart/util/ChartDataBuild.java +++ b/core/backend/src/main/java/io/dataease/service/chart/util/ChartDataBuild.java @@ -11,6 +11,7 @@ import org.apache.commons.lang3.StringUtils; import java.math.BigDecimal; import java.math.RoundingMode; +import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; @@ -1282,6 +1283,120 @@ public class ChartDataBuild { } } + private static String getDateFormat(String dateStyle, String datePattern) { + String split; + if (StringUtils.equalsIgnoreCase(datePattern, "date_split")) { + split = "/"; + } else { + split = "-"; + } + switch (dateStyle) { + case "y": + return "yyyy"; + case "y_M": + return "yyyy" + split + "MM"; + case "y_M_d": + return "yyyy" + split + "MM" + split + "dd"; + case "H_m_s": + return "HH:mm:ss"; + case "y_M_d_H": + return "yyyy" + split + "MM" + split + "dd" + " HH"; + case "y_M_d_H_m": + return "yyyy" + split + "MM" + split + "dd" + " HH:mm"; + case "y_M_d_H_m_s": + return "yyyy" + split + "MM" + split + "dd" + " HH:mm:ss"; + default: + return "yyyy-MM-dd HH:mm:ss"; + } + } + + public static Map transTimeBarDataAntV(List xAxisBase, List xAxis, List xAxisExt, List yAxis, List extStack, List data, ChartViewWithBLOBs view, boolean isDrill) { + + Map map = new HashMap<>(); + if (CollectionUtils.isEmpty(xAxisBase) || CollectionUtils.isEmpty(xAxisExt) || xAxisExt.size() < 2) { + map.put("data", new ArrayList<>()); + return map; + } + if (xAxisExt.stream().filter(ext -> !ext.isDrill()).count() != 2) { + map.put("data", new ArrayList<>()); + return map; + } + + List dates = new ArrayList<>(); + + ChartViewFieldDTO xAxis1 = xAxis.get(xAxisBase.size()); + SimpleDateFormat sdf = new SimpleDateFormat(getDateFormat(xAxis1.getDateStyle(), xAxis1.getDatePattern())); + + List dataList = new ArrayList<>(); + for (int i1 = 0; i1 < data.size(); i1++) { + String[] row = data.get(i1); + + StringBuilder xField = new StringBuilder(); + if (isDrill) { + xField.append(row[xAxis.size() - 1]); + } else { + for (int i = 0; i < xAxisBase.size(); i++) { + if (i == xAxisBase.size() - 1) { + xField.append(row[i]); + } else { + xField.append(row[i]).append("\n"); + } + } + } + Map obj = new HashMap<>(); + obj.put("field", xField.toString()); + obj.put("category", xField.toString()); + + List dimensionList = new ArrayList<>(); + + for (int j = 0; j < xAxis.size(); j++) { + ChartDimensionDTO chartDimensionDTO = new ChartDimensionDTO(); + chartDimensionDTO.setId(xAxis.get(j).getId()); + chartDimensionDTO.setValue(row[j]); + dimensionList.add(chartDimensionDTO); + } + + obj.put("dimensionList", dimensionList); + + List values = new ArrayList<>(); + + if (row[xAxisBase.size()] == null || row[xAxisBase.size() + 1] == null) { + continue; + } + + values.add(row[xAxisBase.size()]); + values.add(row[xAxisBase.size() + 1]); + obj.put("values", values); + + + try { + Date date = sdf.parse(row[xAxisBase.size()]); + if (date != null) { + dates.add(date); + } + } catch (Exception ignore) { + } + try { + Date date = sdf.parse(row[xAxisBase.size() + 1]); + if (date != null) { + dates.add(date); + } + } catch (Exception ignore) { + } + + dataList.add(obj); + } + + map.put("minTime", sdf.format(dates.stream().min(Date::compareTo).orElse(null))); + + + map.put("maxTime", sdf.format(dates.stream().max(Date::compareTo).orElse(null))); + + map.put("data", dataList); + return map; + + } + public static Map transBidirectionalBarData(List xAxis, List yAxis, ChartViewDTO view, List data, boolean isDrill) { Map map = new HashMap<>(); diff --git a/core/frontend/src/icons/svg/bar-time-range.svg b/core/frontend/src/icons/svg/bar-time-range.svg new file mode 100644 index 0000000000..6ba83faae7 --- /dev/null +++ b/core/frontend/src/icons/svg/bar-time-range.svg @@ -0,0 +1,8 @@ + + + + Layer 1 + + + + \ No newline at end of file diff --git a/core/frontend/src/lang/en.js b/core/frontend/src/lang/en.js index 3483eec61a..85f2df9401 100644 --- a/core/frontend/src/lang/en.js +++ b/core/frontend/src/lang/en.js @@ -1184,6 +1184,7 @@ export default { chart_bar_stack: 'Stack Bar', chart_percentage_bar_stack: 'Percentage Stack Bar', chart_bar_horizontal: 'Horizontal Bar', + chart_bar_time_range: 'Time Bar', chart_bar_stack_horizontal: 'Stack Horizontal Bar', chart_percentage_bar_stack_horizontal: 'Horizontal Percentage Stack Bar', chart_bidirectional_bar: 'Bidirectional Bar', diff --git a/core/frontend/src/lang/tw.js b/core/frontend/src/lang/tw.js index 08e07bfbb2..d52d3f0218 100644 --- a/core/frontend/src/lang/tw.js +++ b/core/frontend/src/lang/tw.js @@ -1182,6 +1182,7 @@ export default { chart_bar_stack: '堆疊柱狀圖', chart_percentage_bar_stack: '百分比柱狀圖', chart_bar_horizontal: '橫嚮柱狀圖', + chart_bar_time_range: '時間條形圖', chart_bar_stack_horizontal: '橫嚮堆疊柱狀圖', chart_percentage_bar_stack_horizontal: '橫嚮百分比柱狀圖', chart_bidirectional_bar: '對稱柱狀圖', diff --git a/core/frontend/src/lang/zh.js b/core/frontend/src/lang/zh.js index 1d7b179209..8217e9d27a 100644 --- a/core/frontend/src/lang/zh.js +++ b/core/frontend/src/lang/zh.js @@ -1182,6 +1182,7 @@ export default { chart_bar_stack: '堆叠柱状图', chart_percentage_bar_stack: '百分比柱状图', chart_bar_horizontal: '横向柱状图', + chart_bar_time_range: '时间条形图', chart_bar_stack_horizontal: '横向堆叠柱状图', chart_percentage_bar_stack_horizontal: '横向百分比柱状图', chart_bidirectional_bar: '对称柱状图', 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 97b30a42d1..ee531aff15 100644 --- a/core/frontend/src/views/chart/chart/bar/bar_antv.js +++ b/core/frontend/src/views/chart/chart/bar/bar_antv.js @@ -262,6 +262,130 @@ export function hBaseBarOptionAntV(plot, container, chart, action, isGroup, isSt return plot } +export function timeRangeBarOptionAntV(plot, container, chart, action) { + // theme + const theme = getTheme(chart) + // attr + const label = getLabel(chart) + const tooltip = getTooltip(chart) + // style + const legend = getLegend(chart) + const yAxis = getXAxis(chart) + const xAxis = getYAxis(chart) + // data + const data = _.cloneDeep(chart.data.data) + + const minTime = chart.data.minTime + const maxTime = chart.data.maxTime + + // config + const slider = getSlider(chart) + const analyse = getAnalyse(chart) + // options + const options = { + theme: theme, + data: data, + xField: 'values', + yField: 'field', + seriesField: 'category', + appendPadding: getPadding(chart), + label: label, + tooltip: tooltip, + legend: legend, + xAxis: xAxis, + yAxis: yAxis, + slider: slider, + annotations: analyse, + isRange: true, + meta: { + values: { + type: 'time', + min: minTime, + max: maxTime + } + }, + brush: { + enabled: true, + isStartEnable: (context) => { + // 按住 shift 键,才能开启交互 + if (context.event.gEvent.originalEvent?.shiftKey) { + return true + } + return false + } + }, + interactions: [ + { + type: 'legend-active', cfg: { + start: [{ trigger: 'legend-item:mouseenter', action: ['element-active:reset'] }], + end: [{ trigger: 'legend-item:mouseleave', action: ['element-active:reset'] }] + } + }, + { + type: 'legend-filter', cfg: { + start: [{ trigger: 'legend-item:click', action: ['list-unchecked:toggle', 'data-filter:filter', 'element-active:reset', 'element-highlight:reset'] }] + } + }, + { + type: 'tooltip', cfg: { + start: [{ trigger: 'interval:mousemove', action: 'tooltip:show' }], + end: [{ trigger: 'interval:mouseleave', action: 'tooltip:hide' }] + } + }, + { + type: 'active-region', cfg: { + start: [{ trigger: 'interval:mousemove', action: 'active-region:show' }], + end: [{ trigger: 'interval:mouseleave', action: 'active-region:hide' }] + } + } + ] + } + // size + let customAttr = {} + if (chart.customAttr) { + customAttr = JSON.parse(chart.customAttr) + if (customAttr.size) { + const s = JSON.parse(JSON.stringify(customAttr.size)) + if (s.barDefault) { + delete options.marginRatio + } else { + options.marginRatio = s.barGap + } + } + } + + delete options.isGroup + delete options.isStack + + options.isPercent = chart.type.includes('percentage') + // custom color + options.color = antVCustomColor(chart) + if (customAttr.color.gradient) { + options.color = options.color.map((ele) => { + return setGradientColor(ele, customAttr.color.gradient) + }) + } + // 处理空值 + if (chart.senior) { + let emptyDataStrategy = JSON.parse(chart.senior)?.functionCfg?.emptyDataStrategy + if (!emptyDataStrategy) { + emptyDataStrategy = 'breakLine' + } + handleEmptyDataStrategy(emptyDataStrategy, chart, data, options) + } + + // 开始渲染 + if (plot) { + plot.destroy() + } + plot = new Bar(container, options) + + plot.off('interval:click') + plot.on('interval:click', action) + + return plot +} + export function baseBidirectionalBarOptionAntV(plot, container, chart, action, isGroup, isStack) { // theme const theme = getTheme(chart) diff --git a/core/frontend/src/views/chart/chart/common/common_antv.js b/core/frontend/src/views/chart/chart/common/common_antv.js index 32436edad7..44bf308322 100644 --- a/core/frontend/src/views/chart/chart/common/common_antv.js +++ b/core/frontend/src/views/chart/chart/common/common_antv.js @@ -422,7 +422,7 @@ export function getTooltip(chart) { res = valueFormatter(param.value, formatterItem) } } - } else if (includesAny(chart.type, 'bar', 'scatter', 'radar', 'area') && !chart.type.includes('group')) { + } else if (includesAny(chart.type, 'bar', 'scatter', 'radar', 'area') && !chart.type.includes('group') && chart.type !== 'bar-time-range') { obj = { name: param.category, value: param.value } for (let i = 0; i < yAxis.length; i++) { const f = yAxis[i] @@ -470,6 +470,9 @@ export function getTooltip(chart) { res = valueFormatter(param.value, formatterItem) } } + } else if (chart.type === 'bar-time-range') { + obj = { values: param.values, name: param.category } + res = param.values[0] + ' - ' + param.values[1] } else { res = param.value } @@ -724,7 +727,7 @@ export function getXAxis(chart) { delete axis.maxLimit delete axis.tickCount const axisValue = a.axisValue - if (chart.type.includes('horizontal')) { + if (chart.type.includes('horizontal') || chart.type === 'bar-time-range') { if (axisValue && !axisValue.auto) { const yAxisSeriesMaxList = [] const maxList = [] @@ -814,7 +817,7 @@ export function getYAxis(chart) { if (chart.type === 'waterfall') { return value } else { - if (!chart.type.includes('horizontal')) { + if (!(chart.type.includes('horizontal') || chart.type === 'bar-time-range')) { if (!a.axisLabelFormatter) { return valueFormatter(value, formatterItem) } else { @@ -841,7 +844,7 @@ export function getYAxis(chart) { delete axis.maxLimit delete axis.tickCount const axisValue = a.axisValue - if (!chart.type.includes('horizontal')) { + if (!chart.type.includes('horizontal') || chart.type === 'bar-time-range') { if (axisValue && !axisValue.auto) { const yAxisSeriesMaxList = [] const maxList = [] diff --git a/core/frontend/src/views/chart/chart/util.js b/core/frontend/src/views/chart/chart/util.js index 68b309c8f9..03f5f97c8b 100644 --- a/core/frontend/src/views/chart/chart/util.js +++ b/core/frontend/src/views/chart/chart/util.js @@ -1100,6 +1100,85 @@ export const TYPE_CONFIGS = [ ] } }, + { + render: 'antv', + category: 'chart.chart_type_compare', + value: 'bar-time-range', + title: 'chart.chart_bar_time_range', + icon: 'bar-time-range', + properties: [ + 'color-selector', + + 'label-selector-ant-v', + 'tooltip-selector-ant-v', + 'x-axis-selector-ant-v', + 'y-axis-selector-ant-v', + 'title-selector-ant-v', + 'legend-selector-ant-v' + ], + propertyInner: { + 'color-selector': [ + 'value', + 'colorPanel', + 'customColor', + 'gradient', + 'alpha' + ], + 'size-selector-ant-v': [ + 'barDefault', + 'barGap' + ], + 'label-selector-ant-v': [ + 'show', + 'fontSize', + 'color', + 'position-h' + ], + 'tooltip-selector-ant-v': [ + 'show', + 'textStyle' + ], + 'x-axis-selector-ant-v': [ + 'show', + 'position', + 'name', + 'nameTextStyle', + 'splitLine', + 'axisForm', + 'axisLabel' + ], + 'y-axis-selector-ant-v': [ + 'show', + 'position', + 'name', + 'nameTextStyle', + 'splitLine', + 'axisForm', + 'axisLabel' + ], + 'title-selector-ant-v': [ + 'show', + 'title', + 'fontSize', + 'color', + 'hPosition', + 'isItalic', + 'isBolder', + 'remarkShow', + 'fontFamily', + 'letterSpace', + 'fontShadow' + ], + 'legend-selector-ant-v': [ + 'show', + 'icon', + 'orient', + 'textStyle', + 'hPosition', + 'vPosition' + ] + } + }, { render: 'antv', category: 'chart.chart_type_compare', @@ -3517,7 +3596,7 @@ export function getColors(chart, colors, reset) { } } } - } else if ((includesAny(chart.type, 'bar', 'radar', 'area')) && !chart.type.includes('group')) { + } else if ((includesAny(chart.type, 'bar', 'radar', 'area')) && !chart.type.includes('group') && chart.type !== 'bar-time-range') { if (Object.prototype.toString.call(chart.yaxis) === '[object Array]') { series = JSON.parse(JSON.stringify(chart.yaxis)) } else { diff --git a/core/frontend/src/views/chart/components/ChartComponentG2.vue b/core/frontend/src/views/chart/components/ChartComponentG2.vue index 28f940d251..23f8ae5569 100644 --- a/core/frontend/src/views/chart/components/ChartComponentG2.vue +++ b/core/frontend/src/views/chart/components/ChartComponentG2.vue @@ -45,7 +45,7 @@ import { baseLiquid } from '@/views/chart/chart/liquid/liquid' import { uuid } from 'vue-uuid' import ViewTrackBar from '@/components/canvas/components/editor/ViewTrackBar' import { getRemark, hexColorToRGBA } from '@/views/chart/chart/util' -import { baseBarOptionAntV, hBaseBarOptionAntV, baseBidirectionalBarOptionAntV } from '@/views/chart/chart/bar/bar_antv' +import { baseBarOptionAntV, hBaseBarOptionAntV, baseBidirectionalBarOptionAntV, timeRangeBarOptionAntV } from '@/views/chart/chart/bar/bar_antv' import { baseAreaOptionAntV, baseLineOptionAntV } from '@/views/chart/chart/line/line_antv' import { basePieOptionAntV, basePieRoseOptionAntV } from '@/views/chart/chart/pie/pie_antv' import { baseScatterOptionAntV } from '@/views/chart/chart/scatter/scatter_antv' @@ -273,6 +273,8 @@ export default { this.myChart = hBaseBarOptionAntV(this.myChart, this.chartId, chart, this.antVAction, true, false) } else if (equalsAny(chart.type, 'bar-stack-horizontal', 'percentage-bar-stack-horizontal')) { this.myChart = hBaseBarOptionAntV(this.myChart, this.chartId, chart, this.antVAction, false, true) + } else if (equalsAny(chart.type, 'bar-time-range')) { + this.myChart = timeRangeBarOptionAntV(this.myChart, this.chartId, chart, this.antVAction) } else if (chart.type === 'line') { this.myChart = baseLineOptionAntV(this.myChart, this.chartId, chart, this.antVAction) } else if (chart.type === 'area') { @@ -389,8 +391,12 @@ export default { } return } - const quotaList = this.pointParam.data.quotaList - quotaList[0]['value'] = this.pointParam.data.value + let quotaList = this.pointParam.data.quotaList + if (this.chart.type === 'bar-time-range') { + quotaList = this.pointParam.data.dimensionList + } else { + quotaList[0]['value'] = this.pointParam.data.value + } const linkageParam = { option: 'linkage', name: this.pointParam.data.name, diff --git a/core/frontend/src/views/chart/components/componentStyle/XAxisSelectorAntV.vue b/core/frontend/src/views/chart/components/componentStyle/XAxisSelectorAntV.vue index b658e8cc66..11c66de5ce 100644 --- a/core/frontend/src/views/chart/components/componentStyle/XAxisSelectorAntV.vue +++ b/core/frontend/src/views/chart/components/componentStyle/XAxisSelectorAntV.vue @@ -28,14 +28,18 @@ size="mini" @change="changeXAxisStyle('position')" > -
- {{ $t('chart.text_pos_top') }} - {{ $t('chart.text_pos_bottom') }} -
-
+
{{ $t('chart.text_pos_left') }} {{ $t('chart.text_pos_center') }}
+
+ {{ $t('chart.text_pos_left') }} + {{ $t('chart.text_pos_right') }} +
+
+ {{ $t('chart.text_pos_top') }} + {{ $t('chart.text_pos_bottom') }} +
diff --git a/core/frontend/src/views/chart/components/componentStyle/YAxisSelectorAntV.vue b/core/frontend/src/views/chart/components/componentStyle/YAxisSelectorAntV.vue index acfede6f0f..4bbcbe7199 100644 --- a/core/frontend/src/views/chart/components/componentStyle/YAxisSelectorAntV.vue +++ b/core/frontend/src/views/chart/components/componentStyle/YAxisSelectorAntV.vue @@ -28,14 +28,18 @@ size="mini" @change="changeYAxisStyle('position')" > -
- {{ $t('chart.text_pos_left') }} - {{ $t('chart.text_pos_right') }} -
-
+
{{ $t('chart.text_pos_top') }} {{ $t('chart.text_pos_bottom') }}
+
+ {{ $t('chart.text_pos_top') }} + {{ $t('chart.text_pos_bottom') }} +
+
+ {{ $t('chart.text_pos_left') }} + {{ $t('chart.text_pos_right') }} +
- + @@ -755,7 +756,7 @@ @@ -2122,7 +2123,7 @@ export default { return equalsAny(this.view.type, 'table-normal', 'table-info') }, showAnalyseCfg() { - if (this.view.type === 'bidirectional-bar') { + if (this.view.type === 'bidirectional-bar' || this.view.type === 'bar-time-range') { return false } return includesAny(this.view.type, 'bar', 'line', 'area', 'gauge', 'liquid') || @@ -2502,7 +2503,7 @@ export default { } }) if (equalsAny(view.type, 'table-pivot', 'bar-group', 'bar-group-stack', 'flow-map', 'race-bar') || - (view.render === 'antv' && (view.type === 'line' || view.type === 'scatter'))) { + (view.render === 'antv' && (view.type === 'line' || view.type === 'scatter' || view.type === 'bar-time-range'))) { view.xaxisExt.forEach(function(ele) { if (!ele.dateStyle || ele.dateStyle === '') { ele.dateStyle = 'y_M_d' @@ -3310,6 +3311,19 @@ export default { if (this.view.type !== 'table-info') { this.dragCheckType(this.view.xaxisExt, 'd') } + if (this.view.type === 'bar-time-range') { + // 针对时间条形图,需要限定类型为时间类型 + if (this.view.xaxisExt && this.view.xaxisExt.length > 0) { + for (let i = this.view.xaxisExt.length - 1; i >= 0; i--) { + if (this.view.xaxisExt[i].deType !== 1) { + this.view.xaxisExt.splice(i, 1) + } + } + } + if (this.view.xaxisExt.length > 2) { + this.view.xaxisExt = [this.view.xaxisExt[0], this.view.xaxisExt[1]] + } + } if ((this.view.type === 'map' || this.view.type === 'word-cloud' || this.view.type === 'scatter') && this.view.xaxisExt.length > 1) { this.view.xaxisExt = [this.view.xaxisExt[0]] } diff --git a/core/frontend/src/views/chart/view/ChartStyle.vue b/core/frontend/src/views/chart/view/ChartStyle.vue index 696b1f4580..ad750844b7 100644 --- a/core/frontend/src/views/chart/view/ChartStyle.vue +++ b/core/frontend/src/views/chart/view/ChartStyle.vue @@ -471,13 +471,13 @@ export default { return false }, xAisTitle() { - if (this.chart.type === 'bidirectional-bar') { + if (this.chart.type === 'bidirectional-bar' || this.chart.type === 'bar-time-range') { return this.$t('chart.yAxis') } return this.$t('chart.xAxis') }, yAxisTitle() { - if (this.chart.type === 'bidirectional-bar') { + if (this.chart.type === 'bidirectional-bar' || this.chart.type === 'bar-time-range') { return this.$t('chart.xAxis') } if (this.chart.type === 'chart-mix') {