From 9c600ec0e146db220dfd5d54c6d492e3597667ba Mon Sep 17 00:00:00 2001 From: ulleo Date: Fri, 10 May 2024 16:09:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E8=A1=A8):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=8C=BA=E9=97=B4=E6=9D=A1=E5=BD=A2=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8930 --- .../chart/dao/auto/entity/CoreChartView.java | 24 +- .../dao/auto/mapper/CoreChartViewMapper.java | 4 +- .../chart/manage/ChartDataManage.java | 32 ++ .../dataease/chart/utils/ChartDataBuild.java | 268 ++++++++++ .../main/resources/i18n/core_en_US.properties | 7 + .../main/resources/i18n/core_zh_CN.properties | 7 + .../main/resources/i18n/core_zh_TW.properties | 11 +- .../src/assets/svg/bar-range-dark.svg | 7 + .../src/assets/svg/bar-range.svg | 7 + core/core-frontend/src/locales/zh-CN.ts | 8 + .../src/models/chart/chart-attr.d.ts | 4 + .../core-frontend/src/models/chart/chart.d.ts | 1 + .../editor/drag-item/DimensionItem.vue | 3 + .../editor-style/components/LabelSelector.vue | 28 +- .../components/TooltipSelector.vue | 26 +- .../editor-style/components/XAxisSelector.vue | 16 +- .../views/chart/components/editor/index.vue | 485 +++++++++++++----- .../chart/components/editor/util/chart.ts | 7 + .../components/js/panel/charts/bar/common.ts | 13 + .../js/panel/charts/bar/range-bar.ts | 383 ++++++++++++++ .../js/panel/charts/others/chart-mix.ts | 2 + .../js/panel/charts/others/indicator.ts | 1 + .../views/components/ChartComponentG2Plot.vue | 8 +- .../api/chart/dto/ChartViewBaseDTO.java | 5 + 24 files changed, 1230 insertions(+), 127 deletions(-) create mode 100644 core/core-frontend/src/assets/svg/bar-range-dark.svg create mode 100644 core/core-frontend/src/assets/svg/bar-range.svg create mode 100644 core/core-frontend/src/views/chart/components/js/panel/charts/bar/range-bar.ts diff --git a/core/core-backend/src/main/java/io/dataease/chart/dao/auto/entity/CoreChartView.java b/core/core-backend/src/main/java/io/dataease/chart/dao/auto/entity/CoreChartView.java index 3f2e7eba51..a3444089e9 100644 --- a/core/core-backend/src/main/java/io/dataease/chart/dao/auto/entity/CoreChartView.java +++ b/core/core-backend/src/main/java/io/dataease/chart/dao/auto/entity/CoreChartView.java @@ -5,11 +5,11 @@ import java.io.Serializable; /** *

- * + * 组件图表表 *

* * @author fit2cloud - * @since 2023-08-20 + * @since 2024-05-07 */ @TableName("core_chart_view") public class CoreChartView implements Serializable { @@ -191,10 +191,21 @@ public class CoreChartView implements Serializable { */ private Boolean jumpActive; + /** + * 复制来源 + */ private Long copyFrom; + /** + * 复制ID + */ private Long copyId; + /** + * 区间条形图开启时间纬度开启聚合 + */ + private Boolean aggregate; + public Long getId() { return id; } @@ -491,6 +502,14 @@ public class CoreChartView implements Serializable { this.copyId = copyId; } + public Boolean getAggregate() { + return aggregate; + } + + public void setAggregate(Boolean aggregate) { + this.aggregate = aggregate; + } + @Override public String toString() { return "CoreChartView{" + @@ -531,6 +550,7 @@ public class CoreChartView implements Serializable { ", jumpActive = " + jumpActive + ", copyFrom = " + copyFrom + ", copyId = " + copyId + + ", aggregate = " + aggregate + "}"; } } diff --git a/core/core-backend/src/main/java/io/dataease/chart/dao/auto/mapper/CoreChartViewMapper.java b/core/core-backend/src/main/java/io/dataease/chart/dao/auto/mapper/CoreChartViewMapper.java index 54751e3d67..372e70703d 100644 --- a/core/core-backend/src/main/java/io/dataease/chart/dao/auto/mapper/CoreChartViewMapper.java +++ b/core/core-backend/src/main/java/io/dataease/chart/dao/auto/mapper/CoreChartViewMapper.java @@ -6,11 +6,11 @@ import org.apache.ibatis.annotations.Mapper; /** *

- * Mapper 接口 + * 组件图表表 Mapper 接口 *

* * @author fit2cloud - * @since 2023-08-20 + * @since 2024-05-07 */ @Mapper public interface CoreChartViewMapper extends BaseMapper { diff --git a/core/core-backend/src/main/java/io/dataease/chart/manage/ChartDataManage.java b/core/core-backend/src/main/java/io/dataease/chart/manage/ChartDataManage.java index 0a41ee598c..b91720a599 100644 --- a/core/core-backend/src/main/java/io/dataease/chart/manage/ChartDataManage.java +++ b/core/core-backend/src/main/java/io/dataease/chart/manage/ChartDataManage.java @@ -34,6 +34,7 @@ import io.dataease.utils.JsonUtil; import jakarta.annotation.Resource; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -127,6 +128,35 @@ public class ChartDataManage { List yAxisExt = new ArrayList<>(view.getYAxisExt()); yAxis.addAll(yAxisExt); } + boolean skipBarRange = false; + boolean barRangeDate = false; + if (StringUtils.equalsIgnoreCase(view.getType(), "bar-range")) { //针对区间条形图进行处理 + yAxis.clear(); + if (CollectionUtils.isNotEmpty(view.getYAxis()) && CollectionUtils.isNotEmpty(view.getYAxisExt())) { + ChartViewFieldDTO axis1 = view.getYAxis().get(0); + ChartViewFieldDTO axis2 = view.getYAxisExt().get(0); + + if (StringUtils.equalsIgnoreCase(axis1.getGroupType(), "q") && StringUtils.equalsIgnoreCase(axis2.getGroupType(), "q")) { + yAxis.add(axis1); + yAxis.add(axis2); + } else if (StringUtils.equalsIgnoreCase(axis1.getGroupType(), "d") && axis1.getDeType() == 1 && StringUtils.equalsIgnoreCase(axis2.getGroupType(), "d") && axis2.getDeType() == 1) { + barRangeDate = true; + if (BooleanUtils.isTrue(view.getAggregate())) { + axis1.setSummary("min"); + axis2.setSummary("max"); + yAxis.add(axis1); + yAxis.add(axis2); + } else { + xAxis.add(axis1); + xAxis.add(axis2); + } + } else { + skipBarRange = true; + } + + } + } + List extStack = new ArrayList<>(view.getExtStack()); List extBubble = new ArrayList<>(view.getExtBubble()); if (ObjectUtils.isNotEmpty(view.getExtLabel()) && enableExtData(view.getType())) { @@ -753,6 +783,8 @@ public class ChartDataManage { mapChart = ChartDataBuild.transLabelChartData(xAxis, yAxis, view, data, isDrill); } else if (StringUtils.containsIgnoreCase(view.getType(), "quadrant")) { mapChart = ChartDataBuild.transQuadrantDataAntV(xAxis, yAxis, view, data, extBubble, isDrill); + } else if (StringUtils.equalsIgnoreCase(view.getType(), "bar-range")) { + mapChart = ChartDataBuild.transTimeBarDataAntV(skipBarRange, barRangeDate, xAxisBase, xAxis, yAxis, view, data, isDrill); } else { mapChart = ChartDataBuild.transChartDataAntV(xAxis, yAxis, view, data, isDrill); } diff --git a/core/core-backend/src/main/java/io/dataease/chart/utils/ChartDataBuild.java b/core/core-backend/src/main/java/io/dataease/chart/utils/ChartDataBuild.java index ff6a09546a..a6575614de 100644 --- a/core/core-backend/src/main/java/io/dataease/chart/utils/ChartDataBuild.java +++ b/core/core-backend/src/main/java/io/dataease/chart/utils/ChartDataBuild.java @@ -1,13 +1,17 @@ package io.dataease.chart.utils; import io.dataease.api.chart.dto.*; +import io.dataease.i18n.Lang; +import io.dataease.i18n.Translator; import io.dataease.utils.IDUtils; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; 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; @@ -1323,4 +1327,268 @@ public class ChartDataBuild { return map; } + public static Map transTimeBarDataAntV(boolean skipBarRange, boolean isDate, List xAxisBase, List xAxis, List yAxis, ChartViewDTO view, List data, boolean isDrill) { + + Map map = new HashMap<>(); + if (skipBarRange) { + map.put("data", new ArrayList<>()); + return map; + } + + List dates = new ArrayList<>(); + List numbers = new ArrayList<>(); + + ChartViewFieldDTO dateAxis1 = null; + + SimpleDateFormat sdf = null; + if (isDate) { + if (BooleanUtils.isTrue(view.getAggregate())) { + dateAxis1 = yAxis.get(0); + } else { + dateAxis1 = xAxis.get(xAxisBase.size()); + } + sdf = new SimpleDateFormat(getDateFormat(dateAxis1.getDateStyle(), dateAxis1.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 i = 0; i < xAxisBase.size(); i++) { + ChartDimensionDTO chartDimensionDTO = new ChartDimensionDTO(); + chartDimensionDTO.setId(xAxis.get(i).getId()); + chartDimensionDTO.setValue(row[i]); + dimensionList.add(chartDimensionDTO); + } + if (isDrill) { + int index = xAxis.size() - 1; + ChartDimensionDTO chartDimensionDTO = new ChartDimensionDTO(); + chartDimensionDTO.setId(xAxis.get(index).getId()); + chartDimensionDTO.setValue(row[index]); + dimensionList.add(chartDimensionDTO); + } + obj.put("dimensionList", dimensionList); + + + List values = new ArrayList<>(); + + if (row[xAxisBase.size()] == null || row[xAxisBase.size() + 1] == null) { + continue; + } + + if (isDate) { + int index; + if (BooleanUtils.isTrue(view.getAggregate())) { + index = xAxis.size(); + } else { + index = xAxisBase.size(); + } + + values.add(row[index]); + values.add(row[index + 1]); + obj.put("values", values); + Date date1 = null, date2 = null; + try { + date1 = sdf.parse(row[index]); + if (date1 != null) { + dates.add(date1); + } + } catch (Exception ignore) { + } + try { + date2 = sdf.parse(row[index + 1]); + if (date2 != null) { + dates.add(date2); + } + } catch (Exception ignore) { + } + //间隔时间 + obj.put("gap", getTimeGap(date1, date2, dateAxis1.getDateStyle())); + + } else { + values.add(new BigDecimal(row[xAxis.size()])); + values.add(new BigDecimal(row[xAxis.size() + 1])); + obj.put("values", values); + + numbers.add(new BigDecimal(row[xAxis.size()])); + numbers.add(new BigDecimal(row[xAxis.size() + 1])); + + //间隔差 + obj.put("gap", new BigDecimal(row[xAxis.size() + 1]).subtract(new BigDecimal(row[xAxis.size()]))); + } + + dataList.add(obj); + } + + if (isDate) { + Date minDate = dates.stream().min(Date::compareTo).orElse(null); + if (minDate != null) { + map.put("minTime", sdf.format(minDate)); + } + Date maxDate = dates.stream().max(Date::compareTo).orElse(null); + if (maxDate != null) { + map.put("maxTime", sdf.format(maxDate)); + } + } else { + map.put("min", numbers.stream().min(BigDecimal::compareTo).orElse(null)); + map.put("max", numbers.stream().max(BigDecimal::compareTo).orElse(null)); + } + + map.put("isDate", isDate); + map.put("data", dataList); + return map; + + } + + 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"; + } + } + + private static String getTimeGap(Date from, Date to, String dateStyle) { + if (from == null || to == null) { + return ""; + } + Calendar fromCalender = Calendar.getInstance(); + fromCalender.setTime(from); + + Calendar toCalender = Calendar.getInstance(); + toCalender.setTime(to); + + long yearGap = 0; + long monthGap = 0; + long dayGap = (toCalender.getTimeInMillis() - fromCalender.getTimeInMillis()) / (1000 * 3600 * 24); + long hourGap = ((toCalender.getTimeInMillis() - fromCalender.getTimeInMillis()) / (1000 * 3600)) % 24; + long minuteGap = ((toCalender.getTimeInMillis() - fromCalender.getTimeInMillis()) / (1000 * 60)) % 60; + long secondGap = ((toCalender.getTimeInMillis() - fromCalender.getTimeInMillis()) / 1000) % 60; + + String language = "zh-CN"; //国际化 + Lang lang = Lang.getLangWithoutDefault(language); + boolean isEnUs = Lang.en_US.equals(lang); + String splitter = isEnUs ? " " : ""; + + String yearGapStr = ""; + String monthGapStr = ""; + + String dayGapStr = ""; + if (dayGap != 0) { + dayGapStr = dayGap + splitter + Translator.get("i18n_day") + (isEnUs && dayGap != 1 ? "s" : ""); + } + String hourGapStr = ""; + if (hourGap != 0) { + hourGapStr = hourGap + splitter + Translator.get("i18n_hour") + (isEnUs && hourGap != 1 ? "s" : ""); + } + String minuteGapStr = ""; + if (minuteGap != 0) { + minuteGapStr = minuteGap + splitter + Translator.get("i18n_minute") + (isEnUs && minuteGap != 1 ? "s" : ""); + } + String secondGapStr = ""; + if (secondGap != 0) { + secondGapStr = secondGap + splitter + Translator.get("i18n_second") + (isEnUs && secondGap != 1 ? "s" : ""); + } + + List list = new ArrayList<>(); + + switch (dateStyle) { + case "y": + yearGap = toCalender.get(Calendar.YEAR) - fromCalender.get(Calendar.YEAR); + yearGapStr = yearGap == 0 ? "" : (yearGap + splitter + Translator.get("i18n_year") + (isEnUs && yearGap != 1 ? "s" : "")); + return yearGapStr; + case "y_M": + yearGap = ((toCalender.get(Calendar.YEAR) - fromCalender.get(Calendar.YEAR)) * 12L + (toCalender.get(Calendar.MONTH) - fromCalender.get(Calendar.MONTH))) / 12; + monthGap = ((toCalender.get(Calendar.YEAR) - fromCalender.get(Calendar.YEAR)) * 12L + (toCalender.get(Calendar.MONTH) - fromCalender.get(Calendar.MONTH))) % 12; + + yearGapStr = yearGap == 0 ? "" : (yearGap + splitter + Translator.get("i18n_year") + (isEnUs && yearGap != 1 ? "s" : "")); + monthGapStr = monthGap == 0 ? "" : (monthGap + splitter + Translator.get("i18n_month") + (isEnUs && monthGap != 1 ? "s" : "")); + + if (!yearGapStr.isEmpty()) { + list.add(yearGapStr); + } + if (!monthGapStr.isEmpty()) { + list.add(monthGapStr); + } + return StringUtils.join(list, splitter); + case "y_M_d": + return dayGapStr; + case "y_M_d_H": + if (!dayGapStr.isEmpty()) { + list.add(dayGapStr); + } + if (!hourGapStr.isEmpty()) { + list.add(hourGapStr); + } + return StringUtils.join(list, splitter); + case "y_M_d_H_m": + if (!dayGapStr.isEmpty()) { + list.add(dayGapStr); + } + if (!hourGapStr.isEmpty()) { + list.add(hourGapStr); + } + if (!minuteGapStr.isEmpty()) { + list.add(minuteGapStr); + } + return StringUtils.join(list, splitter); + case "H_m_s": + case "y_M_d_H_m_s": + if (!dayGapStr.isEmpty()) { + list.add(dayGapStr); + } + if (!hourGapStr.isEmpty()) { + list.add(hourGapStr); + } + if (!minuteGapStr.isEmpty()) { + list.add(minuteGapStr); + } + if (!secondGapStr.isEmpty()) { + list.add(secondGapStr); + } + return StringUtils.join(list, splitter); + default: + return ""; + } + } + } diff --git a/core/core-backend/src/main/resources/i18n/core_en_US.properties b/core/core-backend/src/main/resources/i18n/core_en_US.properties index babe1471e4..44535ed761 100644 --- a/core/core-backend/src/main/resources/i18n/core_en_US.properties +++ b/core/core-backend/src/main/resources/i18n/core_en_US.properties @@ -51,3 +51,10 @@ i18n_error_login_type=error login type i18n_schema_is_empty=Schema is empty! i18n_table_name_repeat=Has duplicate name: i18n_sql_not_empty=SQL cannot be empty! + +i18n_year=Year +i18n_month=Month +i18n_day=Day +i18n_hour=Hour +i18n_minute=Minute +i18n_second=Second diff --git a/core/core-backend/src/main/resources/i18n/core_zh_CN.properties b/core/core-backend/src/main/resources/i18n/core_zh_CN.properties index 4fc8609f59..02954f501e 100644 --- a/core/core-backend/src/main/resources/i18n/core_zh_CN.properties +++ b/core/core-backend/src/main/resources/i18n/core_zh_CN.properties @@ -64,3 +64,10 @@ i18n_sql_not_empty=sql \u4E0D\u80FD\u4E3A\u7A7A i18n_menu.parameter=\u7CFB\u7EDF\u53C2\u6570 i18n_user_old_pwd_error=\u539F\u59CB\u5BC6\u7801\u9519\u8BEF i18n_menu.toolbox-log=\u64CD\u4F5C\u65E5\u5FD7 + +i18n_year=\u5E74 +i18n_month=\u6708 +i18n_day=\u5929 +i18n_hour=\u5C0F\u65F6 +i18n_minute=\u5206\u949F +i18n_second=\u79D2 diff --git a/core/core-backend/src/main/resources/i18n/core_zh_TW.properties b/core/core-backend/src/main/resources/i18n/core_zh_TW.properties index 62880d8aad..4dac5fb8d4 100644 --- a/core/core-backend/src/main/resources/i18n/core_zh_TW.properties +++ b/core/core-backend/src/main/resources/i18n/core_zh_TW.properties @@ -31,7 +31,7 @@ i18n_union_field_can_not_empty=\u95DC\u806F\u5B57\u6BB5\u4E0D\u80FD\u70BA\u7A7A i18n_table_duplicate=\u76F8\u540C\u7BC0\u9EDE\u9700\u91CD\u65B0\u62D6\u5165\u624D\u80FD\u7E7C\u7E8C\u65B0\u5EFA\u6578\u64DA\u96C6 i18n_no_column_permission=\u6C92\u6709\u5217\u6B0A\u9650 i18n_fetch_error=SQL\u57F7\u884C\u5931\u6557\uFF0C\u8ACB\u6AA2\u67E5\u8868\u3001\u5B57\u6BB5\u3001\u95DC\u806F\u95DC\u7CFB\u7B49\u4FE1\u606F\u662F\u5426\u6B63\u78BA\u4E26\u91CD\u65B0\u7DE8\u8F2F\u3002 -i18n_no_datasource_permission=\u65e0\u6570\u636e\u6e90\u8bbf\u95ee\u6743\u9650 +i18n_no_datasource_permission=\u65E0\u6570\u636E\u6E90\u8BBF\u95EE\u6743\u9650 i18n_field_circular_ref=\u5B57\u6BB5\u5B58\u5728\u5FAA\u74B0\u5F15\u7528 @@ -51,4 +51,11 @@ i18n_login_name_pwd_err=\u7528\u6236\u540D\u6216\u5BC6\u78BC\u932F\u8AA4 i18n_error_login_type=\u767B\u9304\u985E\u578B\u932F\u8AA4 i18n_schema_is_empty=schema\u70BA\u7A7A\uFF01 i18n_table_name_repeat=\u540D\u7A31\u91CD\u8907: -i18n_sql_not_empty=sql\u4e0d\u80fd\u70ba\u7a7a +i18n_sql_not_empty=sql\u4E0D\u80FD\u70BA\u7A7A + +i18n_year=\u5E74 +i18n_month=\u6708 +i18n_day=\u5929 +i18n_hour=\u5C0F\u6642 +i18n_minute=\u5206\u9418 +i18n_second=\u79D2 diff --git a/core/core-frontend/src/assets/svg/bar-range-dark.svg b/core/core-frontend/src/assets/svg/bar-range-dark.svg new file mode 100644 index 0000000000..484d1e1efd --- /dev/null +++ b/core/core-frontend/src/assets/svg/bar-range-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/core/core-frontend/src/assets/svg/bar-range.svg b/core/core-frontend/src/assets/svg/bar-range.svg new file mode 100644 index 0000000000..2f3e88ed11 --- /dev/null +++ b/core/core-frontend/src/assets/svg/bar-range.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/core/core-frontend/src/locales/zh-CN.ts b/core/core-frontend/src/locales/zh-CN.ts index 12ffe370e8..c5b0e64b3c 100644 --- a/core/core-frontend/src/locales/zh-CN.ts +++ b/core/core-frontend/src/locales/zh-CN.ts @@ -503,6 +503,8 @@ export default { data_preview: '数据预览', dimension: '维度', quota: '指标', + time_dimension_or_quota: '时间维度或指标', + aggregate_time: '聚合时间纬度', title: '标题', show: '显示', chart_type: '图表类型', @@ -678,6 +680,7 @@ export default { chart_bar_horizontal: '横向柱状图', chart_bar_stack_horizontal: '横向堆叠柱状图', chart_percentage_bar_stack_horizontal: '横向百分比柱状图', + chart_bar_range: '区间条形图', chart_line: '基础折线图', chart_area_stack: '堆叠折线图', chart_pie: '饼图', @@ -760,6 +763,8 @@ export default { chart_style: '样式', drag_block_type_axis: '类别轴', drag_block_value_axis: '值轴', + drag_block_value_start: '开始值', + drag_block_value_end: '结束值', drag_block_value_axis_left: '左值轴', drag_block_value_axis_right: '右值轴', drag_block_table_data_column: '数据列', @@ -939,6 +944,7 @@ export default { value_formatter_unit: '数量单位', value_formatter_decimal_count: '小数位数', value_formatter_suffix: '单位后缀', + show_gap: '显示间隔值', indicator_suffix_placeholder: '请输入1-10个字符', indicator_suffix: '后缀', indicator_value: '指标值', @@ -1101,6 +1107,8 @@ export default { error_not_number: '不支持拖拽非数值类型指标', error_q_2_d: '不支持拖拽指标至维度', error_d_2_q: '不支持拖拽维度至指标', + error_d_not_time_2_q: '不支持拖拽非时间类型的维度', + error_bar_range_axis_type_not_equal: '开始值与结束值需要设置相同类型', only_input_number: '请输入正确数值', value_min_max_invalid: '最小值必须小于最大值', add_assist_line: '添加辅助线', diff --git a/core/core-frontend/src/models/chart/chart-attr.d.ts b/core/core-frontend/src/models/chart/chart-attr.d.ts index cd2f745c11..19117984b1 100644 --- a/core/core-frontend/src/models/chart/chart-attr.d.ts +++ b/core/core-frontend/src/models/chart/chart-attr.d.ts @@ -659,6 +659,8 @@ declare interface ChartLabelAttr { * 多系列标签设置 */ seriesLabelFormatter: SeriesFormatter[] + + showGap?: boolean } /** * 提示设置 @@ -690,6 +692,8 @@ declare interface ChartTooltipAttr { * 多系列提示设置 */ seriesTooltipFormatter: SeriesFormatter[] + + showGap?: boolean } /** diff --git a/core/core-frontend/src/models/chart/chart.d.ts b/core/core-frontend/src/models/chart/chart.d.ts index 4654cdeebb..6dce0c7ad1 100644 --- a/core/core-frontend/src/models/chart/chart.d.ts +++ b/core/core-frontend/src/models/chart/chart.d.ts @@ -40,6 +40,7 @@ declare interface Chart { resultCount: number linkageActive: boolean jumpActive: boolean + aggregate?: boolean } declare type CustomAttr = DeepPartial | JSONString> declare type CustomStyle = DeepPartial | JSONString> diff --git a/core/core-frontend/src/views/chart/components/editor/drag-item/DimensionItem.vue b/core/core-frontend/src/views/chart/components/editor/drag-item/DimensionItem.vue index f32ab2b064..619c8078d4 100644 --- a/core/core-frontend/src/views/chart/components/editor/drag-item/DimensionItem.vue +++ b/core/core-frontend/src/views/chart/components/editor/drag-item/DimensionItem.vue @@ -110,6 +110,7 @@ const sort = param => { item.value.index = props.index item.value.sort = param.type item.value.customSort = [] + delete item.value.axisType emit('onDimensionItemChange', item.value) } } @@ -122,6 +123,7 @@ const beforeSort = type => { const dateStyle = param => { item.value.dateStyle = param.type + item.value.axisType = props.type emit('onDimensionItemChange', item.value) } @@ -133,6 +135,7 @@ const beforeDateStyle = type => { const datePattern = param => { item.value.datePattern = param.type + item.value.axisType = props.type emit('onDimensionItemChange', item.value) } diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/LabelSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/LabelSelector.vue index e1c1b0f3c4..72ebce865e 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/LabelSelector.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/LabelSelector.vue @@ -204,8 +204,23 @@ const showSeriesLabelFormatter = computed(() => { }) const showDivider = computed(() => { const DIVIDER_PROPS = ['labelFormatter', 'showDimension', 'showQuota', 'showProportion'] - return includesAny(props.propertyInner, ...DIVIDER_PROPS) + return includesAny(props.propertyInner, ...DIVIDER_PROPS) && !isBarRangeTime.value }) + +const isBarRangeTime = computed(() => { + if (props.chart.type === 'bar-range') { + const tempYAxis = props.chart.yAxis[0] + const tempYAxisExt = props.chart.yAxisExt[0] + if ( + (tempYAxis && tempYAxis.groupType === 'd') || + (tempYAxisExt && tempYAxisExt.groupType === 'd') + ) { + return true + } + } + return false +}) + onMounted(() => { init() }) @@ -329,7 +344,7 @@ onMounted(() => { :class="{ 'divider-dark': themes === 'dark' }" v-if="showDivider" /> - diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/TooltipSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/TooltipSelector.vue index eed42e7a71..7baabca4fd 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/TooltipSelector.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/TooltipSelector.vue @@ -151,6 +151,21 @@ const aggregationList = computed(() => { } return AGGREGATION_TYPE }) + +const isBarRangeTime = computed(() => { + if (props.chart.type === 'bar-range') { + const tempYAxis = props.chart.yAxis[0] + const tempYAxisExt = props.chart.yAxisExt[0] + if ( + (tempYAxis && tempYAxis.groupType === 'd') || + (tempYAxisExt && tempYAxisExt.groupType === 'd') + ) { + return true + } + } + return false +}) + watch( [() => props.chart.customAttr.tooltip, () => props.chart.customAttr.tooltip.show], () => { @@ -425,7 +440,7 @@ onMounted(() => { - diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/XAxisSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/XAxisSelector.vue index a750839d41..25b5a89fcd 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/XAxisSelector.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/XAxisSelector.vue @@ -52,6 +52,20 @@ const fontSizeList = computed(() => { return arr }) +const isBarRangeTime = computed(() => { + if (props.chart.type === 'bar-range') { + const tempYAxis = props.chart.yAxis[0] + const tempYAxisExt = props.chart.yAxisExt[0] + if ( + (tempYAxis && tempYAxis.groupType === 'd') || + (tempYAxisExt && tempYAxisExt.groupType === 'd') + ) { + return true + } + } + return false +}) + const changeAxisStyle = prop => { if ( state.axisForm.axisValue.splitCount && @@ -374,7 +388,7 @@ onMounted(() => { /> - +
@@ -1955,6 +2194,22 @@ const drop = (ev: MouseEvent, type = 'xAxis') => {
+ + + + + {{ t('chart.aggregate_time') }} + + + diff --git a/core/core-frontend/src/views/chart/components/editor/util/chart.ts b/core/core-frontend/src/views/chart/components/editor/util/chart.ts index 3307f56354..ac4fd9024d 100644 --- a/core/core-frontend/src/views/chart/components/editor/util/chart.ts +++ b/core/core-frontend/src/views/chart/components/editor/util/chart.ts @@ -1198,6 +1198,13 @@ export const CHART_TYPE_CONFIGS = [ value: 'percentage-bar-stack-horizontal', title: t('chart.chart_percentage_bar_stack_horizontal'), icon: 'percentage-bar-stack-horizontal' + }, + { + render: 'antv', + category: 'compare', + value: 'bar-range', + title: t('chart.chart_bar_range'), + icon: 'bar-range' } ] }, diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/common.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/common.ts index 8841229d85..937017e007 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/common.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/common.ts @@ -12,6 +12,19 @@ export const BAR_EDITOR_PROPERTY: EditorProperty[] = [ 'jump-set', 'linkage' ] +export const BAR_RANGE_EDITOR_PROPERTY: EditorProperty[] = [ + 'background-overall-component', + 'basic-style-selector', + 'label-selector', + 'tooltip-selector', + 'x-axis-selector', + 'y-axis-selector', + 'title-selector', + 'legend-selector', + 'function-cfg', + 'jump-set', + 'linkage' +] export const BAR_EDITOR_PROPERTY_INNER: EditorPropertyInner = { 'background-overall-component': ['all'], diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/range-bar.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/range-bar.ts new file mode 100644 index 0000000000..53ce1d20cc --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/range-bar.ts @@ -0,0 +1,383 @@ +import { + G2PlotChartView, + G2PlotDrawOptions +} from '@/views/chart/components/js/panel/types/impl/g2plot' +import { Bar, BarOptions } from '@antv/g2plot/esm/plots/bar' +import { getPadding, setGradientColor } from '@/views/chart/components/js/panel/common/common_antv' +import { cloneDeep, find } from 'lodash-es' +import { flow, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util' +import { valueFormatter } from '@/views/chart/components/js/formatter' +import { + BAR_AXIS_TYPE, + BAR_RANGE_EDITOR_PROPERTY, + BAR_EDITOR_PROPERTY_INNER +} from '@/views/chart/components/js/panel/charts/bar/common' +import { Datum } from '@antv/g2plot/esm/types/common' +import { useI18n } from '@/hooks/web/useI18n' + +const { t } = useI18n() +const DEFAULT_DATA = [] + +/** + * 区间条形图 + */ +export class RangeBar extends G2PlotChartView { + axisConfig = { + ...this['axisConfig'], + yAxis: { + name: `${t('chart.drag_block_value_start')} / ${t('chart.time_dimension_or_quota')}`, + limit: 1, + type: 'q' + }, + yAxisExt: { + name: `${t('chart.drag_block_value_end')} / ${t('chart.time_dimension_or_quota')}`, + limit: 1, + type: 'q' + } + } + properties = BAR_RANGE_EDITOR_PROPERTY + propertyInner = { + ...BAR_EDITOR_PROPERTY_INNER, + 'label-selector': ['hPosition', 'color', 'fontSize', 'labelFormatter', 'showGap'], + 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'tooltipFormatter', 'showGap'], + 'x-axis-selector': [...BAR_EDITOR_PROPERTY_INNER['x-axis-selector'], 'axisLabelFormatter'] + } + axis: AxisType[] = [...BAR_AXIS_TYPE, 'yAxisExt'] + protected baseOptions: BarOptions = { + data: [], + xField: 'values', + yField: 'field', + colorFiled: 'category', + isGroup: true, + 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' }] + } + } + ] + } + + drawChart(drawOptions: G2PlotDrawOptions): Bar { + const { chart, container, action } = drawOptions + if (!chart.data?.data?.length) { + return + } + // data + const data: Array = cloneDeep(chart.data.data) + + data.forEach(d => { + d.tempId = (Math.random() * 10000000).toString() + }) + + const ifAggregate = !!chart.aggregate + + const isDate = !!chart.data.isDate + + const minTime = chart.data.minTime + const maxTime = chart.data.maxTime + + const minNumber = chart.data.min + const maxNumber = chart.data.max + + // options + const initOptions: BarOptions = { + ...this.baseOptions, + appendPadding: getPadding(chart), + data, + seriesField: isDate ? (ifAggregate ? 'category' : undefined) : 'category', + isGroup: isDate ? !ifAggregate : false, + isStack: isDate ? !ifAggregate : false, + meta: isDate + ? { + values: { + type: 'time', + min: minTime, + max: maxTime, + mask: 'YYYY-MM-DD HH:mm:ss' + }, + tempId: { + key: true + } + } + : { + values: { + min: minNumber, + max: maxNumber, + mask: 'YYYY-MM-DD HH:mm:ss' + }, + tempId: { + key: true + } + } + } + + const options = this.setupOptions(chart, initOptions) + + // 开始渲染 + const newChart = new Bar(container, options) + + newChart.on('interval:click', action) + + return newChart + } + + protected configXAxis(chart: Chart, options: BarOptions): BarOptions { + const tmpOptions = super.configXAxis(chart, options) + if (!tmpOptions.xAxis) { + return tmpOptions + } + const xAxis = parseJson(chart.customStyle).xAxis + const axisValue = xAxis.axisValue + const isDate = !!chart.data.isDate + if (tmpOptions.xAxis.label) { + tmpOptions.xAxis.label.formatter = value => { + if (isDate) { + return value + } + return valueFormatter(value, xAxis.axisLabelFormatter) + } + } + if (tmpOptions.xAxis.position === 'top') { + tmpOptions.xAxis.position = 'left' + } + if (tmpOptions.xAxis.position === 'bottom') { + tmpOptions.xAxis.position = 'right' + } + if (!axisValue?.auto) { + const axis = { + xAxis: { + ...tmpOptions.xAxis, + min: axisValue.min, + max: axisValue.max, + minLimit: axisValue.min, + maxLimit: axisValue.max, + tickCount: axisValue.splitCount + } + } + return { ...tmpOptions, ...axis } + } + return tmpOptions + } + + protected configTooltip(chart: Chart, options: BarOptions): BarOptions { + const isDate = !!chart.data.isDate + let tooltip + let customAttr: DeepPartial + if (chart.customAttr) { + customAttr = parseJson(chart.customAttr) + // tooltip + if (customAttr.tooltip) { + const t = JSON.parse(JSON.stringify(customAttr.tooltip)) + if (t.show) { + tooltip = { + formatter: function (param: Datum) { + let res + if (isDate) { + res = param.values[0] + ' ~ ' + param.values[1] + if (t.showGap) { + res = res + ' (' + param.gap + ')' + } + } else { + res = + valueFormatter(param.values[0], t.tooltipFormatter) + + ' ~ ' + + valueFormatter(param.values[1], t.tooltipFormatter) + if (t.showGap) { + res = res + ' (' + valueFormatter(param.gap, t.tooltipFormatter) + ')' + } + } + return { value: res, values: param.values, name: param.field } + } + } + } else { + tooltip = false + } + } + } + return { ...options, tooltip } + } + + protected configBasicStyle(chart: Chart, options: BarOptions): BarOptions { + const isDate = !!chart.data.isDate + const ifAggregate = !!chart.aggregate + const basicStyle = parseJson(chart.customAttr).basicStyle + + if (isDate && !ifAggregate) { + const customColors = [] + const groups = [] + for (let i = 0; i < chart.data.data.length; i++) { + const name = chart.data.data[i].field + if (groups.indexOf(name) < 0) { + groups.push(name) + } + } + for (let i = 0; i < groups.length; i++) { + const s = groups[i] + customColors.push({ + name: s, + color: basicStyle.colors[i % basicStyle.colors.length], + isCustom: false + }) + } + const color = obj => { + const colorObj = find(customColors, o => { + return o.name === obj.field + }) + if (colorObj === undefined) { + return undefined + } + const color = hexColorToRGBA(colorObj.color, basicStyle.alpha) + if (basicStyle.gradient) { + return setGradientColor(color, true) + } else { + return color + } + } + + options = { + ...options, + color + } + } else { + if (basicStyle.gradient) { + let color = basicStyle.colors + color = color.map(ele => { + const tmp = hexColorToRGBA(ele, basicStyle.alpha) + return setGradientColor(tmp, true) + }) + options = { + ...options, + color + } + } + } + return options + } + + setupDefaultOptions(chart: ChartObj): ChartObj { + const { customAttr, senior } = chart + const { label } = customAttr + if (!['left', 'middle', 'right'].includes(label.position)) { + label.position = 'middle' + } + senior.functionCfg.emptyDataStrategy = 'ignoreData' + return chart + } + + protected configLabel(chart: Chart, options: BarOptions): BarOptions { + const isDate = !!chart.data.isDate + const ifAggregate = !!chart.aggregate + + const tmpOptions = super.configLabel(chart, options) + if (!tmpOptions.label) { + return { + ...tmpOptions, + label: false + } + } + const labelAttr = parseJson(chart.customAttr).label + + if (isDate && !ifAggregate) { + if (!tmpOptions.label.layout) { + tmpOptions.label.layout = [] + } + tmpOptions.label.layout.push({ type: 'interval-hide-overlap' }) + tmpOptions.label.layout.push({ type: 'limit-in-plot', cfg: { action: 'hide' } }) + } + + const label = { + fields: [], + ...tmpOptions.label, + formatter: (param: Datum) => { + let res + if (isDate) { + if (labelAttr.showGap) { + res = param.gap + } else { + res = param.values[0] + ' ~ ' + param.values[1] + } + } else { + if (labelAttr.showGap) { + res = valueFormatter(param.gap, labelAttr.labelFormatter) + } else { + res = + valueFormatter(param.values[0], labelAttr.labelFormatter) + + ' ~ ' + + valueFormatter(param.values[1], labelAttr.labelFormatter) + } + } + return res + } + } + return { + ...tmpOptions, + label + } + } + + protected configYAxis(chart: Chart, options: BarOptions): BarOptions { + const tmpOptions = super.configYAxis(chart, options) + if (!tmpOptions.yAxis) { + return tmpOptions + } + if (tmpOptions.yAxis.position === 'left') { + tmpOptions.yAxis.position = 'bottom' + } + if (tmpOptions.yAxis.position === 'right') { + tmpOptions.yAxis.position = 'top' + } + return tmpOptions + } + + protected setupOptions(chart: Chart, options: BarOptions): BarOptions { + return flow( + this.configTheme, + this.configBasicStyle, + this.configLabel, + this.configTooltip, + this.configLegend, + this.configXAxis, + this.configYAxis, + this.configSlider, + this.configAnalyseHorizontal, + this.configEmptyDataStrategy + )(chart, options) + } + + constructor(name = 'bar-range') { + super(name, DEFAULT_DATA) + } +} diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts index 83bf2cb246..c69513fc4a 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts @@ -44,10 +44,12 @@ export class ColumnLineMix extends G2PlotChartView { ...this['axisConfig'], yAxis: { name: `${t('chart.drag_block_value_axis_left')} / ${t('chart.quota')}`, + limit: 1, type: 'q' }, yAxisExt: { name: `${t('chart.drag_block_value_axis_right')} / ${t('chart.quota')}`, + limit: 1, type: 'q' } } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/indicator.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/indicator.ts index 0264aa4720..22f8973a12 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/others/indicator.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/indicator.ts @@ -55,6 +55,7 @@ export class IndicatorChartView extends AbstractChartView { axisConfig: AxisConfig = { yAxis: { name: `${t('chart.quota')}`, + limit: 1, type: 'q' } } diff --git a/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue index e926d6eb22..947620c2f5 100644 --- a/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue +++ b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue @@ -226,8 +226,12 @@ const trackClick = trackAction => { if (state.pointParam.data.dimensionList.length > 1) { checkName = state.pointParam.data.dimensionList[0].id } - const quotaList = state.pointParam.data.quotaList - quotaList[0]['value'] = state.pointParam.data.value + let quotaList = state.pointParam.data.quotaList + if (curView.type === 'bar-range') { + quotaList = state.pointParam.data.dimensionList + } else { + quotaList[0]['value'] = state.pointParam.data.value + } const linkageParam = { option: 'linkage', name: checkName, diff --git a/sdk/api/api-base/src/main/java/io/dataease/api/chart/dto/ChartViewBaseDTO.java b/sdk/api/api-base/src/main/java/io/dataease/api/chart/dto/ChartViewBaseDTO.java index 5e9178eb88..631dd264eb 100644 --- a/sdk/api/api-base/src/main/java/io/dataease/api/chart/dto/ChartViewBaseDTO.java +++ b/sdk/api/api-base/src/main/java/io/dataease/api/chart/dto/ChartViewBaseDTO.java @@ -202,4 +202,9 @@ public class ChartViewBaseDTO implements Serializable { */ private Boolean jumpActive; + /** + * 区间条形图开启时间纬度开启聚合 + */ + private Boolean aggregate; + }