diff --git a/backend/pom.xml b/backend/pom.xml index 79aeffd..628ac39 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -160,6 +160,16 @@ jcommander 1.82 + + + org.flywaydb + flyway-core + + + + org.flywaydb + flyway-mysql + diff --git a/backend/src/main/java/com/stdproject/controller/ChartDataController.java b/backend/src/main/java/com/stdproject/controller/ChartDataController.java index 3720c0d..40b74a5 100644 --- a/backend/src/main/java/com/stdproject/controller/ChartDataController.java +++ b/backend/src/main/java/com/stdproject/controller/ChartDataController.java @@ -17,6 +17,7 @@ import jakarta.annotation.Resource; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -34,7 +35,7 @@ public class ChartDataController { @Operation(summary = "获取图表数据") @OperationLog(type = "01", module = "数据可视化", description = "获取图表数据") @PostMapping("getData") - public ResponseResult getData(ChartViewDTO chartViewDTO) throws Exception { + public ResponseResult getData(@RequestBody ChartViewDTO chartViewDTO) throws Exception { try { // 从模板数据获取 DatasetUtils.viewDecode(chartViewDTO); diff --git a/backend/src/main/java/com/stdproject/controller/UserController.java b/backend/src/main/java/com/stdproject/controller/UserController.java index 6ebc503..7de06c5 100644 --- a/backend/src/main/java/com/stdproject/controller/UserController.java +++ b/backend/src/main/java/com/stdproject/controller/UserController.java @@ -8,11 +8,13 @@ import com.stdproject.config.WebConfig; import com.stdproject.entity.AppOptLog; import com.stdproject.entity.LoginUser; import com.stdproject.entity.User; +import com.stdproject.entity.vo.CurIpVO; import com.stdproject.service.IAppOptLogService; import com.stdproject.service.IUserService; import com.stdproject.utils.JwtUtils; import com.stdproject.utils.RsaUtils; import com.stdproject.utils.commonUtils; +import io.gisbi.utils.IPUtils; import io.micrometer.common.util.StringUtils; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; @@ -276,4 +278,13 @@ private Long jwtExpirationMs; sysLogService.save(sysLog); return ResponseResult.success(); } + + @GetMapping("/ipInfo") + public CurIpVO ipInfo() { + CurIpVO curIpVO = new CurIpVO(); + curIpVO.setAccount("admin"); + curIpVO.setName("管理员"); + curIpVO.setIp(IPUtils.get()); + return curIpVO; + } } diff --git a/backend/src/main/java/com/stdproject/controller/VisualizationLinkJumpController.java b/backend/src/main/java/com/stdproject/controller/VisualizationLinkJumpController.java new file mode 100644 index 0000000..53eeabd --- /dev/null +++ b/backend/src/main/java/com/stdproject/controller/VisualizationLinkJumpController.java @@ -0,0 +1,68 @@ +package com.stdproject.controller; + +import com.stdproject.common.OperationLog; +import com.stdproject.config.ResponseResult; +import com.stdproject.entity.dto.LinkageInfoDTO; +import com.stdproject.entity.dto.VisualizationLinkJumpDTO; +import com.stdproject.entity.dto.VisualizationLinkJumpInfoDTO; +import com.stdproject.mapper.ExtVisualizationLinkJumpMapper; +import com.stdproject.mapper.ExtVisualizationLinkageMapper; +import com.stdproject.response.VisualizationLinkJumpBaseResponse; +import io.gisbi.constant.CommonConstants; +import io.gisbi.utils.AuthUtils; +import io.gisbi.utils.ModelUtils; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/linkJump") +public class VisualizationLinkJumpController { + + @Resource + private ExtVisualizationLinkJumpMapper extVisualizationLinkJumpMapper; + + @Operation(summary = "根据可视化资源ID查询跳转信息") + @OperationLog(type = "01", module = "数据可视化", description = "根据可视化资源ID查询跳转信息") + @GetMapping("/queryVisualizationJumpInfo/{dvId}/{resourceTable}") + public ResponseResult queryVisualizationJumpInfo(@PathVariable Long dvId,@PathVariable String resourceTable) throws Exception { + Map resultBase = new HashMap<>(); + List resultLinkJumpList = null; +// String userid=com.stdproject.utils.AuthUtils.getUserId(); + String userid="1922852335260831746"; + if (CommonConstants.RESOURCE_TABLE.SNAPSHOT.equals(resourceTable)) { + resultLinkJumpList = extVisualizationLinkJumpMapper.queryWithDvIdSnapshot(dvId, Long.parseLong(userid), false); + } else { + resultLinkJumpList = extVisualizationLinkJumpMapper.queryWithDvId(dvId,Long.parseLong(userid), false); + } + Optional.ofNullable(resultLinkJumpList).orElse(new ArrayList<>()).forEach(resultLinkJump -> { + if (resultLinkJump.getChecked()) { + Long sourceViewId = resultLinkJump.getSourceViewId(); + Optional.ofNullable(resultLinkJump.getLinkJumpInfoArray()).orElse(new ArrayList<>()).forEach(linkJumpInfo -> { + if (linkJumpInfo.getChecked()) { + String sourceJumpInfo = sourceViewId + "#" + linkJumpInfo.getSourceFieldId(); + // 内部仪表板跳转 需要设置好仪表板ID + if ("inner".equals(linkJumpInfo.getLinkType())) { + if (linkJumpInfo.getTargetDvId() != null) { + resultBase.put(sourceJumpInfo, linkJumpInfo); + } + } else { + // 外部跳转 + resultBase.put(sourceJumpInfo, linkJumpInfo); + } + } + }); + } + }); + VisualizationLinkJumpBaseResponse result = new VisualizationLinkJumpBaseResponse(resultBase, null); + return ResponseResult.successData(result); + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/com/stdproject/controller/VisualizationLinkageController.java b/backend/src/main/java/com/stdproject/controller/VisualizationLinkageController.java new file mode 100644 index 0000000..8b82b52 --- /dev/null +++ b/backend/src/main/java/com/stdproject/controller/VisualizationLinkageController.java @@ -0,0 +1,56 @@ +package com.stdproject.controller; + +import com.stdproject.common.OperationLog; +import com.stdproject.config.ResponseResult; +import com.stdproject.entity.dto.LinkageInfoDTO; +import com.stdproject.mapper.ExtVisualizationLinkageMapper; +import com.stdproject.service.manage.ChartDataManage; +import com.stdproject.utils.DatasetUtils; +import io.gisbi.constant.CommonConstants; +import io.gisbi.exception.DEException; +import io.gisbi.extensions.view.dto.ChartViewDTO; +import io.gisbi.result.ResultCode; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/linkage") +public class VisualizationLinkageController { + + @Resource + private ExtVisualizationLinkageMapper extVisualizationLinkageMapper; + + @Operation(summary = "根据资源ID查询联动信息") + @OperationLog(type = "01", module = "数据可视化", description = "根据资源ID查询联动信息") + @GetMapping("/getVisualizationAllLinkageInfo/{dvId}/{resourceTable}") + public ResponseResult getVisualizationAllLinkageInfo(@PathVariable Long dvId,@PathVariable String resourceTable) throws Exception { + List info = null; + if (CommonConstants.RESOURCE_TABLE.SNAPSHOT.equals(resourceTable)) { + info = extVisualizationLinkageMapper.getPanelAllLinkageInfoSnapshot(dvId); + }else{ + info = extVisualizationLinkageMapper.getPanelAllLinkageInfo(dvId); + } + info = info.stream().map(item -> { + item.setTargetInfoList(item.getTargetInfoList().stream().map(targetInfo -> { + if (targetInfo.contains(".")) { + String[] split = targetInfo.split("\\."); + if (split.length == 2) { + targetInfo = split[1]; + } + } + return targetInfo; + }).collect(Collectors.toList())); + return item; + }).collect(Collectors.toList()); + return ResponseResult.successData( info); + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/com/stdproject/entity/vo/CurIpVO.java b/backend/src/main/java/com/stdproject/entity/vo/CurIpVO.java new file mode 100644 index 0000000..ac83fb4 --- /dev/null +++ b/backend/src/main/java/com/stdproject/entity/vo/CurIpVO.java @@ -0,0 +1,18 @@ +package com.stdproject.entity.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +public class CurIpVO implements Serializable { + @Serial + private static final long serialVersionUID = -3025566841330382707L; + + private String account; + + private String name; + + private String ip; +} diff --git a/backend/src/main/java/com/stdproject/mapper/ExtVisualizationLinkJumpMapper.java b/backend/src/main/java/com/stdproject/mapper/ExtVisualizationLinkJumpMapper.java index 1c28437..84a701c 100644 --- a/backend/src/main/java/com/stdproject/mapper/ExtVisualizationLinkJumpMapper.java +++ b/backend/src/main/java/com/stdproject/mapper/ExtVisualizationLinkJumpMapper.java @@ -17,41 +17,5 @@ public interface ExtVisualizationLinkJumpMapper { List queryWithDvIdSnapshot(@Param("dvId") Long dvId,@Param("uid") Long uid,@Param("isDesktop") Boolean isDesktop); - VisualizationLinkJumpDTO queryWithViewId(@Param("dvId") Long dvId,@Param("viewId") Long viewId,@Param("uid") Long uid,@Param("isDesktop") Boolean isDesktop); - void deleteJumpTargetViewInfoSnapshot(@Param("dvId") Long dvId,@Param("viewId") Long viewId); - - void deleteJumpInfoSnapshot(@Param("dvId") Long dvId,@Param("viewId") Long viewId); - - void deleteJumpSnapshot(@Param("dvId") Long dvId,@Param("viewId") Long viewId); - - void deleteJumpTargetViewInfoWithVisualization(@Param("dvId") Long dvId); - - void deleteJumpInfoWithVisualization(@Param("dvId") Long dvId); - - void deleteJumpWithVisualization(@Param("dvId") Long dvId); - - void deleteJumpTargetViewInfoWithVisualizationSnapshot(@Param("dvId") Long dvId); - - void deleteJumpInfoWithVisualizationSnapshot(@Param("dvId") Long dvId); - - void deleteJumpWithVisualizationSnapshot(@Param("dvId") Long dvId); - - List getTargetVisualizationJumpInfo(@Param("request") VisualizationLinkJumpBaseRequest request); - - List getTargetVisualizationJumpInfoSnapshot(@Param("request") VisualizationLinkJumpBaseRequest request); - - void copyLinkJump(@Param("copyId")Long copyId); - - void copyLinkJumpInfo(@Param("copyId")Long copyId); - - void copyLinkJumpTarget(@Param("copyId")Long copyId); - - List findLinkJumpWithDvId(@Param("dvId")Long dvId); - - List findLinkJumpInfoWithDvId(@Param("dvId")Long dvId); - - List getViewTableDetails(@Param("dvId")Long dvId); - - List queryOutParamsTargetWithDvId(@Param("dvId")Long dvId); } diff --git a/backend/src/main/java/com/stdproject/mapper/ExtVisualizationLinkageMapper.java b/backend/src/main/java/com/stdproject/mapper/ExtVisualizationLinkageMapper.java index 4d6e6f9..dbad192 100644 --- a/backend/src/main/java/com/stdproject/mapper/ExtVisualizationLinkageMapper.java +++ b/backend/src/main/java/com/stdproject/mapper/ExtVisualizationLinkageMapper.java @@ -13,32 +13,6 @@ import java.util.List; @Mapper public interface ExtVisualizationLinkageMapper { - - List getViewLinkageGather(@Param("dvId") Long dvId, @Param("sourceViewId") Long sourceViewId, @Param("targetViewIds") List targetViewIds); - - List getPanelAllLinkageInfo(@Param("dvId") Long dvId); - - List getViewLinkageGatherSnapshot(@Param("dvId") Long dvId, @Param("sourceViewId") Long sourceViewId, @Param("targetViewIds") List targetViewIds); - - List getPanelAllLinkageInfoSnapshot(@Param("dvId") Long dvId); - - List queryTableField(@Param("table_id") Long tableId); - - List queryTableFieldWithViewId(@Param("viewId") Long viewId); - - void deleteViewLinkage(@Param("dvId") Long dvId,@Param("sourceViewId") Long sourceViewId); - - void deleteViewLinkageField(@Param("dvId") Long dvId,@Param("sourceViewId") Long sourceViewId); - - void deleteViewLinkageSnapshot(@Param("dvId") Long dvId,@Param("sourceViewId") Long sourceViewId); - - void deleteViewLinkageFieldSnapshot(@Param("dvId") Long dvId,@Param("sourceViewId") Long sourceViewId); - - void copyViewLinkage(@Param("copyId") Long copyId); - - void copyViewLinkageField(@Param("copyId") Long copyId); - - List findLinkageWithDvId(@Param("dvId") Long dvId); - - List findLinkageFieldWithDvId(@Param("dvId") Long dvId); + List getPanelAllLinkageInfo(@Param("dvId") Long dvId); + List getPanelAllLinkageInfoSnapshot(@Param("dvId") Long dvId); } diff --git a/backend/src/main/java/com/stdproject/service/manage/ChartViewManege.java b/backend/src/main/java/com/stdproject/service/manage/ChartViewManege.java index 041cc01..9684f07 100644 --- a/backend/src/main/java/com/stdproject/service/manage/ChartViewManege.java +++ b/backend/src/main/java/com/stdproject/service/manage/ChartViewManege.java @@ -251,9 +251,7 @@ public class ChartViewManege { ChartFieldCompareDTO chartFieldCompareDTO = new ChartFieldCompareDTO(); chartFieldCompareDTO.setType("none"); dto.setCompareCalc(chartFieldCompareDTO); - - dto.setFormatterCfg(new FormatterCfgDTO().setUnitLanguage(Lang.isChinese() ? "ch" : "en")); - + dto.setFormatterCfg(new FormatterCfgDTO()); dto.setSort("none"); dto.setFilter(Collections.emptyList()); return dto; diff --git a/backend/src/main/java/com/stdproject/service/manage/DatasetSQLManage.java b/backend/src/main/java/com/stdproject/service/manage/DatasetSQLManage.java index e86de99..6c54fd5 100644 --- a/backend/src/main/java/com/stdproject/service/manage/DatasetSQLManage.java +++ b/backend/src/main/java/com/stdproject/service/manage/DatasetSQLManage.java @@ -8,10 +8,7 @@ import com.stdproject.entity.CoreDatasource; import com.stdproject.entity.union.*; import com.stdproject.mapper.CoreDatasetGroupMapper; import com.stdproject.mapper.CoreDatasourceMapper; -import com.stdproject.utils.DatasetTableTypeConstants; -import com.stdproject.utils.SqlUtils; -import com.stdproject.utils.SqlparserUtils; -import com.stdproject.utils.TableUtils; +import com.stdproject.utils.*; import io.gisbi.constant.SQLConstants; import io.gisbi.exception.DEException; import io.gisbi.extensions.datasource.api.PluginManageApi; @@ -480,8 +477,8 @@ public class DatasetSQLManage { if (coreDatasource.getType().contains(DatasourceConfiguration.DatasourceType.Excel.name()) || coreDatasource.getType().contains(DatasourceConfiguration.DatasourceType.API.name())) { coreDatasource = engineManage.getDeEngine(); } - - Map map = JsonUtil.parseObject(coreDatasource.getConfiguration(), Map.class); + String config= String.valueOf(EncryptUtils.aesDecrypt(coreDatasource.getConfiguration())); + Map map=JsonUtil.parseObject(config, Map.class); if (!isCross && ObjectUtils.isNotEmpty(map.get("schema"))) { schemaAlias = (String) map.get("schema"); } else { diff --git a/backend/src/main/java/com/stdproject/service/provider/CalciteProvider.java b/backend/src/main/java/com/stdproject/service/provider/CalciteProvider.java index af0eb1c..9136aa2 100644 --- a/backend/src/main/java/com/stdproject/service/provider/CalciteProvider.java +++ b/backend/src/main/java/com/stdproject/service/provider/CalciteProvider.java @@ -478,8 +478,8 @@ public class CalciteProvider extends Provider { public Map jdbcFetchResultField(DatasourceRequest datasourceRequest) throws BusinessException { DatasourceSchemaDTO value = datasourceRequest.getDsList().entrySet().iterator().next().getValue(); datasourceRequest.setDatasource(value); - - DatasourceConfiguration datasourceConfiguration = JsonUtil.parseObject(datasourceRequest.getDatasource().getConfiguration(), DatasourceConfiguration.class); + String config=String.valueOf(EncryptUtils.aesDecrypt(datasourceRequest.getDatasource().getConfiguration())); + DatasourceConfiguration datasourceConfiguration = JsonUtil.parseObject(config, DatasourceConfiguration.class); Map map = new LinkedHashMap<>(); List fieldList = new ArrayList<>(); diff --git a/backend/src/main/java/com/stdproject/utils/RsaUtils.java b/backend/src/main/java/com/stdproject/utils/RsaUtils.java index 805d29f..00c3e5c 100644 --- a/backend/src/main/java/com/stdproject/utils/RsaUtils.java +++ b/backend/src/main/java/com/stdproject/utils/RsaUtils.java @@ -1,8 +1,14 @@ package com.stdproject.utils; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; @@ -15,8 +21,11 @@ import java.security.spec.X509EncodedKeySpec; * @date 2020-05-18 **/ public class RsaUtils { - + public static final String IV_KEY = "0000000000000000"; private static final String SRC = "123456"; + private static final String ALGORITHM = "AES"; + public static String symmetricKey = null; + private static final int KEY_SIZE = 128; public static void main(String[] args) throws Exception { System.out.println("\n"); @@ -30,6 +39,7 @@ public class RsaUtils { System.out.println("\n"); + } /** @@ -69,6 +79,8 @@ public class RsaUtils { System.out.println("***************** 私钥加密公钥解密结束 *****************"); } + + /** * 公钥解密 * @@ -156,8 +168,61 @@ public class RsaUtils { String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded()); return new RsaKeyPair(publicKeyString, privateKeyString); } + /** + * 生成AES对称加密密钥 + * @return Base64编码的密钥字符串 + */ + public static String generateSymmetricKey() { + try { + if (StringUtils.isEmpty(symmetricKey)) { + KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM); + keyGenerator.init(KEY_SIZE, new SecureRandom()); + SecretKey secretKey = keyGenerator.generateKey(); + symmetricKey = java.util.Base64.getEncoder().encodeToString(secretKey.getEncoded()); + } + return symmetricKey; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + /** + * 使用AES对称密钥进行加密 + * @param data 待加密的数据 + * @return 加密后的Base64编码字符串 + */ + public static String symmetricEncrypt(String data) { + try { + byte[] iv = IV_KEY.getBytes(StandardCharsets.UTF_8); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKeySpec secretKeySpec = new SecretKeySpec(java.util.Base64.getDecoder().decode(generateSymmetricKey()), ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + byte[] ciphertext = cipher.doFinal(data.getBytes("UTF-8")); + return java.util.Base64.getEncoder().encodeToString(ciphertext); + } catch (Exception e) { + throw new RuntimeException(e); + } + } - + /** + * 使用AES对称密钥进行解密 + * @param data 待解密的Base64编码字符串 + * @return 解密后的原始数据 + */ + public static String symmetricDecrypt(String data) { + try { + byte[] iv = IV_KEY.getBytes(StandardCharsets.UTF_8); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); + SecretKeySpec secretKeySpec = new SecretKeySpec(java.util.Base64.getDecoder().decode(generateSymmetricKey()), ALGORITHM); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + byte[] decodedCiphertext = java.util.Base64.getDecoder().decode(data); + byte[] decryptedText = cipher.doFinal(decodedCiphertext); + return new String(decryptedText, "UTF-8"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } /** * RSA密钥对对象 */ diff --git a/backend/src/main/java/com/stdproject/utils/Utils.java b/backend/src/main/java/com/stdproject/utils/Utils.java index 65ad66a..d3e505d 100644 --- a/backend/src/main/java/com/stdproject/utils/Utils.java +++ b/backend/src/main/java/com/stdproject/utils/Utils.java @@ -279,7 +279,8 @@ public class Utils { public static String replaceSchemaAlias(String sql, Map dsMap) { DatasourceSchemaDTO value = dsMap.entrySet().iterator().next().getValue(); - Map map = JsonUtil.parseObject(value.getConfiguration(), Map.class); + String config= String.valueOf(EncryptUtils.aesDecrypt(value.getConfiguration())); + Map map = JsonUtil.parseObject(config, Map.class); if (ObjectUtils.isNotEmpty(map.get("schema"))) { return sql; } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 11060a5..ffcd210 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -19,9 +19,9 @@ spring: main: allow-bean-definition-overriding: true datasource: - url: ${DB_URL:jdbc:mysql://121.37.111.42:3306/gisbi-demodb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true} + url: ${DB_URL:jdbc:mysql://192.168.1.58:3306/gisbi-demodb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true} username: ${DB_USERNAME:root} - password: ${DB_PASSWORD:mysql_F8ysiK@2024} + password: ${DB_PASSWORD:123456} driver-class-name: com.mysql.cj.jdbc.Driver # HikariCP连接池配置 hikari: @@ -33,7 +33,13 @@ spring: max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1 - + flyway: + enabled: true + table: flyway_schema_history + validate-on-migrate: true + locations: classpath:db/migration + baseline-on-migrate: true + out-of-order: true cache: jcache: config: classpath:ehcache.xml # 指定Ehcache配置文件路径 @@ -80,6 +86,7 @@ logging: org.springframework.security: ${LOG_LEVEL_SECURITY:WARN} org.hibernate.SQL: ${LOG_LEVEL_SQL:WARN} org.hibernate.type.descriptor.sql.BasicBinder: ${LOG_LEVEL_SQL_PARAMS:WARN} + org.flywaydb: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" @@ -160,6 +167,7 @@ spring: security: jwt: enabled: true + logging: level: root: WARN diff --git a/backend/src/main/resources/mybatis/ExtDataVisualizationMapper.xml b/backend/src/main/resources/mybatis/ExtDataVisualizationMapper.xml index 7bfd40b..b9a300a 100644 --- a/backend/src/main/resources/mybatis/ExtDataVisualizationMapper.xml +++ b/backend/src/main/resources/mybatis/ExtDataVisualizationMapper.xml @@ -124,7 +124,7 @@ - + diff --git a/backend/src/main/resources/mybatis/ExtVisualizationLinkJumpMapper.xml b/backend/src/main/resources/mybatis/ExtVisualizationLinkJumpMapper.xml new file mode 100644 index 0000000..458b11b --- /dev/null +++ b/backend/src/main/resources/mybatis/ExtVisualizationLinkJumpMapper.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mybatis/ExtVisualizationLinkageMapper.xml b/backend/src/main/resources/mybatis/ExtVisualizationLinkageMapper.xml new file mode 100644 index 0000000..5397083 --- /dev/null +++ b/backend/src/main/resources/mybatis/ExtVisualizationLinkageMapper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/components.d.ts b/frontend/components.d.ts index c6b7ce9..ec78696 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -9,10 +9,48 @@ export {} declare module '@vue/runtime-core' { export interface GlobalComponents { - ElCard: typeof import('element-plus-secondary/es')['ElCard'] + ElBreadcrumb: typeof import('element-plus-secondary/es')['ElBreadcrumb'] + ElBreadcrumbItem: typeof import('element-plus-secondary/es')['ElBreadcrumbItem'] + ElButton: typeof import('element-plus-secondary/es')['ElButton'] + ElCheckbox: typeof import('element-plus-secondary/es')['ElCheckbox'] + ElCheckboxGroup: typeof import('element-plus-secondary/es')['ElCheckboxGroup'] + ElCol: typeof import('element-plus-secondary/es')['ElCol'] + ElCollapse: typeof import('element-plus-secondary/es')['ElCollapse'] + ElCollapseItem: typeof import('element-plus-secondary/es')['ElCollapseItem'] + ElColorPicker: typeof import('element-plus-secondary/es')['ElColorPicker'] + ElContainer: typeof import('element-plus-secondary/es')['ElContainer'] + ElDatePicker: typeof import('element-plus-secondary/es')['ElDatePicker'] + ElDialog: typeof import('element-plus-secondary/es')['ElDialog'] + ElDivider: typeof import('element-plus-secondary/es')['ElDivider'] + ElDropdown: typeof import('element-plus-secondary/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus-secondary/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus-secondary/es')['ElDropdownMenu'] + ElEmpty: typeof import('element-plus-secondary/es')['ElEmpty'] + ElForm: typeof import('element-plus-secondary/es')['ElForm'] + ElFormItem: typeof import('element-plus-secondary/es')['ElFormItem'] + ElHeader: typeof import('element-plus-secondary/es')['ElHeader'] ElIcon: typeof import('element-plus-secondary/es')['ElIcon'] - ElTimeline: typeof import('element-plus-secondary/es')['ElTimeline'] - ElTimelineItem: typeof import('element-plus-secondary/es')['ElTimelineItem'] + ElInput: typeof import('element-plus-secondary/es')['ElInput'] + ElInputNumber: typeof import('element-plus-secondary/es')['ElInputNumber'] + ElMain: typeof import('element-plus-secondary/es')['ElMain'] + ElOption: typeof import('element-plus-secondary/es')['ElOption'] + ElOptionGroup: typeof import('element-plus-secondary/es')['ElOptionGroup'] + ElPopover: typeof import('element-plus-secondary/es')['ElPopover'] + ElRadio: typeof import('element-plus-secondary/es')['ElRadio'] + ElRadioGroup: typeof import('element-plus-secondary/es')['ElRadioGroup'] + ElRow: typeof import('element-plus-secondary/es')['ElRow'] + ElScrollbar: typeof import('element-plus-secondary/es')['ElScrollbar'] + ElSelect: typeof import('element-plus-secondary/es')['ElSelect'] + ElSelectV2: typeof import('element-plus-secondary/es')['ElSelectV2'] + ElSpace: typeof import('element-plus-secondary/es')['ElSpace'] + ElSwitch: typeof import('element-plus-secondary/es')['ElSwitch'] + ElTabPane: typeof import('element-plus-secondary/es')['ElTabPane'] + ElTabs: typeof import('element-plus-secondary/es')['ElTabs'] + ElTimePicker: typeof import('element-plus-secondary/es')['ElTimePicker'] + ElTooltip: typeof import('element-plus-secondary/es')['ElTooltip'] + ElTree: typeof import('element-plus-secondary/es')['ElTree'] + ElTreeSelect: typeof import('element-plus-secondary/es')['ElTreeSelect'] + ElUpload: typeof import('element-plus-secondary/es')['ElUpload'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } diff --git a/frontend/src/api/data-visualization/chart.ts b/frontend/src/api/data-visualization/chart.ts index b1bad2d..8267dae 100644 --- a/frontend/src/api/data-visualization/chart.ts +++ b/frontend/src/api/data-visualization/chart.ts @@ -1,4 +1,6 @@ import request from '@/data-visualization/config/axios' +import { originNameHandleWithArr, originNameHandleBackWithArr } from '@/data-visualization/utils/CalculateFields' +import { cloneDeep } from 'lodash-es' export interface Field { id: number | string datasourceId: number | string @@ -6,7 +8,7 @@ export interface Field { datasetGroupId: number | string originName: string name: string - dataeaseName: string + gisbiName: string groupType: string type: string deType: number @@ -27,6 +29,7 @@ export interface ComponentInfo { export const getFieldByDQ = async (id, chartId, data): Promise => { return request.post({ url: `/chart/listByDQ/${id}/${chartId}`, data: data }).then(res => { + originNameHandleBackWithArr(res?.data, ['dimensionList', 'quotaList']) return res?.data }) } @@ -52,10 +55,33 @@ export const deleteChartFieldByChartId = async (chartId): Promise => // 通过图表对象获取数据 export const getData = async (data): Promise => { delete data.data - return request.post({ url: '/chartData/getData', data }).then(res => { - if (res.code === 0) { + const copyData = cloneDeep(data) + const fields = [ + 'xAxis', + 'xAxisExt', + 'yAxis', + 'yAxisExt', + 'extBubble', + 'extLabel', + 'extStack', + 'extTooltip' + ] + const dataFields = ['fields', 'sourceFields'] + originNameHandleWithArr(copyData, fields) + return request.post({ url: '/chartData/getData', data: copyData }).then(res => { + if (res.code == 0) { + originNameHandleBackWithArr(res?.data, fields) + // 动态计算字段在数据中,也需要转码 + originNameHandleWithArr(res?.data?.data, dataFields) + originNameHandleBackWithArr(res?.data?.data, dataFields) + originNameHandleBackWithArr(res?.data?.data?.left, ['fields']) + originNameHandleBackWithArr(res?.data?.data?.right, ['fields']) return res?.data } else { + originNameHandleBackWithArr(res, fields) + originNameHandleBackWithArr(res?.data, dataFields) + originNameHandleBackWithArr(res?.data?.left, ['fields']) + originNameHandleBackWithArr(res?.data?.right, ['fields']) return res } }) diff --git a/frontend/src/api/data-visualization/dataset.ts b/frontend/src/api/data-visualization/dataset.ts index e5babfb..db8c87a 100644 --- a/frontend/src/api/data-visualization/dataset.ts +++ b/frontend/src/api/data-visualization/dataset.ts @@ -1,9 +1,17 @@ import request from '@/utils/request' import { type Field } from '@/api/data-visualization/chart' +import { + originNameHandle, + originNameHandleBack, + originNameHandleBackWithArr +} from '@/data-visualization/utils/CalculateFields' + +import { cloneDeep } from 'lodash-es' import { nameTrim } from '@/data-visualization/utils/utils' export interface DatasetOrFolder { name: string action?: string + isCross?: boolean id?: number | string pid?: number | string appId?: number | string @@ -57,6 +65,7 @@ export interface Dataset { id: string pid: string name: string + isCross?: boolean union?: Array<{}> allFields?: Array<{}> } @@ -72,7 +81,12 @@ export interface Table { // edit export const saveDatasetTree = async (data: DatasetOrFolder): Promise => { nameTrim(data) - return request.post({ url: '/datasetTree/save', data }).then(res => { + const copyData = cloneDeep(data) + originNameHandle(copyData.allFields) + return request.post({ url: '/datasetTree/save', data: copyData }).then(res => { + if (res?.data?.allFields?.length) { + originNameHandleBack(res?.data?.allFields) + } return res?.data }) } @@ -80,7 +94,12 @@ export const saveDatasetTree = async (data: DatasetOrFolder): Promise // create export const createDatasetTree = async (data: DatasetOrFolder): Promise => { nameTrim(data) - return request.post({ url: '/datasetTree/create', data }).then(res => { + const copyData = cloneDeep(data) + originNameHandle(copyData.allFields) + return request.post({ url: '/datasetTree/create', data: copyData }).then(res => { + if (res?.data?.allFields?.length) { + originNameHandleBack(res?.data?.allFields) + } return res?.data }) } @@ -135,7 +154,8 @@ export const exportDatasetData = (data = {}) => { url: '/datasetTree/exportDataset', method: 'post', data: data, - loading: true + loading: true, + responseType: 'blob' }) } @@ -177,7 +197,16 @@ export const getTableField = async (data): Promise => { } export const getPreviewData = async (data): Promise => { - return request.post({ url: '/datasetData/previewData', data }).then(res => { + const copyData = cloneDeep(data) + originNameHandle(copyData.allFields) + return request.post({ url: '/datasetData/previewData', data: copyData }).then(res => { + if (res?.data?.allFields?.length) { + originNameHandleBack(res?.data?.allFields) + } + + if (res?.data?.data?.fields?.length) { + originNameHandleBack(res?.data?.data?.fields) + } return res?.data }) } @@ -196,6 +225,9 @@ export const getDatasetTotal = async (id): Promise => { export const getDatasetDetails = async (id): Promise => { return request.post({ url: `/datasetTree/details/${id}`, data: {} }).then(res => { + if (res?.data?.allFields?.length) { + originNameHandleBack(res?.data?.allFields) + } return res?.data }) } @@ -219,6 +251,9 @@ export const getDsDetails = async (data): Promise => { } export const getDsDetailsWithPerm = async (data): Promise => { return request.post({ url: '/datasetTree/detailWithPerm', data }).then(res => { + ;(res?.data || []).forEach(ele => { + originNameHandleBackWithArr(ele, ['dimensionList', 'quotaList']) + }) return res?.data }) } @@ -236,15 +271,22 @@ export const columnPermissionList = (page: number, limit: number, datasetId: num export const rowPermissionTargetObjList = (datasetId: number, type: string) => request.get({ url: '/dataset/rowPermissions/authObjs/' + datasetId + '/' + type }) -export const listFieldByDatasetGroup = (datasetId: number) => - request.post({ url: '/datasetField/listByDatasetGroup/' + datasetId }) +export const listFieldByDatasetGroup = (datasetId: number) => { + return request.post({ url: '/datasetField/listByDatasetGroup/' + datasetId }).then(res => { + originNameHandleBack(res?.data) + return res + }) +} export const multFieldValuesForPermissions = (data = {}) => { return request.post({ url: '/datasetField/multFieldValuesForPermissions', data }) } export const listFieldsWithPermissions = (datasetId: number) => { - return request.get({ url: '/datasetField/listWithPermissions/' + datasetId }) + return request.get({ url: '/datasetField/listWithPermissions/' + datasetId }).then(res => { + originNameHandleBack(res?.data) + return res + }) } export const copilotFields = (datasetId: number) => { @@ -301,11 +343,11 @@ export const getFunction = async (): Promise => { }) } -export const exportTasks = async (type): Promise => { - return request.post({ url: '/exportCenter/exportTasks/' + type, data: {} }).then(res => { - return res - }) -} +export const exportTasksRecords = () => + request.post({ url: `/exportCenter/exportTasks/records`, data: {} }) + +export const exportTasks = (page: number, limit: number, status: string) => + request.post({ url: `/exportCenter/exportTasks/${status}/${page}/${limit}`, data: {} }) export const exportRetry = async (id): Promise => { return request.post({ url: '/exportCenter/retry/' + id, data: {} }).then(res => { @@ -325,6 +367,12 @@ export const exportDelete = async (id): Promise => { }) } +export const generateDownloadUri = async (id): Promise => { + return request.get({ url: '/exportCenter/generateDownloadUri/' + id }).then(res => { + return res?.data + }) +} + export const exportDeleteAll = async (type, data): Promise => { return request.post({ url: '/exportCenter/deleteAll/' + type, data }).then(res => { return res?.data diff --git a/frontend/src/api/data-visualization/visualization/dataVisualization.ts b/frontend/src/api/data-visualization/visualization/dataVisualization.ts index d959063..a51b548 100644 --- a/frontend/src/api/data-visualization/visualization/dataVisualization.ts +++ b/frontend/src/api/data-visualization/visualization/dataVisualization.ts @@ -1,10 +1,14 @@ import request from '@/data-visualization/config/axios' +import { originNameHandleWithArr } from '@/data-visualization/utils/CalculateFields' +import { cloneDeep } from 'lodash-es' export interface ResourceOrFolder { name: string id?: number | string pid?: number | string nodeType: 'folder' | 'leaf' type: string + mobileLayout: boolean + status: boolean } export interface Panel { @@ -20,11 +24,16 @@ export const findCopyResource = async (dvId, busiFlag): Promise => { } export const findById = async ( - dvId:any, - busiFlag:any, + dvId, + busiFlag, attachInfo = { source: 'main', taskId: null } ): Promise => { let busiFlagResult = busiFlag + if (!busiFlagResult) { + await findDvType(dvId).then(res => { + busiFlagResult = res.data + }) + } const data = { id: dvId, busiFlag: busiFlagResult, ...attachInfo } return request.post({ url: '/dataVisualization/findById', data }) } @@ -60,8 +69,24 @@ export const appCanvasNameCheck = async data => export const updateBase = data => request.post({ url: '/dataVisualization/updateBase', data }) -export const updateCanvas = data => - request.post({ url: '/dataVisualization/updateCanvas', data, loading: true }) +export const updateCanvas = data => { + const copyData = cloneDeep(data) + const fields = [ + 'xAxis', + 'xAxisExt', + 'yAxis', + 'yAxisExt', + 'extBubble', + 'extLabel', + 'extStack', + 'extTooltip' + ] + + for (const key in copyData.canvasViewInfo) { + originNameHandleWithArr(copyData.canvasViewInfo[key], fields) + } + return request.post({ url: '/dataVisualization/updateCanvas', data: copyData, loading: true }) +} export const moveResource = data => request.post({ url: '/dataVisualization/move', data }) diff --git a/frontend/src/api/data-visualization/visualization/linkJump.ts b/frontend/src/api/data-visualization/visualization/linkJump.ts index 615401f..97306f0 100644 --- a/frontend/src/api/data-visualization/visualization/linkJump.ts +++ b/frontend/src/api/data-visualization/visualization/linkJump.ts @@ -26,9 +26,9 @@ export function queryTargetVisualizationJumpInfo(requestInfo) { }) } -export function queryVisualizationJumpInfo(dvId) { +export function queryVisualizationJumpInfo(dvId, resourceTable = 'snapshot') { return request.get({ - url: '/linkJump/queryVisualizationJumpInfo/' + dvId, + url: '/linkJump/queryVisualizationJumpInfo/' + dvId + '/' + resourceTable, loading: false }) } diff --git a/frontend/src/api/data-visualization/visualization/linkage.ts b/frontend/src/api/data-visualization/visualization/linkage.ts index 41d50dd..ae8ffd3 100644 --- a/frontend/src/api/data-visualization/visualization/linkage.ts +++ b/frontend/src/api/data-visualization/visualization/linkage.ts @@ -8,8 +8,8 @@ export const getViewLinkageGatherArray = data => export const saveLinkage = data => request.post({ url: '/linkage/saveLinkage', data }) -export const getPanelAllLinkageInfo = dvId => - request.get({ url: '/linkage/getVisualizationAllLinkageInfo/' + dvId }) +export const getPanelAllLinkageInfo = (dvId, resourceTable = 'snapshot') => + request.get({ url: '/linkage/getVisualizationAllLinkageInfo/' + dvId + '/' + resourceTable }) export const updateLinkageActive = data => request.post({ url: '/linkage/updateLinkageActive', data }) diff --git a/frontend/src/assets/svg/DataEase.svg b/frontend/src/assets/svg/DataEase.svg index aa2b44b..3fe02e0 100644 --- a/frontend/src/assets/svg/DataEase.svg +++ b/frontend/src/assets/svg/DataEase.svg @@ -1,17 +1,17 @@ - - - - Layer 1 - - - - - - - - - - - GIS-BI开发平台 - + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/svg/Excel-remote-ds.svg b/frontend/src/assets/svg/Excel-remote-ds.svg new file mode 100644 index 0000000..1c9b0b3 --- /dev/null +++ b/frontend/src/assets/svg/Excel-remote-ds.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/svg/bullet-graph-dark.svg b/frontend/src/assets/svg/bullet-graph-dark.svg new file mode 100644 index 0000000..69bfd6a --- /dev/null +++ b/frontend/src/assets/svg/bullet-graph-dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/svg/bullet-graph-origin.svg b/frontend/src/assets/svg/bullet-graph-origin.svg new file mode 100644 index 0000000..69bfd6a --- /dev/null +++ b/frontend/src/assets/svg/bullet-graph-origin.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/svg/bullet-graph.svg b/frontend/src/assets/svg/bullet-graph.svg new file mode 100644 index 0000000..0d54e3d --- /dev/null +++ b/frontend/src/assets/svg/bullet-graph.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/svg/dv-cancel-publish.svg b/frontend/src/assets/svg/dv-cancel-publish.svg new file mode 100644 index 0000000..2fe8923 --- /dev/null +++ b/frontend/src/assets/svg/dv-cancel-publish.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/dv-copy-dark.svg b/frontend/src/assets/svg/dv-copy-dark.svg index 9a1c506..bd8e12a 100644 --- a/frontend/src/assets/svg/dv-copy-dark.svg +++ b/frontend/src/assets/svg/dv-copy-dark.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/src/assets/svg/dv-dashboard-spine-disabled.svg b/frontend/src/assets/svg/dv-dashboard-spine-disabled.svg new file mode 100644 index 0000000..e736673 --- /dev/null +++ b/frontend/src/assets/svg/dv-dashboard-spine-disabled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/svg/dv-dashboard-spine-mobile-disabled.svg b/frontend/src/assets/svg/dv-dashboard-spine-mobile-disabled.svg new file mode 100644 index 0000000..9cef1a1 --- /dev/null +++ b/frontend/src/assets/svg/dv-dashboard-spine-mobile-disabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/assets/svg/dv-recover_outlined.svg b/frontend/src/assets/svg/dv-recover_outlined.svg new file mode 100644 index 0000000..765f1a0 --- /dev/null +++ b/frontend/src/assets/svg/dv-recover_outlined.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/svg/dv-share.svg b/frontend/src/assets/svg/dv-share.svg index 6f76a9b..6c4ca78 100644 --- a/frontend/src/assets/svg/dv-share.svg +++ b/frontend/src/assets/svg/dv-share.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/src/assets/svg/exit-fullscreen.svg b/frontend/src/assets/svg/exit-fullscreen.svg index 485c128..532662d 100644 --- a/frontend/src/assets/svg/exit-fullscreen.svg +++ b/frontend/src/assets/svg/exit-fullscreen.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/assets/svg/icon_excel.svg b/frontend/src/assets/svg/icon_excel.svg index d215044..ac07bdb 100644 --- a/frontend/src/assets/svg/icon_excel.svg +++ b/frontend/src/assets/svg/icon_excel.svg @@ -1,11 +1,5 @@ - - - - - - - - - + + + diff --git a/frontend/src/assets/svg/logo.svg b/frontend/src/assets/svg/logo.svg index 366fe68..6acaee2 100644 --- a/frontend/src/assets/svg/logo.svg +++ b/frontend/src/assets/svg/logo.svg @@ -1,20 +1,21 @@ - - - - background - - - - Layer 1 - - GIS-BI开发平台 - - \ No newline at end of file + + + diff --git a/frontend/src/assets/svg/style-underline.svg b/frontend/src/assets/svg/style-underline.svg index c8da46f..c676981 100644 --- a/frontend/src/assets/svg/style-underline.svg +++ b/frontend/src/assets/svg/style-underline.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/frontend/src/data-visualization/chart/components/editor/common/ChartTemplateInfo.vue b/frontend/src/data-visualization/chart/components/editor/common/ChartTemplateInfo.vue index 8774c12..6c287a5 100644 --- a/frontend/src/data-visualization/chart/components/editor/common/ChartTemplateInfo.vue +++ b/frontend/src/data-visualization/chart/components/editor/common/ChartTemplateInfo.vue @@ -1,45 +1,98 @@
- -
- - {{ - t('visualization.template_view_tips') - }} - -
+
+
+ +
+

{{ t('visualization.template_view_tips') }}

+
+ +
+ + diff --git a/frontend/src/data-visualization/chart/components/editor/common/TemplateTips.vue b/frontend/src/data-visualization/chart/components/editor/common/TemplateTips.vue new file mode 100644 index 0000000..745cc55 --- /dev/null +++ b/frontend/src/data-visualization/chart/components/editor/common/TemplateTips.vue @@ -0,0 +1,76 @@ + + + + diff --git a/frontend/src/data-visualization/chart/components/editor/util/chart.ts b/frontend/src/data-visualization/chart/components/editor/util/chart.ts index 22aa597..d7687fe 100644 --- a/frontend/src/data-visualization/chart/components/editor/util/chart.ts +++ b/frontend/src/data-visualization/chart/components/editor/util/chart.ts @@ -1,6 +1,6 @@ import { useI18n } from '@/data-visualization/hooks/web/useI18n' import { deepCopy } from '@/data-visualization/utils/utils' -import { formatterItem } from '@/data-visualization/chart/components/js/formatter' +import { formatterItem, isEnLocal } from '@/data-visualization/chart/components/js/formatter' const { t } = useI18n() export const DEFAULT_COLOR_CASE: DeepPartial = { @@ -318,6 +318,32 @@ export const DEFAULT_MISC: ChartMiscAttr = { min: 0, max: 0, fieldId: undefined + }, + bullet: { + bar: { + ranges: { + fill: ['rgba(0,128,255,0.3)'], + size: 20, + showType: 'dynamic', + fixedRangeNumber: 3, + symbol: 'circle', + symbolSize: 4 + }, + measures: { + fill: ['rgba(0,128,255,1)'], + size: 15, + symbol: 'circle', + symbolSize: 4 + }, + target: { + fill: 'rgb(0,0,0)', + size: 20, + showType: 'dynamic', + value: 0, + symbol: 'line', + symbolSize: 4 + } + } } } @@ -453,7 +479,8 @@ export const DEFAULT_TABLE_HEADER: ChartTableHeaderAttr = { headerGroupConfig: { columns: [], meta: [] - } + }, + rowHeaderFreeze: true } export const DEFAULT_TABLE_CELL: ChartTableCellAttr = { tableFontColor: '#000000', @@ -552,17 +579,6 @@ export const DEFAULT_TITLE_STYLE_DARK = { remarkBackgroundColor: '#5A5C62' } -export const DEFAULT_LEGEND_STYLE: ChartLegendStyle = { - show: true, - hPosition: 'center', - vPosition: 'bottom', - orient: 'horizontal', - icon: 'circle', - color: '#333333', - fontSize: 12, - size: 4 -} - export const DEFAULT_LEGEND_STYLE_BASE: ChartLegendStyle = { show: true, hPosition: 'center', @@ -571,7 +587,24 @@ export const DEFAULT_LEGEND_STYLE_BASE: ChartLegendStyle = { icon: 'circle', color: '#333333', fontSize: 12, - size: 4 + size: 4, + showRange: true, + sort: 'none', + customSort: [] +} + +export const DEFAULT_LEGEND_STYLE: ChartLegendStyle = { + show: true, + hPosition: 'center', + vPosition: 'bottom', + orient: 'horizontal', + icon: 'circle', + color: '#333333', + fontSize: 12, + size: 4, + showRange: true, + sort: 'none', + customSort: [] } export const DEFAULT_LEGEND_STYLE_LIGHT: ChartLegendStyle = { @@ -634,6 +667,7 @@ export const DEFAULT_XAXIS_STYLE: ChartAxisStyle = { }, axisLabelFormatter: { type: 'auto', + unitLanguage: isEnLocal ? 'en' : 'ch', unit: 1, suffix: '', decimalCount: 2, @@ -680,6 +714,7 @@ export const DEFAULT_YAXIS_STYLE: ChartAxisStyle = { }, axisLabelFormatter: { type: 'auto', + unitLanguage: isEnLocal ? 'en' : 'ch', unit: 1, suffix: '', decimalCount: 2, @@ -724,6 +759,7 @@ export const DEFAULT_YAXIS_EXT_STYLE: ChartAxisStyle = { }, axisLabelFormatter: { type: 'auto', + unitLanguage: isEnLocal ? 'en' : 'ch', unit: 1, suffix: '', decimalCount: 2, @@ -1165,7 +1201,7 @@ export const CHART_FONT_FAMILY = [ { name: t('chart.font_family_kai_ti'), value: 'KaiTi' } ] -export const CHART_FONT_FAMILY_MAP:any = { +export const CHART_FONT_FAMILY_MAP = { 'Microsoft YaHei': 'Microsoft YaHei', SimSun: 'SimSun, "Songti SC", STSong', SimHei: 'SimHei, Helvetica', @@ -1395,6 +1431,13 @@ export const CHART_TYPE_CONFIGS = [ value: 'stock-line', title: t('chart.chart_stock_line'), icon: 'stock-line' + }, + { + render: 'antv', + category: 'compare', + value: 'bullet-graph', + title: t('chart.bullet_chart'), + icon: 'bullet-graph' } ] }, @@ -1651,6 +1694,7 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = { zoomButtonColor: '#aaa', zoomBackground: '#fff', tableLayoutMode: 'grid', + defaultExpandLevel: 1, calcTopN: false, topN: 5, topNLabel: t('datasource.other'), @@ -1679,7 +1723,9 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = { radarAreaColor: true, circleBorderColor: '#fff', circleBorderWidth: 0, - circlePadding: 0 + circlePadding: 0, + quotaPosition: 'col', + quotaColLabel: t('dataset.value') } export const BASE_VIEW_CONFIG = { diff --git a/frontend/src/data-visualization/chart/components/editor/util/dataVisualization.ts b/frontend/src/data-visualization/chart/components/editor/util/dataVisualization.ts index 93b6919..b8188db 100644 --- a/frontend/src/data-visualization/chart/components/editor/util/dataVisualization.ts +++ b/frontend/src/data-visualization/chart/components/editor/util/dataVisualization.ts @@ -1,5 +1,5 @@ export const VIEW_DETAILS_BASH_STYLE = - '{"id":"view-dialog-details-001","title":"图表明细","sceneId":0,"tableId":"1692381412250939392","type":"table-info","render":"antv","resultCount":1000,"resultMode":"all","refreshViewEnable":false,"refreshTime":5,"refreshUnit":"minute","xAxis":[{"id":"1692330126490","datasourceId":"1691734038709071872","datasetTableId":"7098147058204282880","datasetGroupId":"1692381412250939392","chartId":null,"originName":"月","name":"月","dbFieldName":null,"description":null,"dataeaseName":"f_dd62e53a9192cdf4","groupType":"d","type":"ANY","precision":null,"scale":null,"deType":0,"deExtractType":0,"extField":0,"checked":true,"columnIndex":null,"lastSyncTime":null,"dateFormat":null,"dateFormatType":null,"fieldShortName":"f_dd62e53a9192cdf4","summary":null,"sort":"none","dateStyle":"y_M_d","datePattern":"date_sub","chartType":null,"compareCalc":null,"logic":null,"filterType":null,"index":null,"formatterCfg":null,"chartShowName":null,"filter":[],"customSort":null,"busiType":null},{"id":"1692330126489","datasourceId":"1691734038709071872","datasetTableId":"7098147058204282880","datasetGroupId":"1692381412250939392","chartId":null,"originName":"年份","name":"年份","dbFieldName":null,"description":null,"dataeaseName":"f_190480c43bdda8df","groupType":"q","type":"BIGINT","precision":null,"scale":null,"deType":2,"deExtractType":2,"extField":0,"checked":true,"columnIndex":null,"lastSyncTime":null,"dateFormat":null,"dateFormatType":null,"fieldShortName":"f_190480c43bdda8df","summary":"sum","sort":"none","dateStyle":"y_M_d","datePattern":"date_sub","chartType":"bar","compareCalc":{"type":"none","resultData":"percent","field":null,"custom":null},"logic":null,"filterType":null,"index":null,"formatterCfg":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"chartShowName":null,"filter":[],"customSort":null,"busiType":null}],"xAxisExt":[],"yAxis":[],"yAxisExt":[],"extStack":[],"drillFields":[],"viewFields":[],"extBubble":[],"extLabel":[],"extTooltip":[],"customFilter":{},"customAttr":{"basicStyle":{"alpha":100,"tableBorderColor":"#E6E7E4","tableScrollBarColor":"#00000024","tableColumnMode":"adapt","tableColumnWidth":100,"tablePageMode":"pull","tablePageSize":20,"gaugeStyle":"default","colorScheme":"default","colors":["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],"mapVendor":"amap","gradient":false,"lineWidth":2,"lineSymbol":"circle","lineSymbolSize":4,"lineSmooth":true,"barDefault":true,"barWidth":40,"barGap":0.4,"lineType":"solid","scatterSymbol":"circle","scatterSymbolSize":8,"radarShape":"polygon","mapStyle":"normal","areaBorderColor":"#303133","suspension":true,"areaBaseColor":"#FFFFFF","mapSymbolOpacity":0.7,"mapSymbolStrokeWidth":2,"mapSymbol":"circle","mapSymbolSize":20,"radius":100,"innerRadius":60},"misc":{"pieInnerRadius":0,"pieOuterRadius":80,"radarShape":"polygon","radarSize":80,"gaugeMinType":"fix","gaugeMinField":{"id":"","summary":""},"gaugeMin":0,"gaugeMaxType":"fix","gaugeMaxField":{"id":"","summary":""},"gaugeMax":100,"gaugeStartAngle":225,"gaugeEndAngle":-45,"nameFontSize":18,"valueFontSize":18,"nameValueSpace":10,"valueFontColor":"#5470c6","valueFontFamily":"Microsoft YaHei","valueFontIsBolder":false,"valueFontIsItalic":false,"valueLetterSpace":0,"valueFontShadow":false,"showName":true,"nameFontColor":"#000000","nameFontFamily":"Microsoft YaHei","nameFontIsBolder":false,"nameFontIsItalic":false,"nameLetterSpace":"0","nameFontShadow":false,"treemapWidth":80,"treemapHeight":80,"liquidMax":100,"liquidMaxType":"fix","liquidMaxField":{"id":"","summary":""},"liquidSize":80,"liquidShape":"circle","hPosition":"center","vPosition":"center","mapPitch":0,"mapLineType":"arc","mapLineWidth":1,"mapLineAnimateDuration":3,"mapLineGradient":false,"mapLineSourceColor":"#146C94","mapLineTargetColor":"#576CBC"},"label":{"show":false,"position":"top","color":"#000000","fontSize":10,"formatter":"","labelLine":{"show":true},"labelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"reserveDecimalCount":2,"labelShadow":false,"labelBgColor":"","labelShadowColor":"","quotaLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"showDimension":true,"showQuota":false,"showProportion":true,"seriesLabelFormatter":[]},"tooltip":{"show":true,"trigger":"item","confine":true,"fontSize":10,"color":"#000000","tooltipFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"backgroundColor":"#FFFFFF","seriesTooltipFormatter":[]},"tableTotal":{"row":{"showGrandTotals":true,"showSubTotals":true,"reverseLayout":false,"reverseSubLayout":false,"label":"总计","subLabel":"小计","subTotalsDimensions":[],"calcTotals":{"aggregation":"SUM"},"calcSubTotals":{"aggregation":"SUM"},"totalSort":"none","totalSortField":""},"col":{"showGrandTotals":true,"showSubTotals":true,"reverseLayout":false,"reverseSubLayout":false,"label":"总计","subLabel":"小计","subTotalsDimensions":[],"calcTotals":{"aggregation":"SUM"},"calcSubTotals":{"aggregation":"SUM"},"totalSort":"none","totalSortField":""}},"tableHeader":{"indexLabel":"序号","showIndex":false,"tableHeaderAlign":"left","tableHeaderBgColor":"#F5F6F7","tableHeaderFontColor":"#646A73","tableTitleFontSize":14,"tableTitleHeight":36},"tableCell":{"tableFontColor":"#1F2329","tableItemAlign":"right","tableItemBgColor":"#FFFFFF","tableItemFontSize":14,"tableItemHeight":36},"map":{"id":"","level":"world"}},"customStyle":{"text":{"show":false,"fontSize":"18","hPosition":"left","vPosition":"top","isItalic":false,"isBolder":true,"remarkShow":false,"remark":"","fontFamily":"Microsoft YaHei","letterSpace":"0","fontShadow":false,"color":"#000000","remarkBackgroundColor":"#ffffff"},"legend":{"show":true,"hPosition":"center","vPosition":"bottom","orient":"horizontal","icon":"circle","color":"#000000","fontSize":12},"xAxis":{"show":true,"position":"bottom","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":true,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":false,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":10,"max":100,"split":10,"splitCount":10},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"yAxis":{"show":true,"position":"left","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":false,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":10,"max":100,"split":10,"splitCount":10},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"yAxisExt":{"show":true,"position":"right","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":false,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":null,"max":null,"split":null,"splitCount":null},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"misc":{"showName":false,"color":"#000000","fontSize":12,"axisColor":"#999","splitNumber":5,"axisLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"type":"solid"}},"axisTick":{"show":false,"length":5,"lineStyle":{"color":"#000000","width":1,"type":"solid"}},"axisLabel":{"show":false,"rotate":0,"margin":8,"color":"#000000","fontSize":"12","formatter":"{value}"},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"type":"solid"}},"splitArea":{"show":true}}},"senior":{"functionCfg":{"sliderShow":false,"sliderRange":[0,10],"sliderBg":"#FFFFFF","sliderFillBg":"#BCD6F1","sliderTextColor":"#999999","emptyDataStrategy":"breakLine","emptyDataFieldCtrl":[]},"assistLine":[],"threshold":{"gaugeThreshold":"","labelThreshold":[],"tableThreshold":[],"textLabelThreshold":[]},"scrollCfg":{"open":false,"row":1,"interval":2000,"step":50}},"chartExtRequest":{"user":"1","filter":[],"drill":[],"resultCount":1000,"resultMode":"all"}}' + '{"id":"view-dialog-details-001","title":"图表明细","sceneId":0,"tableId":"1692381412250939392","type":"table-info","render":"antv","resultCount":1000,"resultMode":"all","refreshViewEnable":false,"refreshTime":5,"refreshUnit":"minute","xAxis":[{"id":"1692330126490","datasourceId":"1691734038709071872","datasetTableId":"7098147058204282880","datasetGroupId":"1692381412250939392","chartId":null,"originName":"月","name":"月","dbFieldName":null,"description":null,"gisbiName":"f_dd62e53a9192cdf4","groupType":"d","type":"ANY","precision":null,"scale":null,"deType":0,"deExtractType":0,"extField":0,"checked":true,"columnIndex":null,"lastSyncTime":null,"dateFormat":null,"dateFormatType":null,"fieldShortName":"f_dd62e53a9192cdf4","summary":null,"sort":"none","dateStyle":"y_M_d","datePattern":"date_sub","chartType":null,"compareCalc":null,"logic":null,"filterType":null,"index":null,"formatterCfg":null,"chartShowName":null,"filter":[],"customSort":null,"busiType":null},{"id":"1692330126489","datasourceId":"1691734038709071872","datasetTableId":"7098147058204282880","datasetGroupId":"1692381412250939392","chartId":null,"originName":"年份","name":"年份","dbFieldName":null,"description":null,"gisbiName":"f_190480c43bdda8df","groupType":"q","type":"BIGINT","precision":null,"scale":null,"deType":2,"deExtractType":2,"extField":0,"checked":true,"columnIndex":null,"lastSyncTime":null,"dateFormat":null,"dateFormatType":null,"fieldShortName":"f_190480c43bdda8df","summary":"sum","sort":"none","dateStyle":"y_M_d","datePattern":"date_sub","chartType":"bar","compareCalc":{"type":"none","resultData":"percent","field":null,"custom":null},"logic":null,"filterType":null,"index":null,"formatterCfg":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"chartShowName":null,"filter":[],"customSort":null,"busiType":null}],"xAxisExt":[],"yAxis":[],"yAxisExt":[],"extStack":[],"drillFields":[],"viewFields":[],"extBubble":[],"extLabel":[],"extTooltip":[],"customFilter":{},"customAttr":{"basicStyle":{"alpha":100,"tableBorderColor":"#E6E7E4","tableScrollBarColor":"#00000024","tableColumnMode":"adapt","tableColumnWidth":100,"tablePageMode":"pull","tablePageSize":20,"gaugeStyle":"default","colorScheme":"default","colors":["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],"mapVendor":"amap","gradient":false,"lineWidth":2,"lineSymbol":"circle","lineSymbolSize":4,"lineSmooth":true,"barDefault":true,"barWidth":40,"barGap":0.4,"lineType":"solid","scatterSymbol":"circle","scatterSymbolSize":8,"radarShape":"polygon","mapStyle":"normal","areaBorderColor":"#303133","suspension":true,"areaBaseColor":"#FFFFFF","mapSymbolOpacity":0.7,"mapSymbolStrokeWidth":2,"mapSymbol":"circle","mapSymbolSize":20,"radius":100,"innerRadius":60},"misc":{"pieInnerRadius":0,"pieOuterRadius":80,"radarShape":"polygon","radarSize":80,"gaugeMinType":"fix","gaugeMinField":{"id":"","summary":""},"gaugeMin":0,"gaugeMaxType":"fix","gaugeMaxField":{"id":"","summary":""},"gaugeMax":100,"gaugeStartAngle":225,"gaugeEndAngle":-45,"nameFontSize":18,"valueFontSize":18,"nameValueSpace":10,"valueFontColor":"#5470c6","valueFontFamily":"Microsoft YaHei","valueFontIsBolder":false,"valueFontIsItalic":false,"valueLetterSpace":0,"valueFontShadow":false,"showName":true,"nameFontColor":"#000000","nameFontFamily":"Microsoft YaHei","nameFontIsBolder":false,"nameFontIsItalic":false,"nameLetterSpace":"0","nameFontShadow":false,"treemapWidth":80,"treemapHeight":80,"liquidMax":100,"liquidMaxType":"fix","liquidMaxField":{"id":"","summary":""},"liquidSize":80,"liquidShape":"circle","hPosition":"center","vPosition":"center","mapPitch":0,"mapLineType":"arc","mapLineWidth":1,"mapLineAnimateDuration":3,"mapLineGradient":false,"mapLineSourceColor":"#146C94","mapLineTargetColor":"#576CBC"},"label":{"show":false,"position":"top","color":"#000000","fontSize":10,"formatter":"","labelLine":{"show":true},"labelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"reserveDecimalCount":2,"labelShadow":false,"labelBgColor":"","labelShadowColor":"","quotaLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"showDimension":true,"showQuota":false,"showProportion":true,"seriesLabelFormatter":[]},"tooltip":{"show":true,"trigger":"item","confine":true,"fontSize":10,"color":"#000000","tooltipFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"backgroundColor":"#FFFFFF","seriesTooltipFormatter":[]},"tableTotal":{"row":{"showGrandTotals":true,"showSubTotals":true,"reverseLayout":false,"reverseSubLayout":false,"label":"总计","subLabel":"小计","subTotalsDimensions":[],"calcTotals":{"aggregation":"SUM"},"calcSubTotals":{"aggregation":"SUM"},"totalSort":"none","totalSortField":""},"col":{"showGrandTotals":true,"showSubTotals":true,"reverseLayout":false,"reverseSubLayout":false,"label":"总计","subLabel":"小计","subTotalsDimensions":[],"calcTotals":{"aggregation":"SUM"},"calcSubTotals":{"aggregation":"SUM"},"totalSort":"none","totalSortField":""}},"tableHeader":{"indexLabel":"序号","showIndex":false,"tableHeaderAlign":"left","tableHeaderBgColor":"#F5F6F7","tableHeaderFontColor":"#646A73","tableTitleFontSize":14,"tableTitleHeight":36},"tableCell":{"tableFontColor":"#1F2329","tableItemAlign":"right","tableItemBgColor":"#FFFFFF","tableItemFontSize":14,"tableItemHeight":36},"map":{"id":"","level":"world"}},"customStyle":{"text":{"show":false,"fontSize":"18","hPosition":"left","vPosition":"top","isItalic":false,"isBolder":true,"remarkShow":false,"remark":"","fontFamily":"Microsoft YaHei","letterSpace":"0","fontShadow":false,"color":"#000000","remarkBackgroundColor":"#ffffff"},"legend":{"show":true,"hPosition":"center","vPosition":"bottom","orient":"horizontal","icon":"circle","color":"#000000","fontSize":12},"xAxis":{"show":true,"position":"bottom","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":true,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":false,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":10,"max":100,"split":10,"splitCount":10},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"yAxis":{"show":true,"position":"left","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":false,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":10,"max":100,"split":10,"splitCount":10},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"yAxisExt":{"show":true,"position":"right","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":false,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":null,"max":null,"split":null,"splitCount":null},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"misc":{"showName":false,"color":"#000000","fontSize":12,"axisColor":"#999","splitNumber":5,"axisLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"type":"solid"}},"axisTick":{"show":false,"length":5,"lineStyle":{"color":"#000000","width":1,"type":"solid"}},"axisLabel":{"show":false,"rotate":0,"margin":8,"color":"#000000","fontSize":"12","formatter":"{value}"},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"type":"solid"}},"splitArea":{"show":true}}},"senior":{"functionCfg":{"sliderShow":false,"sliderRange":[0,10],"sliderBg":"#FFFFFF","sliderFillBg":"#BCD6F1","sliderTextColor":"#999999","emptyDataStrategy":"breakLine","emptyDataFieldCtrl":[]},"assistLine":[],"threshold":{"gaugeThreshold":"","labelThreshold":[],"tableThreshold":[],"textLabelThreshold":[]},"scrollCfg":{"open":false,"row":1,"interval":2000,"step":50}},"chartExtRequest":{"user":"1","filter":[],"drill":[],"resultCount":1000,"resultMode":"all"}}' import { DEFAULT_COLOR_CASE_DARK, @@ -61,6 +61,7 @@ export const MOBILE_SETTING_DARK = { export const DEFAULT_DASHBOARD_STYLE_BASE = { gap: 'yes', gapSize: 5, + gapMode: 'middle', showGrid: false, matrixBase: 4, // 当前matrix的基数 (是pcMatrixCount的几倍) resultMode: 'all', // 图表结果显示模式 all 图表 custom 仪表板自定义 diff --git a/frontend/src/data-visualization/chart/components/js/extremumUitl.ts b/frontend/src/data-visualization/chart/components/js/extremumUitl.ts index 6413f62..714aa62 100644 --- a/frontend/src/data-visualization/chart/components/js/extremumUitl.ts +++ b/frontend/src/data-visualization/chart/components/js/extremumUitl.ts @@ -79,7 +79,8 @@ function createExtremumDiv(id, value, formatterCfg, chart) { transform: translateX(-50%); opacity: 1; transition: opacity 0.2s ease-in-out; - white-space:nowrap;` + white-space:nowrap; + overflow:auto;` ) div.textContent = valueFormatter(value, formatterCfg) const span = document.createElement('span') @@ -109,7 +110,7 @@ const noChildrenFieldChart = chart => { * 支持最值图表的折线图,面积图,柱状图,分组柱状图 * @param chart */ -const supportExtremumChartType = chart => { +export const supportExtremumChartType = chart => { return ['line', 'area', 'bar', 'bar-group'].includes(chart.type) } @@ -138,8 +139,8 @@ function removeDivsWithPrefix(parentDivId, prefix) { export const extremumEvt = (newChart, chart, _options, container) => { chart.container = container + clearExtremum(chart) if (!supportExtremumChartType(chart)) { - clearExtremum(chart) return } const { label: labelAttr } = parseJson(chart.customAttr) @@ -150,7 +151,9 @@ export const extremumEvt = (newChart, chart, _options, container) => { i.forEach(item => { delete item._origin.EXTREME }) - const { minItem, maxItem } = findMinMax(i.filter(item => item._origin.value)) + const { minItem, maxItem } = findMinMax( + i.filter(item => item?._origin?.value !== null && item?._origin?.value !== undefined) + ) if (!minItem || !maxItem) { return } @@ -223,6 +226,7 @@ export const createExtremumPoint = (chart, ev) => { divParent.style.zIndex = '1' divParent.style.opacity = '0' divParent.style.transition = 'opacity 0.2s ease-in-out' + divParent.style.overflow = 'visible' // 将父标注加入到图表中 const containerElement = document.getElementById(chart.container) containerElement.insertBefore(divParent, containerElement.firstChild) diff --git a/frontend/src/data-visualization/chart/components/js/formatter.ts b/frontend/src/data-visualization/chart/components/js/formatter.ts index a297fbb..222c12a 100644 --- a/frontend/src/data-visualization/chart/components/js/formatter.ts +++ b/frontend/src/data-visualization/chart/components/js/formatter.ts @@ -1,7 +1,13 @@ -import { Datum } from '@antv/g2plot' +import { find } from 'lodash-es' +import { useI18n } from '@/data-visualization/hooks/web/useI18n' +import { getLocale } from '@/data-visualization/utils/utils' +const { t } = useI18n() + +export const isEnLocal = !['zh', 'zh-cn', 'zh-CN', 'tw'].includes(getLocale()) export const formatterItem = { type: 'auto', // auto,value,percent + unitLanguage: isEnLocal ? 'en' : 'ch', unit: 1, // 换算单位 suffix: '', // 单位后缀 decimalCount: 2, // 小数位数 @@ -10,12 +16,51 @@ export const formatterItem = { // 单位list export const unitType = [ - { name: 'unit_none', value: 1 }, - { name: 'unit_thousand', value: 1000 }, - { name: 'unit_ten_thousand', value: 10000 }, - { name: 'unit_million', value: 1000000 }, - { name: 'unit_hundred_million', value: 100000000 } + { name: t('chart.unit_none'), value: 1 }, + { name: t('chart.unit_thousand'), value: 1000 }, + { name: t('chart.unit_ten_thousand'), value: 10000 }, + { name: t('chart.unit_million'), value: 1000000 }, + { name: t('chart.unit_hundred_million'), value: 100000000 } ] +export const unitEnType = [ + { name: 'None', value: 1 }, + { name: 'Thousand (K)', value: 1000 }, + { name: 'Million (M)', value: 1000000 }, + { name: 'Billion (B)', value: 1000000000 } +] + +export function getUnitTypeList(lang) { + if (isEnLocal) { + return unitEnType + } + if (lang === 'ch') { + return unitType + } + return unitEnType +} + +export function getUnitTypeValue(lang, value) { + const list = getUnitTypeList(lang) + const item = find(list, l => l.value === value) + if (item) { + return value + } + return 1 +} + +export function initFormatCfgUnit(cfg) { + if (cfg && cfg.unitLanguage === undefined) { + cfg.unitLanguage = 'ch' + } + if (cfg && isEnLocal) { + cfg.unitLanguage = 'en' + } + onChangeFormatCfgUnitLanguage(cfg, cfg.unitLanguage) +} + +export function onChangeFormatCfgUnitLanguage(cfg, lang) { + cfg.unit = getUnitTypeValue(lang, cfg.unit) +} // 格式化方式 export const formatterType = [ @@ -47,17 +92,32 @@ export function valueFormatter(value, formatter) { } function transUnit(value, formatter) { + initFormatCfgUnit(formatter) return value / formatter.unit } function transDecimal(value, formatter) { - const resultV = value.toFixed(formatter.decimalCount) + const resultV = retain(value, formatter.decimalCount) as string if (Object.is(parseFloat(resultV), -0)) { return resultV.slice(1) } return resultV } +function retain(value, n) { + if (!n) return Math.round(value) + const tran = Math.round(value * Math.pow(10, n)) / Math.pow(10, n) + let tranV = tran.toString() + const newVal = tranV.indexOf('.') + if (newVal < 0) { + tranV += '.' + } + for (let i = tranV.length - tranV.indexOf('.'); i <= n; i++) { + tranV += '0' + } + return tranV +} + function transSeparatorAndSuffix(value, formatter) { let str = value + '' if (str.match(/^(\d)(\.\d)?e-(\d)/)) { @@ -74,34 +134,27 @@ function transSeparatorAndSuffix(value, formatter) { //百分比没有后缀,直接返回 return str } else { - if (formatter.unit === 1000) { - str += '千' - } else if (formatter.unit === 10000) { - str += '万' - } else if (formatter.unit === 1000000) { - str += '百万' - } else if (formatter.unit === 100000000) { - str += '亿' + const unit = formatter.unit + + if (formatter.unitLanguage === 'ch') { + if (unit === 1000) { + str += t('chart.unit_thousand') + } else if (unit === 10000) { + str += t('chart.unit_ten_thousand') + } else if (unit === 1000000) { + str += t('chart.unit_million') + } else if (unit === 100000000) { + str += t('chart.unit_hundred_million') + } + } else { + if (unit === 1000) { + str += 'K' + } else if (unit === 1000000) { + str += 'M' + } else if (unit === 1000000000) { + str += 'B' + } } } return str + formatter.suffix.replace(/(^\s*)|(\s*$)/g, '') } - -export function singleDimensionTooltipFormatter(param: Datum, chart: Chart, prop = 'category') { - let res - const yAxis = chart.yAxis - const obj = { name: param[prop], value: param.value } - for (let i = 0; i < yAxis.length; i++) { - const f = yAxis[i] - if (f.name === param[prop]) { - if (f.formatterCfg) { - res = valueFormatter(param.value, f.formatterCfg) - } else { - res = valueFormatter(param.value, formatterItem) - } - break - } - } - obj.value = res ?? '' - return obj -} diff --git a/frontend/src/data-visualization/chart/components/js/g2plot_tooltip_carousel.ts b/frontend/src/data-visualization/chart/components/js/g2plot_tooltip_carousel.ts new file mode 100644 index 0000000..75bcb42 --- /dev/null +++ b/frontend/src/data-visualization/chart/components/js/g2plot_tooltip_carousel.ts @@ -0,0 +1,656 @@ +import { DualAxes, Plot } from '@antv/g2plot' + +/** + * 使用 Map 来存储实例,键为 chart.container 对象 + */ +export const CAROUSEL_MANAGER_INSTANCES = new Map() +/** + * 支持的图表类型 + */ +const CHART_CATEGORY = { + COLUMN: ['bar', 'bar-stack', 'bar-group', 'bar-group-stack', 'percentage-bar-stack'], + LINE: ['line', 'area', 'area-stack'], + MIX: ['chart-mix', 'chart-mix-group', 'chart-mix-stack', 'chart-mix-dual-line'], + PIE: ['pie', 'pie-donut'] +} + +/** + * 判断是否为柱状图 + * @param chartType + */ +export function isColumn(chartType: string) { + return CHART_CATEGORY.COLUMN.includes(chartType) +} + +/** + * 判断是否为折线图 + * @param chartType + */ +export function isLine(chartType: string) { + return CHART_CATEGORY.LINE.includes(chartType) +} + +/** + * 判断是否为饼图 + * @param chartType + */ +export function isPie(chartType: string) { + return CHART_CATEGORY.PIE.includes(chartType) +} + +/** + * 判断是否为组合图 + * @param chartType + */ +export function isMix(chartType: string) { + return CHART_CATEGORY.MIX.includes(chartType) +} + +export function isSupport(chartType: string) { + return Object.values(CHART_CATEGORY).some(category => category.includes(chartType)) +} + +// 轮播配置默认值 +const DEFAULT_CAROUSEL_CONFIG: Required = { + xField: '', + duration: 2000, + interval: 2000, + loop: true +} + +type CarouselConfig = { + xField: string + duration?: number + interval?: number + loop?: boolean +} + +/** + * 图表轮播提示管理类 + * */ +class ChartCarouselTooltip { + private plot: Plot | DualAxes + private config: Required + private currentIndex = 0 + private values: string[] = [] + // 合并定时器管理 + private timers = { interval: null, carousel: null } + private states = { paused: false, destroyed: false } + // 图表可视性变化 + private observers: Map = new Map() + // 图表元素大小变化 + private resizeObservers: Map = new Map() + // 图表是否在可视范围内 + private chartIsVisible: boolean + + private constructor(plot: Plot | DualAxes, private chart: Chart, config: CarouselConfig) { + this.plot = plot + this.config = { ...DEFAULT_CAROUSEL_CONFIG, ...config } + this.init() + } + + /** + * 创建或更新实例 + * */ + static manage(plot: Plot | DualAxes, chart: Chart, config: CarouselConfig) { + if (!isSupport(chart.type)) return null + const container = chart.container + let instance = CAROUSEL_MANAGER_INSTANCES.get(container) + + CAROUSEL_MANAGER_INSTANCES.forEach(instance => { + if (container.includes('viewDialog')) { + instance.paused() + } + }) + + if (instance) { + instance.update(plot, chart, config) + return instance + } + if (isSupport(chart.type)) { + instance = new this(plot, chart, config) + CAROUSEL_MANAGER_INSTANCES.set(container, instance) + } + + return instance + } + + /** + * 销毁实例 + * @param container + */ + static destroyByContainer(container: string) { + const instance = CAROUSEL_MANAGER_INSTANCES.get(container) + if (instance) { + instance.destroy() + } + } + + /** + * 通过容器DOM获取对应实例 + * */ + static getInstanceByContainer(container: string) { + const instance = CAROUSEL_MANAGER_INSTANCES.get(container) + if (instance) { + return instance + } + return null + } + + /** + * 通过chart.id销毁对应实例 + * 关闭放大图表弹窗,销毁对应实例 + * 重启图表自身轮播 + * */ + static closeEnlargeDialogDestroy(id?: string) { + // 首先,暂停并删除包含 'viewDialog' 的实例 + CAROUSEL_MANAGER_INSTANCES?.forEach((instance, key) => { + if (instance.chart.id === id && instance.chart.container.includes('viewDialog')) { + const dialogInstance = CAROUSEL_MANAGER_INSTANCES.get(key) + if (dialogInstance) { + dialogInstance.destroy() + } + } + }) + setTimeout(() => { + // 然后,恢复 + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + if (instance.chartIsVisible) { + instance.resume() + } + }) + }, 400) + } + + /** + * 暂停轮播 + * @param id + */ + static paused(id?: string) { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + if (id && instance.chart.id === id) { + setTimeout(() => instance.paused(), 200) + } + if (!id) { + setTimeout(() => instance.paused(), 200) + } + }) + } + + /** + * @param id + */ + static resume(id?: string) { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + if (instance.chart.id === id) { + instance.paused() + setTimeout(() => instance.resume(), 500) + } + if (!id) { + setTimeout(() => instance.resume(), 200) + } + }) + } + + /** + * 初始化核心逻辑 + * */ + private init() { + this.values = [].concat(this.getUniqueValues()) + if (!this.values.length) return + this.chartIsVisible = true + this.states.paused = false + this.states.destroyed = false + this.bindEventListeners() + this.startCarousel() + } + + /** + * 获取图表唯一值集合 + * */ + private getUniqueValues() { + const data = + this.plot instanceof DualAxes + ? [...this.plot.options.data[0], ...this.plot.options.data[1]] + : this.plot.options.data + + return [...new Set(data.map(item => item[this.config.xField]))] + } + + /** + * 启动轮播 + * */ + private startCarousel() { + if (!this.shouldStart()) { + this.stop() + return + } + // 定义启动嵌套定时器的函数 + const startNestedTimers = () => { + // 重置当前索引 + this.currentIndex = 0 + // 定义递归处理数据数组的函数 + const processArray = () => { + if (this.states.paused || this.states.destroyed || !this.isElementFullyVisible()) return + // 获取当前需要显示的值 + const currentValue = this.values[this.currentIndex] + // 计算 Tooltip 显示的位置 + const point = this.calculatePosition(currentValue) + // 高亮当前数据点 + this.highlightElement(currentValue) + if (point) { + // 显示 Tooltip,并设置其位置为顶部 + this.plot.chart.showTooltip(point) + this.plot.chart.getController('tooltip').update() + } + // 更新索引,指向下一个数据点 + this.currentIndex++ + if (this.currentIndex > this.values.length) { + this.currentIndex = 0 + this.hideTooltip() + this.plot.chart.showTooltip({ x: 0, y: 0 }) + this.plot.chart.getController('tooltip').update() + this.unHighlightPoint(currentValue) + this.timers.interval = setTimeout(() => processArray(), this.config.interval) + } else { + // 如果未遍历完,继续处理下一个数据点 + this.timers.carousel = setTimeout(() => processArray(), this.config.duration) + } + } + processArray() + } + this.stop() + startNestedTimers() + } + + /** + * 判断是否满足启动条件' */ + private shouldStart() { + return ( + this.chart.customAttr?.tooltip?.show && + this.chart.customAttr?.tooltip?.carousel?.enable && + this.values.length > 0 && + this.chartIsVisible + ) + } + + /** + * 判断图表是否在可视范围内 + * */ + private isElementFullyVisible(): boolean { + // 全屏 + const isFullscreen = document.fullscreenElement !== null + // 新页面或公共连接 + const isNewPagePublicLink = document + .getElementById('enlarge-inner-content-' + this.chart.id) + ?.getBoundingClientRect() + const isMobileEdit = document.getElementsByClassName('panel-mobile')?.length > 0 + const isMobileList = document.getElementsByClassName('mobile-com-list')?.length > 0 + if (isMobileList) { + return false + } + const rect = this.plot.chart.ele.getBoundingClientRect() + return ( + rect.top >= (isFullscreen || isNewPagePublicLink || isMobileEdit ? 0 : 64) && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ) + } + /** + * 计算元素位置(核心定位逻辑) + * */ + private calculatePosition(value: string) { + const view = this.plot.chart.views?.[0] || this.plot.chart + // 饼图特殊处理 + if (CHART_CATEGORY.PIE.includes(this.chart.type)) { + return this.getPieTooltipPosition(view, value) + } + if (this.plot instanceof DualAxes) { + return this.getDualAxesTooltipPosition(view, value) + } + const types = view + .scale() + .getGeometries() + .map(item => item.type) + let point = { x: 0, y: 0 } + if (!types.length) return point + types.forEach(type => { + if (type === 'interval' || type === 'point') { + point = view + .scale() + .getGeometries() + .find(item => item.type === type) + .elements.find(item => item.data.field === value && (item.model.x || item.model.y))?.model + } + }) + // 处理柱状图和折线图,柱状图固定y轴位置 + const y = CHART_CATEGORY.COLUMN.includes(this.chart.type) ? 0 : [].concat(point?.y)?.[0] + return { x: [].concat(point?.x)?.[0], y: y } + } + + /** + * 计算饼图元素位置 + * */ + private getPieTooltipPosition(view, value: string) { + const piePoint = view + .scale() + .getGeometries()[0] + ?.elements.find(item => item.data.field === value) + ?.getModel() + if (!piePoint) { + return { x: 0, y: 0 } + } + const coordinates = [ + { x: [].concat(piePoint.x)[0], y: piePoint.y[0] }, + { x: piePoint.x[0], y: piePoint.y[1] }, + { x: piePoint.x[1], y: piePoint.y[0] }, + { x: piePoint.x[1], y: piePoint.y[1] } + ] + const index = coordinates.findIndex(coord => { + const items = this.plot.chart.getTooltipItems(coord) + return items.some(item => item.data.field === value) + }) + if (index !== -1) { + return coordinates[index] + } else { + return { + x: piePoint.x[0], + y: piePoint.y[0] + } + } + } + + /** + * 获取双轴图表的 Tooltip 位置 + * @param view + * @param value + * @private + */ + private getDualAxesTooltipPosition(view, value: string) { + const xScale = view.getXScale() + if (!xScale) return { x: 0, y: 0 } + const values = xScale.values + if (values.length < 2) { + const point = view + .getGeometries()?.[0] + .elements[view.getGeometries()?.[0].elements?.length - 1].getModel() + return point || { x: 0, y: 0 } + } + const [rangeStart, rangeEnd] = xScale.range + const totalMonths = values.length + const bandWidth = (rangeEnd - rangeStart) / totalMonths + const index = values.indexOf(value) + const xPos = rangeStart + bandWidth * (index + 0.5) + return view.getCoordinate().convert({ x: xPos, y: 0 }) + } + + /** + * 高亮指定元素 + * */ + private highlightElement(value: string) { + if (CHART_CATEGORY.LINE.includes(this.chart.type)) return + this.unHighlightPoint(value) + this.plot.setState( + this.getHighlightType(), + (data: any) => data[this.config.xField] === value, + true + ) + } + + /** + * 取消高亮 + * **/ + private unHighlightPoint(value?: string) { + if (CHART_CATEGORY.LINE.includes(this.chart.type)) return + this.plot.setState( + this.getHighlightType(), + (data: any) => data[this.config.xField] !== value, + false + ) + } + private getHighlightType() { + return 'active' + } + + /** + * 隐藏工具提示 + * */ + private hideTooltip() { + const container = this.getTooltipContainer() + if (container) { + container.style.display = 'none' + } + } + + /** + * 获取工具提示容器 + * */ + private getTooltipContainer() { + const tooltipCtl = this.plot.chart.getController('tooltip') + if (!tooltipCtl) { + return + } + return tooltipCtl.tooltip?.cfg?.container + } + + /** + * 绑定事件监听 + * */ + private bindEventListeners() { + // 定义图表元素ID前缀数组 + // 图表在不同的显示页面可能有不同的ID前缀 + const chartElementIds = ['enlarge-inner-content-', 'enlarge-inner-shape-'] + let chartElement = null + + // 查找图表元素 + for (const idPrefix of chartElementIds) { + chartElement = document.getElementById(idPrefix + this.chart.id) + if (chartElement) break + } + + // 绑定鼠标进入和离开事件 + chartElement?.addEventListener('mouseenter', () => this.paused()) + chartElement?.addEventListener('mouseleave', ev => { + setTimeout(() => { + // 获取鼠标位置 + const mouseX = ev.clientX + const mouseY = ev.clientY + // 获取div的边界信息 + const rect = chartElement.getBoundingClientRect() + // 判断鼠标位置是否在div内 + const isInside = + mouseX >= rect.left + 10 && + mouseX <= rect.right - 10 && + mouseY >= rect.top + 10 && + mouseY <= rect.bottom - 10 + console.log(isInside) + if (!isInside) { + this.paused() + this.resume() + } + }, 300) + }) + + // 定义鼠标滚轮事件处理函数 + const handleMouseWheel = this.debounce(() => { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + instance.paused() + instance.resume() + }) + }, 50) + // 定义 touchmove 事件处理函数(移动端) + const handleTouchMove = (event: TouchEvent) => { + handleMouseWheel(event) + } + // 获取目标元素,优先全屏预览 + const targetDiv = + document.getElementById('de-preview-content') || + document.getElementById('preview-canvas-main') || + document.getElementById('dv-main-center') || + document.getElementById('edit-canvas-main') || + document.getElementById('canvas-mark-line') || + document.getElementById('de-canvas-canvas-main') + // 绑定目标元素的事件 + if (targetDiv) { + targetDiv.removeEventListener('wheel', handleMouseWheel) + targetDiv.addEventListener('wheel', handleMouseWheel) + //移除和添加 touchmove 事件监听器(移动端) + targetDiv.removeEventListener('touchmove', handleTouchMove) + targetDiv.addEventListener('touchmove', handleTouchMove) + } + // 页面可见性控制 + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + instance.paused() + }) + } else if (this.chartIsVisible) { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + instance.resume() + }) + } + }) + // 元素可视性观察(交叉观察器) + this.setupIntersectionObserver() + // 元素大小观察(大小观察器) + this.setupResizeObserver() + } + + /** + * 设置暂停状态 + * */ + private setPaused(state: boolean) { + this.states.paused = state + state ? this.stop() : this.startCarousel() + } + /** + * 设置交叉观察器 + * */ + private setupIntersectionObserver() { + setTimeout(() => { + // 监听元素可见性变化,全部可见时开始轮播 + if (!this.observers.get(this.plot.chart.ele.id)) { + this.observers.set( + this.plot.chart.ele.id, + new IntersectionObserver( + entries => { + entries.forEach(entry => { + if (entry.intersectionRatio < 0.7) { + this.paused() + this.chartIsVisible = false + } else { + this.paused() + this.chartIsVisible = true + this.resume() + } + }) + }, + { threshold: [0.7] } + ) + ) + this.observers.get(this.plot.chart.ele.id).observe(this.plot.chart.ele) + } + }, 100) + } + + /** + * 设置元素大小观察器 + * 当元素全部可见时 + * 图表的最外层元素 + * @private + */ + private setupResizeObserver() { + // 放大图表弹窗不需要监听 + if (this.plot.chart.ele.id.includes('viewDialog')) return + // 创建防抖回调函数 + const debouncedCallback = (entries: ResizeObserverEntry[]) => { + for (const entry of entries) { + if (entry.target) { + this.debounce(() => { + this.paused() + this.resume() + }, 200) + } + } + } + // 监听元素大小, 发生变化时重新轮播 + if (!this.resizeObservers.get(this.plot.chart.ele.id)) { + this.resizeObservers.set(this.plot.chart.ele.id, new ResizeObserver(debouncedCallback)) + this.resizeObservers.get(this.plot.chart.ele.id).observe(this.plot.chart.ele) + } + } + + /** + * 更新配置 + * */ + private update(plot: Plot | DualAxes, chart: Chart, config: CarouselConfig) { + this.stop() + this.plot = plot + this.chart = chart + this.config = { ...this.config, ...config } + this.currentIndex = 0 + this.init() + } + + /** + * 停止定时器 + * @private + */ + private stop() { + clearTimeout(this.timers.interval) + clearTimeout(this.timers.carousel) + this.timers = { interval: null, carousel: null } + } + + /** + * 销毁实例 + * */ + destroy() { + this.stop() + this.clearObserver() + this.states.destroyed = true + CAROUSEL_MANAGER_INSTANCES.delete(this.chart.container) + } + /** + * 清除观察器 + * */ + clearObserver() { + const observer = this.observers.get(this.plot.chart.ele.id) + if (observer) { + observer.disconnect() + this.observers.delete(this.plot.chart.ele.id) + } + const resizeObservers = this.resizeObservers.get(this.plot.chart.ele.id) + if (resizeObservers) { + resizeObservers.disconnect() + this.resizeObservers.delete(this.plot.chart.ele.id) + } + } + /** 暂停 */ + paused() { + this.hideTooltip() + this.unHighlightPoint() + this.setPaused(true) + } + + /** 恢复 */ + resume() { + this.setPaused(false) + } + + /** + * 防抖 + */ + private debounce(func: (...args: any[]) => void, delay: number): (...args: any[]) => void { + let timeout: number | null = null + return (...args: any[]) => { + if (timeout) clearTimeout(timeout) + timeout = window.setTimeout(() => { + func(...args) + }, delay) + } + } +} + +export default ChartCarouselTooltip diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bar.ts index 3d25530..63d47cc 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bar.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bar.ts @@ -7,7 +7,6 @@ import { import { flow, hexColorToRGBA, - hexToRgba, parseJson, setUpGroupSeriesColor, setUpStackSeriesColor @@ -21,6 +20,7 @@ import { } from '@/data-visualization/chart/components/js/panel/charts/bar/common' import { configPlotTooltipEvent, + configRoundAngle, getLabel, getPadding, getTooltipContainer, @@ -43,7 +43,14 @@ export class Bar extends G2PlotChartView { ...BAR_EDITOR_PROPERTY_INNER, 'basic-style-selector': [...BAR_EDITOR_PROPERTY_INNER['basic-style-selector'], 'seriesColor'], 'label-selector': ['vPosition', 'seriesLabelFormatter', 'showExtremum'], - 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'], + 'tooltip-selector': [ + 'fontSize', + 'color', + 'backgroundColor', + 'seriesTooltipFormatter', + 'show', + 'carousel' + ], 'y-axis-selector': [...BAR_EDITOR_PROPERTY_INNER['y-axis-selector'], 'axisLabelFormatter'] } protected baseOptions: ColumnOptions = { @@ -69,11 +76,14 @@ export class Bar extends G2PlotChartView { async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, container, action } = drawOptions + chart.container = container if (!chart?.data?.data?.length) { - chart.container = container clearExtremum(chart) return } + const isGroup = 'bar-group' === this.name && chart.xAxisExt?.length > 0 + const isStack = + ['bar-stack', 'bar-group-stack'].includes(this.name) && chart.extStack?.length > 0 const data = cloneDeep(drawOptions.chart.data?.data) const initOptions: ColumnOptions = { ...this.baseOptions, @@ -108,7 +118,7 @@ export class Bar extends G2PlotChartView { const label = { fields: [], ...tmpOptions.label, - formatter: (data: Datum, _point) => { + formatter: (data: Datum) => { if (data.EXTREME) { return '' } @@ -174,19 +184,9 @@ export class Bar extends G2PlotChartView { color } } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const columnStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - columnStyle - } + options = { + ...options, + ...configRoundAngle(chart, 'columnStyle') } let columnWidthRatio const _v = basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio @@ -227,7 +227,10 @@ export class Bar extends G2PlotChartView { tickCount: axisValue.splitCount } } - return { ...tmpOptions, ...axis } + // 根据axis的最小值,过滤options中的data数据,过滤掉小于最小值的数据 + const { data } = options + const newData = data.filter(item => item.value >= axisValue.min) + return { ...tmpOptions, data: newData, ...axis } } return tmpOptions } @@ -276,7 +279,14 @@ export class StackBar extends Bar { 'totalFormatter', 'showStackQuota' ], - 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'tooltipFormatter', 'show'] + 'tooltip-selector': [ + 'fontSize', + 'color', + 'backgroundColor', + 'tooltipFormatter', + 'show', + 'carousel' + ] } protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions { let label = getLabel(chart) @@ -438,6 +448,74 @@ export class GroupBar extends StackBar { } } + async drawChart(drawOptions: G2PlotDrawOptions): Promise { + const plot = await super.drawChart(drawOptions) + if (!plot) { + return plot + } + const { chart } = drawOptions + const { xAxis, xAxisExt, yAxis } = chart + let innerSort = !!(xAxis.length && xAxisExt.length && yAxis.length) + if (innerSort && yAxis[0].sort === 'none') { + innerSort = false + } + if (innerSort && xAxisExt[0].sort !== 'none') { + const sortPriority = chart.sortPriority ?? [] + const yAxisIndex = sortPriority?.findIndex(e => e.id === yAxis[0].id) + const xAxisExtIndex = sortPriority?.findIndex(e => e.id === xAxisExt[0].id) + if (xAxisExtIndex <= yAxisIndex) { + innerSort = false + } + } + if (!innerSort) { + return plot + } + plot.chart.once('beforepaint', () => { + const geo = plot.chart.geometries[0] + const originMapping = geo.beforeMapping.bind(geo) + geo.beforeMapping = originData => { + const values = geo.getXScale().values + const valueMap = values.reduce((p, n) => { + if (!p?.[n]) { + p[n] = { + fieldArr: [], + indexArr: [], + dataArr: [] + } + } + originData.forEach((arr, arrIndex) => { + arr.forEach((item, index) => { + if (item._origin.field === n) { + p[n].fieldArr.push(item.field) + p[n].indexArr.push([arrIndex, index]) + p[n].dataArr.push(item) + } + }) + }) + return p + }, {}) + values.forEach(v => { + const item = valueMap[v] + item.dataArr.sort((a, b) => { + if (yAxis[0].sort === 'asc') { + return a.value - b.value + } + if (yAxis[0].sort === 'desc') { + return b.value - a.value + } + return 0 + }) + item.indexArr.forEach((index, i) => { + item.dataArr[i].field = item.fieldArr[i] + originData[index[0]][index[1]] = item.dataArr[i] + }) + }) + return originMapping(originData) + } + }) + return plot + } + protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions { const tmpLabel = getLabel(chart) if (!tmpLabel) { @@ -448,7 +526,7 @@ export class GroupBar extends StackBar { baseOptions.label.style.fill = labelAttr.color const label = { ...baseOptions.label, - formatter: function (param: Datum, _point) { + formatter: function (param: Datum) { if (param.EXTREME) { return '' } @@ -492,6 +570,7 @@ export class GroupBar extends StackBar { super(name) this.baseOptions = { ...this.baseOptions, + marginRatio: 0, isGroup: true, isStack: false, meta: { @@ -606,7 +685,7 @@ export class PercentageStackBar extends GroupStackBar { propertyInner = { ...this['propertyInner'], 'label-selector': ['color', 'fontSize', 'vPosition', 'reserveDecimalCount'], - 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'show'] + 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'show', 'carousel'] } protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions { const baseOptions = super.configLabel(chart, options) diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bidirectional-bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bidirectional-bar.ts index 41456d0..d327265 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bidirectional-bar.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bidirectional-bar.ts @@ -6,14 +6,14 @@ import { cloneDeep, defaultTo, isEmpty, map } from 'lodash-es' import { configAxisLabelLengthLimit, configPlotTooltipEvent, + configRoundAngle, getPadding, getTooltipContainer, getTooltipItemConditionColor, getYAxis, getYAxisExt, setGradientColor, - TOOLTIP_TPL, - addConditionsStyleColorToData + TOOLTIP_TPL } from '@/data-visualization/chart/components/js/panel/common/common_antv' import type { BidirectionalBar as G2BidirectionalBar, @@ -213,19 +213,9 @@ export class BidirectionalHorizontalBar extends G2PlotChartView< ...options, layout: basicStyle.layout } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const barStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - barStyle - } + options = { + ...options, + ...configRoundAngle(chart, 'barStyle') } return options } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bullet-graph.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bullet-graph.ts new file mode 100644 index 0000000..66ea6de --- /dev/null +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bullet-graph.ts @@ -0,0 +1,507 @@ +import type { + Bullet as G2Bullet, + BulletOptions as G2BulletOptions +} from '@antv/g2plot/esm/plots/bullet' +import { + G2PlotChartView, + G2PlotDrawOptions +} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot' +import { + BAR_AXIS_TYPE, + BAR_EDITOR_PROPERTY, + BAR_EDITOR_PROPERTY_INNER +} from '@/data-visualization/chart/components/js/panel/charts/bar/common' +import { useI18n } from '@/data-visualization/hooks/web/useI18n' +import { flow, parseJson } from '@/data-visualization/chart/components/js/util' +import { BulletOptions } from '@antv/g2plot' +import { isEmpty } from 'lodash-es' +import { + configAxisLabelLengthLimit, + configPlotTooltipEvent, + getPadding, + getTooltipContainer, + TOOLTIP_TPL +} from '@/data-visualization/chart/components/js/panel/common/common_antv' +import { valueFormatter } from '@/data-visualization/chart/components/js/formatter' + +const { t } = useI18n() + +/** + * 子弹图 + */ +export class BulletGraph extends G2PlotChartView { + constructor() { + super('bullet-graph', []) + } + + axis: AxisType[] = [...BAR_AXIS_TYPE, 'yAxisExt', 'extBubble'] + axisConfig = { + ...this['axisConfig'], + xAxis: { name: `${t('chart.form_type')} / ${t('chart.dimension')}`, type: 'd', limit: 1 }, + yAxis: { name: `${t('chart.progress_current')} / ${t('chart.quota')}`, type: 'q', limit: 1 }, + yAxisExt: { name: `${t('chart.progress_target')} / ${t('chart.quota')}`, type: 'q', limit: 1 }, + extBubble: { + name: `${t('chart.range_bg')} / ${t('chart.quota')}`, + type: 'q', + allowEmpty: true, + limit: 1 + } + } + properties: EditorProperty[] = [ + ...BAR_EDITOR_PROPERTY.filter( + item => !['function-cfg', 'assist-line', 'threshold'].includes(item) + ), + 'bullet-graph-selector' + ] + propertyInner = { + 'basic-style-selector': ['radiusColumnBar', 'layout'], + 'label-selector': ['hPosition', 'fontSize', 'color', 'labelFormatter'], + 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'], + 'x-axis-selector': [ + ...BAR_EDITOR_PROPERTY_INNER['x-axis-selector'].filter(item => item != 'position'), + 'showLengthLimit' + ], + 'y-axis-selector': [ + ...BAR_EDITOR_PROPERTY_INNER['y-axis-selector'].filter( + item => item !== 'axisValue' && item !== 'position' + ), + 'axisLabelFormatter' + ], + 'legend-selector': ['showRange', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition'] + } + + async drawChart(drawOption: G2PlotDrawOptions): Promise { + const { chart, container, action } = drawOption + if (!chart.data?.data?.length) return + const result = mergeBulletData(chart) + // 处理自定义区间 + const { bullet } = parseJson(chart.customAttr).misc + if (bullet.bar.ranges.showType === 'fixed') { + const customRange = bullet.bar.ranges.fixedRange?.map(item => item.fixedRangeValue) || [0] + result.forEach(item => (item.ranges = customRange)) + } else { + result.forEach(item => (item.ranges = item.originalRanges)) + } + // 处理自定义目标值 + if (bullet.bar.target.showType === 'fixed') { + const customTarget = bullet.bar.target.value || 0 + result.forEach(item => (item.target = customTarget)) + } else { + result.forEach(item => (item.target = item.originalTarget)) + } + const initialOptions: BulletOptions = { + appendPadding: getPadding(chart), + data: result.reverse(), + measureField: 'measures', + rangeField: 'ranges', + targetField: 'target', + xField: 'title', + meta: { + title: { + type: 'cat' + } + }, + interactions: [ + { + type: 'active-region', + cfg: { + start: [{ trigger: 'element:mousemove', action: 'active-region:show' }], + end: [{ trigger: 'element:mouseleave', action: 'active-region:hide' }] + } + } + ] + } + const options = this.setupOptions(chart, initialOptions) + let newChart = null + const { Bullet: BulletClass } = await import('@antv/g2plot/esm/plots/bullet') + newChart = new BulletClass(container, options) + newChart.on('element:click', ev => { + const pointData = ev?.data?.data + const dimensionList = options.data.find(item => item.title === pointData.title)?.dimensionList + const actionParams = { + x: ev.x, + y: ev.y, + data: { + data: { + ...pointData, + dimensionList + } + } + } + action(actionParams) + }) + configPlotTooltipEvent(chart, newChart) + configAxisLabelLengthLimit(chart, newChart, null) + return newChart + } + + protected configBasicStyle(chart: Chart, options: BulletOptions): BulletOptions { + const basicStyle = parseJson(chart.customAttr).basicStyle + const { radiusColumnBar, columnBarRightAngleRadius, layout } = basicStyle + let radiusValue = 0 + let rangeLength = 1 + if (radiusColumnBar === 'roundAngle' || radiusColumnBar === 'topRoundAngle') { + radiusValue = columnBarRightAngleRadius + rangeLength = options.data[0]?.ranges?.length + } + const barRadiusStyle = { radius: Array(2).fill(radiusValue) } + const baseRadius = [...barRadiusStyle.radius, ...barRadiusStyle.radius] + options = { + ...options, + bulletStyle: { + range: datum => { + if (!datum.rKey) return { fill: 'rgba(0, 0, 0, 0)' } + if (rangeLength === 1) { + return { + radius: + radiusColumnBar === 'topRoundAngle' ? [...barRadiusStyle.radius, 0, 0] : baseRadius + } + } + if (rangeLength > 1 && datum.rKey === 'ranges_0') { + return { + radius: radiusColumnBar === 'topRoundAngle' ? [] : [0, 0, ...barRadiusStyle.radius] + } + } + if (rangeLength > 1 && datum.rKey === 'ranges_' + (rangeLength - 1)) { + return { radius: [...barRadiusStyle.radius, 0, 0] } + } + }, + measure: datum => { + if (datum.measures) { + return { + radius: + radiusColumnBar === 'topRoundAngle' ? [...barRadiusStyle.radius, 0, 0] : baseRadius + } + } else { + return undefined + } + }, + target: datum => (datum.tKey === 'target' ? { lineWidth: 2 } : undefined) + } + } + if (layout === 'vertical') options = { ...options, layout: 'vertical' } + return options + } + + protected configMisc(chart: Chart, options: BulletOptions): BulletOptions { + const { bullet } = parseJson(chart.customAttr).misc + const isDynamic = bullet.bar.ranges.showType === 'dynamic' + // 动态背景按大小升序 + const rangeColor = isDynamic + ? bullet.bar.ranges.fill + : bullet.bar.ranges.fixedRange + ?.sort((a, b) => (a.fixedRangeValue ?? 0) - (b.fixedRangeValue ?? 0)) + .map(item => item.fill) || [] + return { + ...options, + color: { + measure: [].concat(bullet.bar.measures.fill), + range: [].concat(rangeColor), + target: [].concat(bullet.bar.target.fill) + }, + size: { + measure: bullet.bar.measures.size, + range: bullet.bar.ranges.size, + target: bullet.bar.target.size + } + } + } + + protected configXAxis(chart: Chart, options: BulletOptions): BulletOptions { + const tmpOptions = super.configXAxis(chart, options) + if (!tmpOptions.xAxis || !tmpOptions.xAxis.label) return tmpOptions + + const { layout, xAxis } = tmpOptions + const position = xAxis.position + const style: any = { ...xAxis.label.style } + + if (layout === 'vertical') { + style.textAlign = 'center' + style.textBaseline = position === 'bottom' ? 'top' : 'bottom' + } else { + style.textAlign = position === 'bottom' ? 'end' : 'start' + style.textBaseline = 'middle' + } + + xAxis.label.style = style + return tmpOptions + } + + protected configYAxis(chart: Chart, options: BulletOptions): BulletOptions { + const tmpOptions = super.configYAxis(chart, options) + if (!tmpOptions.yAxis || !tmpOptions.yAxis.label) return tmpOptions + + const yAxis = parseJson(chart.customStyle).yAxis + tmpOptions.yAxis.label.formatter = value => valueFormatter(value, yAxis.axisLabelFormatter) + + const { layout, yAxis: yAxisConfig } = tmpOptions + const position = yAxisConfig.position + const style: any = { ...yAxisConfig.label.style } + + if (layout === 'vertical') { + style.textAlign = position === 'left' ? 'end' : 'start' + style.textBaseline = 'middle' + } else { + style.textAlign = 'center' + style.textBaseline = position === 'left' ? 'top' : 'bottom' + } + + yAxisConfig.label.style = style + return tmpOptions + } + + protected configLabel(chart: Chart, options: BulletOptions): BulletOptions { + const tmpOptions = super.configLabel(chart, options) + if (!tmpOptions.label) return tmpOptions + + const labelAttr = parseJson(chart.customAttr).label + const label: any = { + ...tmpOptions.label, + formatter: param => + param.mKey === 'measures' + ? valueFormatter(param.measures, labelAttr.labelFormatter) + : undefined + } + return { ...tmpOptions, label: { measure: label } } + } + + protected configLegend(chart: Chart, options: BulletOptions): BulletOptions { + const baseLegend = super.configLegend(chart, options).legend + if (!baseLegend) return options + + const { bullet } = parseJson(chart.customAttr).misc + const customStyleLegend = parseJson(chart.customStyle).legend + const items = [] + + const createLegendItem = (value, name, symbol, fill, size = 4) => ({ + value, + name, + marker: { symbol, style: { fill, stroke: value === 'measure' ? '' : fill, r: size } } + }) + + if (customStyleLegend.showRange) { + if (bullet.bar.ranges.showType === 'dynamic') { + if (chart.extBubble.length) { + const rangeName = chart.extBubble[0]?.chartShowName || bullet.bar.ranges.name + items.push( + createLegendItem( + 'dynamic', + rangeName || chart.extBubble[0]?.name, + bullet.bar.ranges.symbol, + [].concat(bullet.bar.ranges.fill)[0], + bullet.bar.ranges.symbolSize + ) + ) + } + } else { + bullet.bar.ranges.fixedRange?.forEach(item => { + items.push( + createLegendItem( + item.name, + item.name, + bullet.bar.ranges.symbol, + item.fill, + bullet.bar.ranges.symbolSize + ) + ) + }) + } + } + + const targetName = chart.yAxisExt[0]?.chartShowName || bullet.bar.target.name + items.push( + createLegendItem( + 'target', + targetName || chart.yAxisExt[0]?.name, + 'line', + [].concat(bullet.bar.target.fill)[0], + bullet.bar.ranges.symbolSize + ) + ) + + const measureName = chart.yAxis[0]?.chartShowName || bullet.bar.measures.name + items.push( + createLegendItem( + 'measure', + measureName || chart.yAxis[0]?.name, + 'square', + [].concat(bullet.bar.measures.fill)[0], + bullet.bar.ranges.symbolSize + ) + ) + + return { + ...options, + legend: { custom: true, position: baseLegend.position, layout: baseLegend.layout, items } + } + } + + protected configTooltip(chart: Chart, options: BulletOptions): BulletOptions { + const customAttr: DeepPartial = parseJson(chart.customAttr) + const tooltipAttr = customAttr.tooltip + const { bullet } = parseJson(chart.customAttr).misc + if (!tooltipAttr.show) return { ...options, tooltip: false } + + const formatterMap = tooltipAttr.seriesTooltipFormatter + ?.filter(i => i.show) + .reduce((pre, next, index) => { + const keys = ['measures', 'target', 'ranges'] + if (keys[index]) pre[keys[index]] = next + return pre + }, {}) as Record + + const tooltip = { + shared: true, + showMarkers: true, + customItems(originalItems) { + if (!tooltipAttr.seriesTooltipFormatter?.length) return originalItems + + const result = [] + const data = options.data.find(item => item.title === originalItems[0].title) + Object.keys(formatterMap).forEach(key => { + if (key === '记录数*') return + const formatter = formatterMap[key] + if (formatter) { + if (key !== 'ranges') { + let value = 0 + if (chart.yAxis[0].id === chart.yAxisExt[0].id) { + value = valueFormatter(parseFloat(data['target'] as string), formatter.formatterCfg) + } else { + value = valueFormatter(parseFloat(data[key] as string), formatter.formatterCfg) + } + const name = isEmpty(formatter.chartShowName) + ? formatter.name + : formatter.chartShowName + result.push({ ...originalItems[0], color: bullet.bar[key].fill, name, value }) + } else { + const ranges = data.ranges + const isDynamic = bullet.bar.ranges.showType === 'dynamic' + ranges.forEach((range, index) => { + const value = valueFormatter( + parseFloat(isDynamic ? data.minRanges[0] : (range as string)), + formatter.formatterCfg + ) + let name = '' + let color: string | string[] + if (bullet.bar.ranges.showType === 'dynamic') { + name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName + color = bullet.bar[key].fill + } else { + const customRange = bullet.bar.ranges.fixedRange[index].name + name = customRange + ? customRange + : isEmpty(formatter.chartShowName) + ? formatter.name + : formatter.chartShowName + color = bullet.bar[key].fixedRange[index].fill + } + result.push({ ...originalItems[0], color, name, value }) + }) + } + } + }) + const dynamicTooltipValue = chart.data.data.find( + d => d.field === originalItems[0]['title'] + )?.dynamicTooltipValue + if (dynamicTooltipValue.length > 0) { + dynamicTooltipValue.forEach(dy => { + const q = tooltipAttr.seriesTooltipFormatter.filter(i => i.id === dy.fieldId) + if (q && q.length > 0) { + const value = valueFormatter(parseFloat(dy.value as string), q[0].formatterCfg) + const name = isEmpty(q[0].chartShowName) ? q[0].name : q[0].chartShowName + result.push({ color: 'grey', name, value }) + } + }) + } + return result + }, + container: getTooltipContainer(`tooltip-${chart.id}`), + itemTpl: TOOLTIP_TPL, + enterable: true + } + return { ...options, tooltip } + } + + setupDefaultOptions(chart: ChartObj): ChartObj { + chart.customAttr.label.position = 'middle' + chart.customStyle.yAxis.splitLine.show = false + return super.setupDefaultOptions(chart) + } + + protected setupOptions(chart: Chart, options: BulletOptions): BulletOptions { + return flow( + this.configTheme, + this.configBasicStyle, + this.configMisc, + this.configXAxis, + this.configYAxis, + this.configLabel, + this.configLegend, + this.configTooltip + )(chart, options, {}, this) + } +} + +/** + * 组装子弹图数据 + * @param chart + */ +function mergeBulletData(chart): any[] { + // 先根据维度分组,再根据指标字段组装成子弹图的格式 + const groupedData = chart.data.data.reduce((acc, item) => { + const field = item.field + if (!acc[field]) { + acc[field] = [] + } + acc[field].push(item) + return acc + }, {}) + const result = [] + // 组装子弹图数据,每个维度对应一个子弹图 + Object.keys(groupedData).forEach(field => { + const items = groupedData[field] + // 初始化子弹图条目结构 + const entry = { + title: field, + ranges: [], + measures: [], + target: [], + dimensionList: items[0].dimensionList, + quotaList: [] + } + + // 防止指标相同时无数据有可能会导致数据不一致 + items.forEach(item => { + const quotaId = item.quotaList[0]?.id + const v = item.value || 0 + if (quotaId === chart.yAxis[0]?.id) { + entry.measures.push(v) + } + if (quotaId === chart.yAxisExt[0]?.id) { + entry.target.push(v) + } + if (quotaId === chart.extBubble[0]?.id) { + entry.ranges.push(v) + } + entry.quotaList.push(item.quotaList[0]) + }) + // 对数据进行累加 + const ranges = chart.extBubble[0]?.id + ? [].concat(entry.ranges?.reduce((acc, curr) => acc + curr, 0)) + : [] + const target = [].concat(entry.target?.reduce((acc, curr) => acc + curr, 0)) + const measures = [].concat(entry.measures?.reduce((acc, curr) => acc + curr, 0)) + const bulletData = { + ...entry, + measures: measures, + target: target, + ranges: ranges, + quotaList: [...entry.quotaList], + minRanges: ranges, + originalRanges: ranges, + originalTarget: target + } + result.push(bulletData) + }) + return result +} diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/horizontal-bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/horizontal-bar.ts index 12e8ec9..a673159 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/horizontal-bar.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/horizontal-bar.ts @@ -6,6 +6,7 @@ import type { Bar, BarOptions } from '@antv/g2plot/esm/plots/bar' import { configAxisLabelLengthLimit, configPlotTooltipEvent, + configRoundAngle, getPadding, getTooltipContainer, setGradientColor, @@ -101,6 +102,17 @@ export class HorizontalBar extends G2PlotChartView { const newChart = new Bar(container, options) newChart.on('interval:click', action) + if (options.label) { + newChart.on('label:click', e => { + action({ + x: e.x, + y: e.y, + data: { + data: e.target.attrs.data + } + }) + }) + } configPlotTooltipEvent(chart, newChart) configAxisLabelLengthLimit(chart, newChart) return newChart @@ -135,7 +147,10 @@ export class HorizontalBar extends G2PlotChartView { tickCount: axisValue.splitCount } } - return { ...tmpOptions, ...axis } + // 根据axis的最小值,过滤options中的data数据,过滤掉小于最小值的数据 + const { data } = options + const newData = data.filter(item => item.value >= axisValue.min) + return { ...tmpOptions, data: newData, ...axis } } return tmpOptions } @@ -157,19 +172,9 @@ export class HorizontalBar extends G2PlotChartView { color } } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const barStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - barStyle - } + options = { + ...options, + ...configRoundAngle(chart, 'barStyle') } let barWidthRatio @@ -234,6 +239,7 @@ export class HorizontalBar extends G2PlotChartView { attrs: { x: 0, y: 0, + data, text: value, textAlign: 'start', textBaseline: 'top', @@ -316,8 +322,24 @@ export class HorizontalStackBar extends HorizontalBar { baseOptions.label.style.fill = labelAttr.color const label = { ...baseOptions.label, - formatter: function (param: Datum) { - return valueFormatter(param.value, labelAttr.labelFormatter) + formatter: function (data: Datum) { + const value = valueFormatter(data.value, labelAttr.labelFormatter) + const group = new Group({}) + group.addShape({ + type: 'text', + attrs: { + x: 0, + y: 0, + data, + text: value, + textAlign: 'start', + textBaseline: 'top', + fontSize: labelAttr.fontSize, + fontFamily: chart.fontFamily, + fill: labelAttr.color + } + }) + return group } } return { @@ -435,11 +457,29 @@ export class HorizontalPercentageStackBar extends HorizontalStackBar { const l = parseJson(customAttr).label const label = { ...baseOptions.label, - formatter: function (param: Datum) { - if (!param.value) { - return '0%' + formatter: function (data: Datum) { + let value = data.value + if (value) { + value = (Math.round(value * 10000) / 100).toFixed(l.reserveDecimalCount) + '%' + } else { + value = '0%' } - return (Math.round(param.value * 10000) / 100).toFixed(l.reserveDecimalCount) + '%' + const group = new Group({}) + group.addShape({ + type: 'text', + attrs: { + x: 0, + y: 0, + data, + text: value, + textAlign: 'start', + textBaseline: 'top', + fontSize: l.fontSize, + fontFamily: chart.fontFamily, + fill: l.color + } + }) + return group } } return { diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/progress-bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/progress-bar.ts index b9399cf..4e0b83c 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/progress-bar.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/progress-bar.ts @@ -3,8 +3,8 @@ import { flow, hexColorToRGBA, parseJson } from '../../../util' import { configAxisLabelLengthLimit, configPlotTooltipEvent, + configRoundAngle, getTooltipContainer, - getTooltipItemConditionColor, setGradientColor, TOOLTIP_TPL } from '../../common/common_antv' @@ -66,7 +66,7 @@ export class ProgressBar extends G2PlotChartView { 'fontSize', 'axisForm', 'axisLabel', - 'position', + // 'position', 'showLengthLimit' ], 'function-cfg': ['emptyDataStrategy'], @@ -166,6 +166,7 @@ export class ProgressBar extends G2PlotChartView { } }) if (basicStyle.gradient) { + // eslint-disable-next-line color1 = color1.map((ele, _index) => { return setGradientColor(ele, true, 0) }) @@ -184,19 +185,9 @@ export class ProgressBar extends G2PlotChartView { } } } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const barStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - barStyle - } + options = { + ...options, + ...configRoundAngle(chart, 'barStyle') } let barWidthRatio @@ -297,12 +288,31 @@ export class ProgressBar extends G2PlotChartView { if (!baseOption.yAxis) { return baseOption } - if (baseOption.yAxis.position === 'left') { + baseOption.yAxis.position = 'bottom' + const yAxis = parseJson(chart.customStyle).yAxis + if (yAxis.axisLabel.show) { + const rotate = yAxis.axisLabel.rotate + let textAlign = 'end' + let textBaseline = 'middle' + if (Math.abs(rotate) > 75) { + textAlign = 'center' + } + if (rotate > 75) { + textBaseline = 'top' + } + if (rotate < -75) { + textBaseline = 'bottom' + } + baseOption.yAxis.label.style.textBaseline = textBaseline + baseOption.yAxis.label.style.textAlign = textAlign + } + + /*if (baseOption.yAxis.position === 'left') { baseOption.yAxis.position = 'bottom' } if (baseOption.yAxis.position === 'right') { baseOption.yAxis.position = 'top' - } + }*/ return baseOption } setupDefaultOptions(chart: ChartObj): ChartObj { diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/range-bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/range-bar.ts index 5db5326..6fb82d7 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/range-bar.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/range-bar.ts @@ -6,6 +6,7 @@ import type { Bar, BarOptions } from '@antv/g2plot/esm/plots/bar' import { configAxisLabelLengthLimit, configPlotTooltipEvent, + configRoundAngle, getPadding, getTooltipContainer, setGradientColor, @@ -22,6 +23,7 @@ import { import { Datum } from '@antv/g2plot/esm/types/common' import { useI18n } from '@/data-visualization/hooks/web/useI18n' import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart' +import { Group } from '@antv/g-canvas' const { t } = useI18n() const DEFAULT_DATA = [] @@ -170,6 +172,17 @@ export class RangeBar extends G2PlotChartView { const newChart = new BarClass(container, options) newChart.on('interval:click', action) + if (options.label) { + newChart.on('label:click', e => { + action({ + x: e.x, + y: e.y, + data: { + data: e.target.attrs.data + } + }) + }) + } configPlotTooltipEvent(chart, newChart) configAxisLabelLengthLimit(chart, newChart) return newChart @@ -309,19 +322,10 @@ export class RangeBar extends G2PlotChartView { } } } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const barStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - barStyle - } + + options = { + ...options, + ...configRoundAngle(chart, 'barStyle') } let barWidthRatio const _v = basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio @@ -391,7 +395,22 @@ export class RangeBar extends G2PlotChartView { valueFormatter(param.values[1], labelAttr.labelFormatter) } } - return res + const group = new Group({}) + group.addShape({ + type: 'text', + attrs: { + x: 0, + y: 0, + data: param, + text: res, + textAlign: 'start', + textBaseline: 'top', + fontSize: labelAttr.fontSize, + fontFamily: chart.fontFamily, + fill: labelAttr.color + } + }) + return group } } return { diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/waterfall.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/waterfall.ts index f827924..518c5c5 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/waterfall.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/waterfall.ts @@ -72,7 +72,8 @@ export class Waterfall extends G2PlotChartView { 'axisForm', 'axisLabel', 'axisLabelFormatter', - 'showLengthLimit' + 'showLengthLimit', + 'axisLine' ], threshold: ['lineThreshold'] } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/line/area.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/line/area.ts index 28a3fc6..8fcba3f 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/line/area.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/line/area.ts @@ -46,7 +46,8 @@ export class Area extends G2PlotChartView { 'label-selector': ['seriesLabelVPosition', 'seriesLabelFormatter', 'showExtremum'], 'tooltip-selector': [ ...LINE_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axis: AxisType[] = [...LINE_AXIS_TYPE] @@ -103,8 +104,8 @@ export class Area extends G2PlotChartView { async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, container, action } = drawOptions + chart.container = container if (!chart.data?.data?.length) { - chart.container = container clearExtremum(chart) return } @@ -147,7 +148,7 @@ export class Area extends G2PlotChartView { fields: [], ...tmpOptions.label, layout: labelAttr.fullDisplay ? [{ type: 'limit-in-plot' }] : tmpOptions.label.layout, - formatter: (data: Datum, _point) => { + formatter: (data: Datum) => { if (data.EXTREME) { return '' } @@ -305,7 +306,7 @@ export class StackArea extends Area { propertyInner = { ...this['propertyInner'], 'label-selector': ['vPosition', 'fontSize', 'color', 'labelFormatter'], - 'tooltip-selector': ['fontSize', 'color', 'tooltipFormatter', 'show'] + 'tooltip-selector': ['fontSize', 'color', 'tooltipFormatter', 'show', 'carousel'] } axisConfig = { ...this['axisConfig'], diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/line/line.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/line/line.ts index d128c6c..5b9b33b 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/line/line.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/line/line.ts @@ -10,10 +10,12 @@ import { TOOLTIP_TPL } from '../../common/common_antv' import { + convertToAlphaColor, flow, getLineConditions, getLineLabelColorByCondition, hexColorToRGBA, + isAlphaColor, parseJson, setUpGroupSeriesColor } from '@/data-visualization/chart/components/js/util' @@ -43,8 +45,10 @@ export class Line extends G2PlotChartView { 'label-selector': ['seriesLabelVPosition', 'seriesLabelFormatter', 'showExtremum'], 'tooltip-selector': [ ...LINE_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' - ] + 'seriesTooltipFormatter', + 'carousel' + ], + 'legend-selector': [...LINE_EDITOR_PROPERTY_INNER['legend-selector'], 'legendSort'] } axis: AxisType[] = [...LINE_AXIS_TYPE, 'xAxisExt'] axisConfig = { @@ -66,8 +70,8 @@ export class Line extends G2PlotChartView { } async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, action, container } = drawOptions + chart.container = container if (!chart.data?.data?.length) { - chart.container = container clearExtremum(chart) return } @@ -146,7 +150,7 @@ export class Line extends G2PlotChartView { fields: [], ...tmpOptions.label, layout: labelAttr.fullDisplay ? [{ type: 'limit-in-plot' }] : tmpOptions.label.layout, - formatter: (data: Datum, _point) => { + formatter: (data: Datum) => { if (data.EXTREME) { return '' } @@ -321,17 +325,30 @@ export class Line extends G2PlotChartView { if (sort?.length) { // 用值域限定排序,有可能出现新数据但是未出现在图表上,所以这边要遍历一下子维度,加到后面,让新数据显示出来 const data = optionTmp.data - data?.forEach(d => { - const cat = d['category'] - if (cat && !sort.includes(cat)) { - sort.push(cat) + const cats = + data?.reduce((p, n) => { + const cat = n['category'] + if (cat && !p.includes(cat)) { + p.push(cat) + } + return p + }, []) || [] + const values = sort.reduce((p, n) => { + if (cats.includes(n)) { + const index = cats.indexOf(n) + if (index !== -1) { + cats.splice(index, 1) + } + p.push(n) } - }) + return p + }, []) + cats.length > 0 && values.push(...cats) optionTmp.meta = { ...optionTmp.meta, category: { type: 'cat', - values: sort + values } } } @@ -351,6 +368,56 @@ export class Line extends G2PlotChartView { fill: style.stroke } } + const { sort, customSort, icon } = customStyle.legend + if (sort && sort !== 'none' && chart.xAxisExt.length) { + const customAttr = parseJson(chart.customAttr) + const { basicStyle } = customAttr + const seriesMap = + basicStyle.seriesColor?.reduce((p, n) => { + p[n.id] = n + return p + }, {}) || {} + const dupCheck = new Set() + const items = optionTmp.data?.reduce((arr, item) => { + if (!dupCheck.has(item.category)) { + const fill = + seriesMap[item.category]?.color ?? + optionTmp.color[dupCheck.size % optionTmp.color.length] + dupCheck.add(item.category) + arr.push({ + name: item.category, + value: item.category, + marker: { + symbol: icon, + style: { + r: size, + fill: isAlphaColor(fill) ? fill : convertToAlphaColor(fill, basicStyle.alpha) + } + } + }) + } + return arr + }, []) + if (sort !== 'custom') { + items.sort((a, b) => { + return sort !== 'desc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name) + }) + } else { + const tmp = [] + ;(customSort || []).forEach(item => { + const index = items.findIndex(i => i.name === item) + if (index !== -1) { + tmp.push(items[index]) + items.splice(index, 1) + } + }) + items.unshift(...tmp) + } + optionTmp.legend.items = items + if (xAxisExt?.customSort?.length > 0) { + delete optionTmp.meta?.category.values + } + } return optionTmp } protected setupOptions(chart: Chart, options: LineOptions): LineOptions { diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/line/stock-line.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/line/stock-line.ts index 625ad65..f621de1 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/line/stock-line.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/line/stock-line.ts @@ -68,22 +68,22 @@ export class StockLine extends G2PlotChartView { const xAxis = chart.xAxis const yAxis = chart.yAxis // 时间字段 - const xAxisDataeaseName = xAxis[0].dataeaseName + const xAxisgisbiName = xAxis[0].gisbiName // 收盘价字段 - const yAxisDataeaseName = yAxis[1].dataeaseName + const yAxisgisbiName = yAxis[1].gisbiName const result = [] for (let i = 0; i < data.length; i++) { if (i < dayCount) { result.push({ - [xAxisDataeaseName]: data[i][xAxisDataeaseName], + [xAxisgisbiName]: data[i][xAxisgisbiName], value: null }) } else { const sum = data .slice(i - dayCount + 1, i + 1) - .reduce((sum, item) => sum + item[yAxisDataeaseName], 0) + .reduce((sum, item) => sum + item[yAxisgisbiName], 0) result.push({ - [xAxisDataeaseName]: data[i][xAxisDataeaseName], + [xAxisgisbiName]: data[i][xAxisgisbiName], value: parseFloat((sum / dayCount).toFixed(3)) }) } @@ -228,7 +228,7 @@ export class StockLine extends G2PlotChartView { const data = parseJson(chart.data?.tableRow) // 时间字段 - const xAxisDataeaseName = xAxis[0].dataeaseName + const xAxisgisbiName = xAxis[0].gisbiName const averages = [5, 10, 20, 60, 120, 180] const legendItems: any[] = [ { @@ -262,9 +262,9 @@ export class StockLine extends G2PlotChartView { // 将均线数据设置到主数据中 data.forEach((item: any) => { - const date = item[xAxisDataeaseName] + const date = item[xAxisgisbiName] for (const [key, value] of averagesLineData) { - item[key] = value.find(m => m[xAxisDataeaseName] === date)?.value + item[key] = value.find(m => m[xAxisgisbiName] === date)?.value } }) @@ -283,7 +283,7 @@ export class StockLine extends G2PlotChartView { top: true, options: { smooth: false, - xField: xAxisDataeaseName, + xField: xAxisgisbiName, yField: key, color: colors[index - 1], xAxis: null, @@ -349,7 +349,7 @@ export class StockLine extends G2PlotChartView { options: { meta: { - [xAxisDataeaseName]: { + [xAxisgisbiName]: { mask: dateFormat } }, @@ -363,12 +363,12 @@ export class StockLine extends G2PlotChartView { min: minValue, max: maxValue }, - xField: xAxisDataeaseName, + xField: xAxisgisbiName, yField: [ - yAxis[0].dataeaseName, - yAxis[1].dataeaseName, - yAxis[2].dataeaseName, - yAxis[3].dataeaseName + yAxis[0].gisbiName, + yAxis[1].gisbiName, + yAxis[2].gisbiName, + yAxis[3].gisbiName ], legend: { position: 'top', @@ -384,7 +384,7 @@ export class StockLine extends G2PlotChartView { const plot = new MixClass(container, option) this.registerEvent(data, plot, averagesLineData) plot.on('schema:click', evt => { - const selectSchema = evt.data.data[xAxisDataeaseName] + const selectSchema = evt.data.data[xAxisgisbiName] const paramData = parseJson(chart.data?.data) const selectData = paramData.filter(item => item.field === selectSchema) const quotaList = [] @@ -440,7 +440,6 @@ export class StockLine extends G2PlotChartView { protected configTooltip(chart: Chart, options: MixOptions): MixOptions { const tooltipAttr = parseJson(chart.customAttr).tooltip - const xAxis = chart.xAxis const newPlots = [] const linePlotList = options.plots.filter(item => item.type === 'line') linePlotList.forEach(item => { @@ -464,7 +463,7 @@ export class StockLine extends G2PlotChartView { const showFiled = chart.data.fields const customTooltipItems = originalItems => { const formattedItems = originalItems.map(item => { - const fieldObj = showFiled.find(q => q.dataeaseName === item.name) + const fieldObj = showFiled.find(q => q.gisbiName === item.name) const displayName = fieldObj?.chartShowName || fieldObj?.name || item.name const formattedName = displayName.startsWith('ma') ? displayName.toUpperCase() : displayName tooltipAttr.tooltipFormatter.decimalCount = 3 diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/liquid/liquid.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/liquid/liquid.ts index 9ca812c..f53ed62 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/liquid/liquid.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/liquid/liquid.ts @@ -74,7 +74,7 @@ export class Liquid extends G2PlotChartView { }) // 处理空数据, 只要有一个指标是空数据,就不显示图表 const hasNoneData = chart.data?.series.some(s => !s.data?.[0]) - this.configEmptyDataStyle(newChart, hasNoneData ? [] : [1], container) + this.configEmptyDataStyle(hasNoneData ? [] : [1], container, newChart) if (hasNoneData) { return } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/bubble-map.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/bubble-map.ts index d9554e9..039df76 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/map/bubble-map.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/bubble-map.ts @@ -402,7 +402,8 @@ export class BubbleMap extends L7PlotChartView { content.push(name) } if (label.showQuota) { - areaMap[name] && content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) + ;(areaMap[name] || areaMap[name] === 0) && + content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) } item.properties['_DE_LABEL_'] = content.join('\n\n') } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/common.ts index 7589f4d..67d89fb 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/map/common.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/common.ts @@ -1,3 +1,6 @@ +import { useI18n } from '@/data-visualization/hooks/web/useI18n' + +const { t } = useI18n() export const MAP_EDITOR_PROPERTY: EditorProperty[] = [ 'background-overall-component', 'border-style', @@ -51,6 +54,29 @@ export const MAP_AXIS_TYPE: AxisType[] = [ 'extTooltip' ] +export const gaodeMapStyleOptions = [ + { name: t('chart.map_style_normal'), value: 'normal' }, + { name: t('chart.map_style_darkblue'), value: 'darkblue' }, + { name: t('chart.map_style_light'), value: 'light' }, + { name: t('chart.map_style_dark'), value: 'dark' }, + { name: t('chart.map_style_fresh'), value: 'fresh' }, + { name: t('chart.map_style_grey'), value: 'grey' }, + { name: t('chart.map_style_blue'), value: 'blue' }, + { name: t('chart.map_style_translate'), value: 'Satellite' }, + { name: t('commons.custom'), value: 'custom' } +] + +export const tdtMapStyleOptions = [ + { name: t('chart.map_style_normal'), value: 'normal' }, + { name: t('chart.map_style_dark'), value: 'black' }, + { name: t('chart.map_style_darkblue'), value: 'indigo' } +] + +export const qqMapStyleOptions = [ + { name: t('chart.map_style_normal'), value: 'normal' }, + { name: t('commons.custom'), value: 'custom' } +] + export declare type MapMouseEvent = MouseEvent & { feature: GeoJSON.Feature } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/map.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/map.ts index 51ceea3..cd18f05 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/map/map.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/map.ts @@ -157,6 +157,11 @@ export class Map extends L7PlotChartView { }) }) data = filterChartDataByRange(sourceData, maxValue, minValue) + if (chart.drill) { + getMaxAndMinValueByData(sourceData, 'value', 0, 0, (max, min) => { + data = filterChartDataByRange(sourceData, max, min) + }) + } } else { data = sourceData } @@ -301,7 +306,8 @@ export class Map extends L7PlotChartView { content.push(name) } if (label.showQuota) { - areaMap[name] && content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) + ;(areaMap[name] || areaMap[name] === 0) && + content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) } item.properties['_DE_LABEL_'] = content.join('\n\n') } @@ -346,11 +352,7 @@ export class Map extends L7PlotChartView { return listDom } - private customConfigLegend( - chart: Chart, - options: ChoroplethOptions, - context: Record - ): ChoroplethOptions { + private customConfigLegend(chart: Chart, options: ChoroplethOptions): ChoroplethOptions { const { basicStyle, misc } = parseJson(chart.customAttr) const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha)) if (basicStyle.suspension === false && basicStyle.showZoom === undefined) { @@ -420,14 +422,14 @@ export class Map extends L7PlotChartView { const isLessThanMin = range[0] < ranges[0][0] && range[1] < ranges[0][0] let rangeColor = colors[colorIndex] if (isLessThanMin) { - rangeColor = hexColorToRGBA(basicStyle.areaBaseColor, basicStyle.alpha) + rangeColor = basicStyle.areaBaseColor } items.push({ value: tmpRange, color: rangeColor }) }) - customLegend['customContent'] = (_: string, _items: CategoryLegendListItem[]) => { + customLegend['customContent'] = () => { if (items?.length) { return this.createLegendCustomContent(items) } @@ -435,13 +437,16 @@ export class Map extends L7PlotChartView { } options.color['value'] = ({ value }) => { const item = items.find(item => value >= item.value[0] && value <= item.value[1]) - return item ? item.color : hexColorToRGBA(basicStyle.areaBaseColor, basicStyle.alpha) + return item ? item.color : basicStyle.areaBaseColor } options.color.scale.domain = [ranges[0][0], ranges[ranges.length - 1][1]] } else { customLegend['customContent'] = (_: string, items: CategoryLegendListItem[]) => { const showItems = items?.length > 30 ? items.slice(0, 30) : items if (showItems?.length) { + if (showItems.length === 1) { + showItems[0].value = options.color.scale.domain.slice(0, 2) + } return this.createLegendCustomContent(showItems) } return '' @@ -508,7 +513,7 @@ export class Map extends L7PlotChartView { content.push(area.name) } if (label.showQuota) { - areaMap[area.name] && + ;(areaMap[area.name] || areaMap[area.name] === 0) && content.push(valueFormatter(areaMap[area.name].value, label.quotaLabelFormatter)) } labelLocation.push({ @@ -567,6 +572,9 @@ export class Map extends L7PlotChartView { return result } const head = originalItem.properties + if (!head) { + return result + } const { adcode } = head const areaName = subAreaMap['156' + adcode] const valItem = areaMap[areaName] diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/symbolic-map.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/symbolic-map.ts index 8e16a7a..217fb0c 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/map/symbolic-map.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/symbolic-map.ts @@ -13,13 +13,17 @@ import { svgStrToUrl } from '@/data-visualization/chart/components/js/util' import { deepCopy } from '@/data-visualization/utils/utils' -import { GaodeMap } from '@antv/l7-maps' import { Scene } from '@antv/l7-scene' import { PointLayer } from '@antv/l7-layers' import { LayerPopup, Popup } from '@antv/l7' -import { mapRendered, mapRendering } from '@/data-visualization/chart/components/js/panel/common/common_antv' +import { + getMapCenter, + getMapScene, + getMapStyle, + mapRendered, + qqMapRendered +} from '@/data-visualization/chart/components/js/panel/common/common_antv' import { configCarouselTooltip } from '@/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel' -import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart' import { filter } from 'lodash-es' const { t } = useI18n() @@ -102,18 +106,10 @@ export class SymbolicMap extends L7ChartView { miscStyle = parseJson(chart.customAttr).misc } - let mapStyle = basicStyle.mapStyleUrl - if (basicStyle.mapStyle !== 'custom') { - mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` - } const mapKey = await this.getMapKey() - let center: [number, number] = [ - DEFAULT_BASIC_STYLE.mapCenter.longitude, - DEFAULT_BASIC_STYLE.mapCenter.latitude - ] - if (basicStyle.autoFit === false) { - center = [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] - } + const mapStyle = getMapStyle(mapKey, basicStyle) + + let center = getMapCenter(basicStyle) // 联动时,聚焦到数据点,多个取第一个 if ( chart.chartExtRequest?.linkageFilters?.length && @@ -121,45 +117,25 @@ export class SymbolicMap extends L7ChartView { chart.data?.tableRow.length ) { // 经度 - const lng = chart.data?.tableRow?.[0][chart.xAxis[0].dataeaseName] + const lng = chart.data?.tableRow?.[0][chart.xAxis[0].gisbiName] // 纬度 - const lat = chart.data?.tableRow?.[0][chart.xAxis[1].dataeaseName] + const lat = chart.data?.tableRow?.[0][chart.xAxis[1].gisbiName] center = [lng, lat] } const chartObj = drawOption.chartObj as unknown as L7Wrapper let scene = chartObj?.getScene() - if (!scene) { - scene = new Scene({ - id: container, - logoVisible: false, - map: new GaodeMap({ - token: mapKey?.key ?? undefined, - style: mapStyle, - pitch: miscStyle.mapPitch, - center, - zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, - showLabel: !(basicStyle.showLabel === false), - WebGLParams: { - preserveDrawingBuffer: true - } - }) - }) - } else { - if (scene.getLayers()?.length) { - await scene.removeAllLayer() - scene.setPitch(miscStyle.mapPitch) - scene.setMapStyle(mapStyle) - scene.map.showLabel = !(basicStyle.showLabel === false) - } - if (basicStyle.autoFit === false) { - scene.setZoomAndCenter(basicStyle.zoomLevel, center) - } - } - mapRendering(container) - scene.once('loaded', () => { - mapRendered(container) - }) - this.configZoomButton(chart, scene) + scene = await getMapScene( + chart, + scene, + container, + mapKey, + basicStyle, + miscStyle, + mapStyle, + center + ) + + this.configZoomButton(chart, scene, mapKey) if (xAxis?.length < 2) { return new L7Wrapper(scene, undefined) } @@ -171,9 +147,13 @@ export class SymbolicMap extends L7ChartView { scene.addPopup(tooltipLayer) } this.buildLabel(chart, configList) + symbolicLayer.once('inited', () => { + mapRendered(container) + }) symbolicLayer.on('inited', () => { chart.container = container configCarouselTooltip(chart, symbolicLayer, symbolicLayer.sourceOption.data, scene) + qqMapRendered(scene) }) symbolicLayer.on('click', ev => { const data = ev.feature @@ -181,17 +161,17 @@ export class SymbolicMap extends L7ChartView { const quotaList = [] chart.data.fields.forEach((item, index) => { Object.keys(data).forEach(key => { - if (key.startsWith('f_') && item.dataeaseName === key) { + if (key.startsWith('f_') && item.gisbiName === key) { if (index === 0) { dimensionList.push({ id: item.id, - dataeaseName: item.dataeaseName, + gisbiName: item.gisbiName, value: data[key] }) } else { quotaList.push({ id: item.id, - dataeaseName: item.dataeaseName, + gisbiName: item.gisbiName, value: data[key] }) } @@ -239,7 +219,7 @@ export class SymbolicMap extends L7ChartView { let colorIndex = 0 // 存储已分配的颜色 const colorAssignments = new Map() - const sizeKey = extBubble.length > 0 ? extBubble[0].dataeaseName : '' + const sizeKey = extBubble.length > 0 ? extBubble[0].gisbiName : '' //条件颜色 const { threshold } = parseJson(chart.senior) @@ -257,7 +237,7 @@ export class SymbolicMap extends L7ChartView { ? chart.data.tableRow.map((item, index) => { item['_index'] = '_index' + index // 颜色标识 - const identifier = item[xAxisExt[0]?.dataeaseName] + const identifier = item[xAxisExt[0]?.gisbiName] // 检查该标识是否已有颜色分配,如果没有则分配 let color = colorAssignments.get(identifier) if (!color) { @@ -271,7 +251,7 @@ export class SymbolicMap extends L7ChartView { if (conditions.length > 0) { for (let i = 0; i < conditions.length; i++) { const c = conditions[i] - const value = item[c.field.dataeaseName] + const value = item[c.field.gisbiName] for (const t of c.conditions) { const v = t.value @@ -332,12 +312,12 @@ export class SymbolicMap extends L7ChartView { .source(data, { parser: { type: 'json', - x: xAxis[0].dataeaseName, - y: xAxis[1].dataeaseName + x: xAxis[0].gisbiName, + y: xAxis[1].gisbiName } }) .active(true) - if (xAxisExt[0]?.dataeaseName) { + if (xAxisExt[0]?.gisbiName) { if (basicStyle.mapSymbol === 'custom' && basicStyle.customIcon) { // 图片无法改色 if (basicStyle.customIcon.startsWith('data')) { @@ -452,8 +432,8 @@ export class SymbolicMap extends L7ChartView { let showFields = tooltip.showFields || [] if (!tooltip.showFields || tooltip.showFields.length === 0) { showFields = [ - ...chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`), - ...chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`) + ...chart.xAxisExt.map(i => `${i.gisbiName}@${i.name}`), + ...chart.xAxis.map(i => `${i.gisbiName}@${i.name}`) ] } // 修改背景色 @@ -584,8 +564,8 @@ export class SymbolicMap extends L7ChartView { let showFields = label.showFields || [] if (!label.showFields || label.showFields.length === 0) { showFields = [ - ...chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`), - ...chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`) + ...chart.xAxisExt.map(i => `${i.gisbiName}@${i.name}`), + ...chart.xAxis.map(i => `${i.gisbiName}@${i.name}`) ] } data.forEach(item => { @@ -613,8 +593,8 @@ export class SymbolicMap extends L7ChartView { .source(data, { parser: { type: 'json', - x: xAxis[0].dataeaseName, - y: xAxis[1].dataeaseName + x: xAxis[0].gisbiName, + y: xAxis[1].gisbiName } }) .shape('textLayerContent', 'text') diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel.ts index 31616a8..d04c874 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel.ts @@ -469,8 +469,8 @@ export class CarouselManager { }) } if (this.chart.type === 'symbolic-map') { - const lngField = this.chart.xAxis[0].dataeaseName - const latField = this.chart.xAxis[1].dataeaseName + const lngField = this.chart.xAxis[0].gisbiName + const latField = this.chart.xAxis[1].gisbiName const { _id } = this.scene ?.getLayers() ?.find(i => i.type === 'PointLayer') @@ -554,8 +554,8 @@ export class CarouselManager { let showFields = tooltip.showFields || [] if (!tooltip.showFields || tooltip.showFields.length === 0) { showFields = [ - ...this.chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`), - ...this.chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`) + ...this.chart.xAxisExt.map(i => `${i.gisbiName}@${i.name}`), + ...this.chart.xAxis.map(i => `${i.gisbiName}@${i.name}`) ] } const style = document.createElement('style') @@ -571,8 +571,8 @@ export class CarouselManager { } ` document.head.appendChild(style) - const lngField = this.chart.xAxis[0].dataeaseName - const latField = this.chart.xAxis[1].dataeaseName + const lngField = this.chart.xAxis[0].gisbiName + const latField = this.chart.xAxis[1].gisbiName const htmlPrefix = `
` const htmlSuffix = '
' const data = this.view.sourceOption.data[index] diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix.ts index 0d2627a..c012fb7 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix.ts @@ -1,9 +1,11 @@ import { G2PlotChartView, G2PlotDrawOptions -} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot' +} from +'@/data-visualization/chart/components/js/panel/types/impl/g2plot' import type { DualAxes, DualAxesOptions } from '@antv/g2plot/esm/plots/dual-axes' import { + configRoundAngle, configPlotTooltipEvent, getAnalyse, getLabel, @@ -25,7 +27,9 @@ import { defaultsDeep, defaults } from 'lodash-es' -import { valueFormatter } from '@/data-visualization/chart/components/js/formatter' +import { valueFormatter } from +'@/data-visualization/chart/components/js/formatter' + import { CHART_MIX_AXIS_TYPE, CHART_MIX_DEFAULT_BASIC_STYLE, @@ -42,6 +46,7 @@ import { } from '@/data-visualization/chart/components/editor/util/chart' import type { Options } from '@antv/g2plot/esm' import { Group } from '@antv/g-canvas' +import { extremumEvt } from '@/data-visualization/chart/components/js/extremumUitl' const { t } = useI18n() const DEFAULT_DATA = [] @@ -56,7 +61,8 @@ export class ColumnLineMix extends G2PlotChartView { 'label-selector': ['vPosition', 'seriesLabelFormatter'], 'tooltip-selector': [ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axis: AxisType[] = [...CHART_MIX_AXIS_TYPE, 'xAxisExtRight', 'yAxisExt'] @@ -94,6 +100,7 @@ export class ColumnLineMix extends G2PlotChartView { async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, action, container } = drawOptions + chart.container = container if (!chart.data?.left?.data?.length && !chart.data?.right?.data?.length) { return } @@ -117,7 +124,6 @@ export class ColumnLineMix extends G2PlotChartView { valueExt: d.value } }) - // options const initOptions: DualAxesOptions = { data: [data1, data2], @@ -127,6 +133,7 @@ export class ColumnLineMix extends G2PlotChartView { geometryOptions: [ { geometry: data1Type, + marginRatio: 0, color: [], isGroup: isGroup, isStack: isStack, @@ -174,6 +181,7 @@ export class ColumnLineMix extends G2PlotChartView { newChart.on('point:click', action) newChart.on('interval:click', action) + extremumEvt(newChart, chart, options, container) configPlotTooltipEvent(chart, newChart) return newChart } @@ -292,18 +300,9 @@ export class ColumnLineMix extends G2PlotChartView { tempOption.geometryOptions[1].smooth = smooth tempOption.geometryOptions[1].point = point tempOption.geometryOptions[1].lineStyle = lineStyle - - if (s.radiusColumnBar === 'roundAngle') { - const columnStyle = { - radius: [ - s.columnBarRightAngleRadius, - s.columnBarRightAngleRadius, - s.columnBarRightAngleRadius, - s.columnBarRightAngleRadius - ] - } - tempOption.geometryOptions[0].columnStyle = columnStyle - tempOption.geometryOptions[1].columnStyle = columnStyle + tempOption.geometryOptions[0] = { + ...tempOption.geometryOptions[0], + ...configRoundAngle(chart, 'columnStyle') } } @@ -328,7 +327,7 @@ export class ColumnLineMix extends G2PlotChartView { } setupDefaultOptions(chart: ChartObj): ChartObj { - const { customAttr, senior } = chart + const { senior } = chart if ( senior.functionCfg.emptyDataStrategy == undefined || senior.functionCfg.emptyDataStrategy === 'ignoreData' @@ -670,7 +669,8 @@ export class GroupColumnLineMix extends ColumnLineMix { 'label-selector': ['vPosition', 'seriesLabelFormatter'], 'tooltip-selector': [ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axisConfig = { @@ -782,7 +782,8 @@ export class StackColumnLineMix extends ColumnLineMix { 'label-selector': ['vPosition', 'seriesLabelFormatter'], 'tooltip-selector': [ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axisConfig = { @@ -895,7 +896,8 @@ export class DualLineMix extends ColumnLineMix { 'label-selector': ['seriesLabelFormatter'], 'tooltip-selector': [ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axisConfig = { diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/circle-packing.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/circle-packing.ts index ab6bbad..fd0a1f6 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/others/circle-packing.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/circle-packing.ts @@ -69,7 +69,7 @@ export class CirclePacking extends G2PlotChartView @@ -123,7 +123,7 @@ export class CirclePacking extends G2PlotChartView { + newChart.on('element:click', param => { const pointData = param?.data?.data if (pointData?.name === t('commons.all')) { return @@ -157,7 +157,7 @@ export class CirclePacking extends G2PlotChartView { + formatter: (d: Datum) => { return d.children.length === 0 ? d.name : '' } } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/gauge.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/gauge.ts index 1a98729..b338298 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/others/gauge.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/gauge.ts @@ -74,7 +74,7 @@ export class Gauge extends G2PlotChartView { // options const initOptions: GaugeOptions = { percent: 0, - appendPadding: getPadding(chart), + appendPadding: [0, 10, 15, 10], axis: { tickInterval: 0.2, label: { @@ -109,8 +109,10 @@ export class Gauge extends G2PlotChartView { } }) }) - const hasNoneData = chart.data?.series.some(s => !s.data?.[0]) - this.configEmptyDataStyle(newChart, hasNoneData ? [] : [1], container) + const hasNoneData = chart.data?.series.some( + s => s.data?.[0] === undefined || s.data?.[0] === null + ) + this.configEmptyDataStyle(hasNoneData ? [] : [1], container, newChart) if (hasNoneData) { return } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/indicator.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/indicator.ts index c28611c..9befd68 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/others/indicator.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/indicator.ts @@ -15,7 +15,8 @@ export class IndicatorChartView extends AbstractChartView { 'indicator-value-selector', 'indicator-name-selector', 'threshold', - 'function-cfg' + 'function-cfg', + 'linkage' ] propertyInner: EditorPropertyInner = { 'background-overall-component': ['all'], diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/quadrant.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/quadrant.ts index d9d4e34..d9d1ec4 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/others/quadrant.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/quadrant.ts @@ -13,7 +13,8 @@ import { configPlotTooltipEvent, configYaxisTitleLengthLimit, getTooltipContainer, - TOOLTIP_TPL + TOOLTIP_TPL, + getPadding } from '../../common/common_antv' import { DEFAULT_LEGEND_STYLE } from '@/data-visualization/chart/components/editor/util/chart' @@ -209,7 +210,7 @@ export class Quadrant extends G2PlotChartView { data: data, xField: 'yAxis', yField: 'yAxisExt', - appendPadding: 30, + appendPadding: getPadding(chart), pointStyle: { fillOpacity: 0.8, stroke: '#bbb' @@ -476,7 +477,6 @@ export class Quadrant extends G2PlotChartView { this.configLegend, this.configXAxis, this.configYAxis, - this.configAnalyse, this.configSlider, this.configBasicStyle )(chart, options, {}, this) diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey.ts index 783efa6..1d8640b 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey.ts @@ -1,13 +1,14 @@ import { G2PlotChartView, G2PlotDrawOptions -} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot' +} from +'@/data-visualization/chart/components/js/panel/types/impl/g2plot' import type { Sankey, SankeyOptions } from '@antv/g2plot/esm/plots/sankey' import { getPadding, setGradientColor } from '@/data-visualization/chart/components/js/panel/common/common_antv' import { cloneDeep, get } from 'lodash-es' import { flow, hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util' -import { valueFormatter } from '@/data-visualization/chart/components/js/formatter' - +import { valueFormatter } from +'@/data-visualization/chart/components/js/formatter' import { Datum } from '@antv/g2plot/esm/types/common' import { useI18n } from '@/data-visualization/hooks/web/useI18n' import { diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/scatter.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/scatter.ts index a06684e..fafee87 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/others/scatter.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/scatter.ts @@ -14,6 +14,8 @@ import { import { useI18n } from '@/data-visualization/hooks/web/useI18n' import { defaults, isEmpty } from 'lodash-es' import { DEFAULT_LEGEND_STYLE } from '@/data-visualization/chart/components/editor/util/chart' +import { type Datum } from '@antv/g2plot/esm' +import { Group } from '@antv/g-canvas' const { t } = useI18n() /** @@ -144,6 +146,17 @@ export class Scatter extends G2PlotChartView { const { Scatter: G2Scatter } = await import('@antv/g2plot/esm/plots/scatter') const newChart = new G2Scatter(container, options) newChart.on('point:click', action) + if (options.label) { + newChart.on('label:click', e => { + action({ + x: e.x, + y: e.y, + data: { + data: e.target.attrs.data + } + }) + }) + } configPlotTooltipEvent(chart, newChart) return newChart } @@ -277,6 +290,41 @@ export class Scatter extends G2PlotChartView { return optionTmp } + protected configLabel(chart: Chart, options: ScatterOptions): ScatterOptions { + const tmpOption = super.configLabel(chart, options) + if (!tmpOption.label) { + return options + } + const { label: labelAttr } = parseJson(chart.customAttr) + tmpOption.label.style.fill = labelAttr.color + const label = { + ...tmpOption.label, + formatter: function (data: Datum) { + const value = valueFormatter(data.value, labelAttr.labelFormatter) + const group = new Group({}) + group.addShape({ + type: 'text', + attrs: { + x: 0, + y: 0, + data, + text: value, + textAlign: 'start', + textBaseline: 'top', + fontSize: labelAttr.fontSize, + fontFamily: chart.fontFamily, + fill: labelAttr.color + } + }) + return group + } + } + return { + ...tmpOption, + label + } + } + protected setupOptions(chart: Chart, options: ScatterOptions) { return flow( this.configTheme, @@ -286,7 +334,6 @@ export class Scatter extends G2PlotChartView { this.configLegend, this.configXAxis, this.configYAxis, - this.configAnalyse, this.configSlider, this.configBasicStyle )(chart, options) diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/word-cloud.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/word-cloud.ts index 0baf181..c447985 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/others/word-cloud.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/word-cloud.ts @@ -1,7 +1,8 @@ import { G2PlotChartView, G2PlotDrawOptions -} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot' +} from +'@/data-visualization/chart/components/js/panel/types/impl/g2plot' import type { WordCloud as G2WordCloud, WordCloudOptions } from '@antv/g2plot/esm/plots/word-cloud' import { filterChartDataByRange, diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/pie/pie.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/pie.ts index 0263080..b689753 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/pie/pie.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/pie.ts @@ -27,19 +27,23 @@ import type { Datum } from '@antv/g2plot/esm/types/common' import { add } from 'mathjs' import isEmpty from 'lodash-es/isEmpty' import { cloneDeep } from 'lodash-es' - +import { useI18n } from '@/data-visualization/hooks/web/useI18n' +const { t } = useI18n() const DEFAULT_DATA = [] export class Pie extends G2PlotChartView { axis: AxisType[] = PIE_AXIS_TYPE properties = PIE_EDITOR_PROPERTY propertyInner: EditorPropertyInner = { ...PIE_EDITOR_PROPERTY_INNER, - 'basic-style-selector': ['colors', 'alpha', 'radius', 'topN', 'seriesColor'] + 'basic-style-selector': ['colors', 'alpha', 'radius', 'topN', 'seriesColor'], + 'tooltip-selector': [...PIE_EDITOR_PROPERTY_INNER['tooltip-selector'], 'carousel'] } axisConfig = PIE_AXIS_CONFIG async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, container, action } = drawOptions + this.configEmptyDataStyle(chart.data?.data, container, null, t('chart.no_data_or_not_positive')) + chart.container = container if (!chart.data?.data?.length) { return } @@ -115,12 +119,22 @@ export class Pie extends G2PlotChartView { field: { type: 'cat' } + }, + state: { + active: { + style: { + lineWidth: 2, + fillOpacity: 0.5 + } + } } } const options = this.setupOptions(chart, initOptions) const { Pie: G2Pie } = await import('@antv/g2plot/esm/plots/pie') const newChart = new G2Pie(container, options) - newChart.on('interval:click', action) + newChart.on('interval:click', d => { + d.data?.data?.field !== customAttr.basicStyle.topNLabel && action(d) + }) configPlotTooltipEvent(chart, newChart) return newChart } @@ -244,6 +258,7 @@ export class Pie extends G2PlotChartView { }, container: getTooltipContainer(`tooltip-${chart.id}`), itemTpl: TOOLTIP_TPL, + shared: true, enterable: true } return { @@ -338,7 +353,8 @@ export class Pie extends G2PlotChartView { export class PieDonut extends Pie { propertyInner: EditorPropertyInner = { ...PIE_EDITOR_PROPERTY_INNER, - 'basic-style-selector': ['colors', 'alpha', 'radius', 'innerRadius', 'topN', 'seriesColor'] + 'basic-style-selector': ['colors', 'alpha', 'radius', 'innerRadius', 'topN', 'seriesColor'], + 'tooltip-selector': [...PIE_EDITOR_PROPERTY_INNER['tooltip-selector'], 'carousel'] } protected configBasicStyle(chart: Chart, options: PieOptions): PieOptions { const tmp = super.configBasicStyle(chart, options) diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/pie/rose.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/rose.ts index 0432ca7..d9aa4b6 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/pie/rose.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/rose.ts @@ -40,6 +40,7 @@ export class Rose extends G2PlotChartView { async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, container, action } = drawOptions + this.configEmptyDataStyle(chart.data?.data, container, null, t('chart.no_data_or_not_positive')) if (!chart?.data?.data?.length) { return } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/common.ts index 08b146e..eb1ecf1 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/table/common.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/common.ts @@ -6,6 +6,7 @@ export const TABLE_EDITOR_PROPERTY: EditorProperty[] = [ 'table-cell-selector', 'title-selector', 'tooltip-selector', + 'summary-selector', 'function-cfg', 'threshold', 'scroll-cfg', diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/t-heatmap.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/t-heatmap.ts index 8750439..bda12f7 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/table/t-heatmap.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/t-heatmap.ts @@ -119,11 +119,12 @@ export class TableHeatmap extends G2PlotChartView { if (!xAxis?.length || !xAxisExt?.length || !extColor?.length) { return } - const xField = xAxis[0].dataeaseName - const xFieldExt = xAxisExt[0].dataeaseName - const extColorField = extColor[0].dataeaseName + const xField = xAxis[0].gisbiName + const xFieldExt = xAxisExt[0].gisbiName + const extColorField = extColor[0].gisbiName // data - const data = cloneDeep(chart.data.tableRow) + const tmpData = cloneDeep(chart.data.tableRow) + const data = tmpData.filter(cell => cell[xField] && cell[xFieldExt] && cell[extColorField]) data.forEach(i => { Object.keys(i).forEach(key => { if (key === '*') { @@ -171,10 +172,10 @@ export class TableHeatmap extends G2PlotChartView { const dimensionList = [] chart.data.fields.forEach(item => { Object.keys(pointData).forEach(key => { - if (key.startsWith('f_') && item.dataeaseName === key) { + if (key.startsWith('f_') && item.gisbiName === key) { dimensionList.push({ id: item.id, - dataeaseName: item.dataeaseName, + gisbiName: item.gisbiName, value: pointData[key] }) } @@ -197,7 +198,7 @@ export class TableHeatmap extends G2PlotChartView { newChart.on('afterrender', ev => { const l = JSON.parse(JSON.stringify(parseJson(chart.customStyle).legend)) if (l.show) { - const rail = ev.view.getController('legend').option[extColor[0].dataeaseName]?.['rail'] + const rail = ev.view.getController('legend').option[extColor[0].gisbiName]?.['rail'] if (rail) { rail.defaultLength = this.getDefaultLength(chart, l) } @@ -207,6 +208,12 @@ export class TableHeatmap extends G2PlotChartView { return newChart } + protected configTheme(chart: Chart, options: HeatmapOptions): HeatmapOptions { + const tmp = super.configTheme(chart, options) + tmp.theme.innerLabels.offset = 0 + return tmp + } + protected configBasicStyle(chart: Chart, options: HeatmapOptions): HeatmapOptions { const basicStyle = parseJson(chart.customAttr).basicStyle const color = basicStyle.colors?.map(ele => { @@ -235,7 +242,7 @@ export class TableHeatmap extends G2PlotChartView { const items = [] const createItem = (fieldObj, items, originalItems) => { const name = fieldObj?.chartShowName ? fieldObj?.chartShowName : fieldObj?.name - let value = originalItems[0].data[fieldObj.dataeaseName] + let value = originalItems[0].data[fieldObj.gisbiName] if (!isNaN(Number(value))) { value = valueFormatter(value, fieldObj?.formatterCfg) } @@ -329,7 +336,7 @@ export class TableHeatmap extends G2PlotChartView { position: 'middle', layout, formatter: data => { - const value = data[extColor[0]?.dataeaseName] + const value = data[extColor[0]?.gisbiName] if (!isNaN(Number(value))) { return valueFormatter(value, extColor[0]?.formatterCfg) } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-info.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-info.ts index 0485bcd..44a49b9 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-info.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-info.ts @@ -14,7 +14,7 @@ import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util' import { S2ChartView, S2DrawOptions } from '../../types/impl/s2' import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common' import { useI18n } from '@/data-visualization/hooks/web/useI18n' -import { isEqual, isNumber, merge } from 'lodash-es' +import { filter, isEqual, isNumber, merge } from 'lodash-es' import { copyContent, CustomDataCell, @@ -22,37 +22,19 @@ import { getRowIndex, calculateHeaderHeight, SortTooltip, - configSummaryRow, - summaryRowStyle, configEmptyDataStyle, getLeafNodes, - getColumns + getColumns, + drawImage, + getSummaryRow, + SummaryCell } from '@/data-visualization/chart/components/js/panel/common/common_table' const { t } = useI18n() + class ImageCell extends CustomDataCell { protected drawTextShape(): void { - const img = new Image() - const { x, y, width, height, fieldValue } = this.meta - img.src = fieldValue as string - img.setAttribute('crossOrigin', 'anonymous') - img.onload = () => { - !this.cfg.children && (this.cfg.children = []) - const { width: imgWidth, height: imgHeight } = img - const ratio = Math.max(imgWidth / width, imgHeight / height) - // 不铺满,部分留白 - const imgShowWidth = (imgWidth / ratio) * 0.8 - const imgShowHeight = (imgHeight / ratio) * 0.8 - this.textShape = this.addShape('image', { - attrs: { - x: x + (imgShowWidth < width ? (width - imgShowWidth) / 2 : 0), - y: y + (imgShowHeight < height ? (height - imgShowHeight) / 2 : 0), - width: imgShowWidth, - height: imgShowHeight, - img - } - }) - } + drawImage.apply(this) } } /** @@ -75,9 +57,7 @@ export class TableInfo extends S2ChartView { 'alpha', 'tablePageMode', 'showHoverStyle', - 'autoWrap', - 'showSummary', - 'summaryLabel' + 'autoWrap' ], 'table-cell-selector': [ ...TABLE_EDITOR_PROPERTY_INNER['table-cell-selector'], @@ -85,7 +65,8 @@ export class TableInfo extends S2ChartView { 'tableColumnFreezeHead', 'tableRowFreezeHead', 'mergeCells' - ] + ], + 'summary-selector': ['showSummary', 'summaryLabel'] } axis: AxisType[] = ['xAxis', 'filter', 'drill'] axisConfig: AxisConfig = { @@ -103,7 +84,7 @@ export class TableInfo extends S2ChartView { const columns = [] const meta = [] const axisMap = chart.xAxis.reduce((pre, cur) => { - pre[cur.dataeaseName] = cur + pre[cur.gisbiName] = cur return pre }, {}) const drillFieldMap = {} @@ -121,17 +102,17 @@ export class TableInfo extends S2ChartView { fields = fields.filter(ele => { return !filterFields.includes(ele.id) }) - drillFieldMap[curDrillField.dataeaseName] = chart.drillFields[0].dataeaseName + drillFieldMap[curDrillField.gisbiName] = chart.drillFields[0].gisbiName fields.splice(drillFieldIndex, 0, curDrillField) } fields.forEach(ele => { - const f = axisMap[ele.dataeaseName] + const f = axisMap[ele.gisbiName] if (f?.hide === true) { return } - columns.push(ele.dataeaseName) + columns.push(ele.gisbiName) meta.push({ - field: ele.dataeaseName, + field: ele.gisbiName, name: ele.chartShowName ?? ele.name, formatter: function (value) { if (!f) { @@ -140,7 +121,7 @@ export class TableInfo extends S2ChartView { if (value === null || value === undefined) { return value } - if (![2, 3].includes(f.deType) || !isNumber(value)) { + if (![2, 3, 4].includes(f.deType) || !isNumber(value)) { return value } let formatCfg = f.formatterCfg @@ -204,7 +185,7 @@ export class TableInfo extends S2ChartView { // 自适应列宽模式下,URL 字段的宽度固定为 120 if (basicStyle.tableColumnMode === 'adapt') { const urlFields = fields.filter( - field => field.deType === 7 && !axisMap[field.dataeaseName]?.hide + field => field.deType === 7 && !axisMap[field.gisbiName]?.hide ) s2Options.style.colCfg.widthByFieldValue = urlFields?.reduce((p, n) => { p[n.chartShowName ?? n.name] = 120 @@ -215,37 +196,6 @@ export class TableInfo extends S2ChartView { s2Options.frozenColCount = tableCell.tableColumnFreezeHead ?? 0 s2Options.frozenRowCount = tableCell.tableRowFreezeHead ?? 0 } - // 开启序号之后,第一列就是序号列,修改 label 即可 - if (s2Options.showSeriesNumber) { - let indexLabel = tableHeader.indexLabel - if (!indexLabel) { - indexLabel = '' - } - s2Options.layoutCoordinate = (_, __, col) => { - if (col.colIndex === 0 && col.rowIndex === 0) { - col.label = indexLabel - col.value = indexLabel - } - } - } - s2Options.dataCell = viewMeta => { - const field = fields.filter(f => f.dataeaseName === viewMeta.valueField)?.[0] - if (field?.deType === 7 && chart.showPosition !== 'dialog') { - return new ImageCell(viewMeta, viewMeta?.spreadsheet) - } - if (viewMeta.colIndex === 0 && s2Options.showSeriesNumber) { - if (tableCell.mergeCells) { - viewMeta.fieldValue = getRowIndex(s2Options.mergedCellsInfo, viewMeta) - } else { - viewMeta.fieldValue = - pageInfo.pageSize * (pageInfo.currentPage - 1) + viewMeta.rowIndex + 1 - } - } - // 配置文本自动换行参数 - viewMeta.autoWrap = tableCell.mergeCells ? false : basicStyle.autoWrap - viewMeta.maxLines = basicStyle.maxLines - return new CustomDataCell(viewMeta, viewMeta?.spreadsheet) - } // tooltip this.configTooltip(chart, s2Options) // 合并单元格 @@ -274,12 +224,12 @@ export class TableInfo extends S2ChartView { return new CustomTableColCell(node, sheet, config) } } - // 总计 - configSummaryRow(chart, s2Options, newData, tableHeader, basicStyle, basicStyle.showSummary) + // 序列号和总计行 + this.configSummaryRowAndIndex(chart, pageInfo, s2Options, s2DataConfig) // 开始渲染 const newChart = new TableSheet(containerDom, s2DataConfig, s2Options) // 总计紧贴在单元格后面 - summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary) + this.summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary) // 开启自动换行 if (basicStyle.autoWrap && !tableCell.mergeCells) { // 调整表头宽度时,计算表头高度 @@ -340,8 +290,8 @@ export class TableInfo extends S2ChartView { } // 第一次渲染初始化,把图片字段固定为 120 进行计算 const urlFields = fields - .filter(field => field.deType === 7 && !axisMap[field.dataeaseName]?.hide) - .map(f => f.dataeaseName) + .filter(field => field.deType === 7 && !axisMap[field.gisbiName]?.hide) + .map(f => f.gisbiName) const totalWidthWithImg = ev.colLeafNodes.reduce((p, n) => { return p + (urlFields.includes(n.field) ? 120 : n.width) }, 0) @@ -386,7 +336,7 @@ export class TableInfo extends S2ChartView { const cell = newChart.getCell(ev.target) const meta = cell.getMeta() as ViewMeta const nameIdMap = fields.reduce((pre, next) => { - pre[next['dataeaseName']] = next['id'] + pre[next['gisbiName']] = next['id'] return pre }, {}) @@ -417,13 +367,13 @@ export class TableInfo extends S2ChartView { newChart.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(newChart, event, meta)) newChart.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(newChart, event, meta)) newChart.on(S2Event.MERGED_CELLS_HOVER, event => this.showTooltip(newChart, event, meta)) + // touch + this.configTouchEvent(newChart, drawOption, meta) } // header resize newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, ev => resizeAction(ev)) // right click newChart.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(newChart, event, meta)) - // touch - this.configTouchEvent(newChart, drawOption, meta) // theme const customTheme = this.configTheme(chart) newChart.setThemeCfg({ theme: customTheme }) @@ -444,6 +394,11 @@ export class TableInfo extends S2ChartView { const fontStyle = tableCell.isItalic ? 'italic' : 'normal' const fontWeight = tableCell.isBolder === false ? 'normal' : 'bold' const mergeCellTheme: S2Theme = { + dataCell: { + cell: { + crossBackgroundColor: tableItemBgColor + } + }, mergedCell: { cell: { backgroundColor: tableItemBgColor, @@ -488,6 +443,92 @@ export class TableInfo extends S2ChartView { return theme } + protected configSummaryRowAndIndex( + chart: Chart, + pageInfo: PageInfo, + s2Options: S2Options, + s2DataConfig: S2DataConfig + ) { + const { tableHeader, basicStyle, tableCell } = parseJson(chart.customAttr) + const fields = chart.data?.fields ?? [] + // 开启序号之后,第一列就是序号列,修改 label 即可 + if (s2Options.showSeriesNumber) { + let indexLabel = tableHeader.indexLabel + if (!indexLabel) { + indexLabel = '' + } + s2Options.layoutCoordinate = (_, __, col) => { + if (col.colIndex === 0 && col.rowIndex === 0) { + col.label = indexLabel + col.value = indexLabel + } + } + } + const { showSummary, summaryLabel } = basicStyle + const data = s2DataConfig.data + const xAxis = chart.xAxis + if (showSummary && data?.length) { + // 设置汇总行高度和表头一致 + const heightByField = {} + heightByField[data.length] = tableHeader.tableTitleHeight + s2Options.style.rowCfg = { heightByField } + // 计算汇总加入到数据里,冻结最后一行 + s2Options.frozenTrailingRowCount = 1 + const axis = filter(xAxis, axis => [2, 3, 4].includes(axis.deType)) + const summaryObj = getSummaryRow(data, axis, basicStyle.seriesSummary) as any + data.push(summaryObj) + } + s2Options.dataCell = viewMeta => { + // 总计行处理 + if (showSummary && viewMeta.rowIndex === data.length - 1) { + if (viewMeta.colIndex === 0) { + if (tableHeader.showIndex) { + viewMeta.fieldValue = summaryLabel ?? t('chart.total_show') + } else { + // 第一列不是数值类型的,显示总计 + if (![2, 3, 4].includes(xAxis?.[0]?.deType)) { + viewMeta.fieldValue = summaryLabel ?? t('chart.total_show') + } + } + } + return new SummaryCell(viewMeta, viewMeta?.spreadsheet) + } + const field = fields.find(f => f.gisbiName === viewMeta.valueField) + if (field?.deType === 7 && chart.showPosition !== 'dialog') { + return new ImageCell(viewMeta, viewMeta?.spreadsheet) + } + if (viewMeta.colIndex === 0 && s2Options.showSeriesNumber) { + if (tableCell.mergeCells) { + viewMeta.fieldValue = getRowIndex(s2Options.mergedCellsInfo, viewMeta) + } else { + viewMeta.fieldValue = + pageInfo.pageSize * (pageInfo.currentPage - 1) + viewMeta.rowIndex + 1 + } + } + // 配置文本自动换行参数 + viewMeta.autoWrap = tableCell.mergeCells ? false : basicStyle.autoWrap + viewMeta.maxLines = basicStyle.maxLines + return new CustomDataCell(viewMeta, viewMeta?.spreadsheet) + } + } + + protected summaryRowStyle(newChart: TableSheet, newData, tableCell, tableHeader, showSummary) { + if (!showSummary || !newData.length) return + const columns = newChart.dataCfg.fields.columns + const showHeader = tableHeader.showTableHeader === true + // 不显示表头时,减少一个表头的高度 + const headerAndSummaryHeight = showHeader ? getMaxTreeDepth(columns) + 1 : 1 + newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => { + const totalHeight = + tableHeader.tableTitleHeight * headerAndSummaryHeight + + tableCell.tableItemHeight * (newData.length - 1) + if (totalHeight < newChart.container.cfg.height) { + newChart.options.height = + totalHeight < newChart.container.cfg.height - 8 ? totalHeight + 8 : totalHeight + } + }) + } + constructor() { super('table-info', []) } @@ -508,3 +549,17 @@ function getStartPosition(node) { } return getStartPosition(node.children[0]) } + +function getMaxTreeDepth(nodes) { + if (!nodes?.length) { + return 0 + } + return Math.max( + ...nodes.map(node => { + if (!node.children?.length) { + return 1 + } + return getMaxTreeDepth(node.children) + 1 + }) + ) +} diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-normal.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-normal.ts index 327dd5b..ffb0b43 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-normal.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-normal.ts @@ -2,10 +2,11 @@ import { useI18n } from '@/data-visualization/hooks/web/useI18n' import { formatterItem, valueFormatter } from '@/data-visualization/chart/components/js/formatter' import { configEmptyDataStyle, - configSummaryRow, copyContent, + CustomDataCell, + getSummaryRow, SortTooltip, - summaryRowStyle + SummaryCell } from '@/data-visualization/chart/components/js/panel/common/common_table' import { S2ChartView, S2DrawOptions } from '@/data-visualization/chart/components/js/panel/types/impl/s2' import { parseJson } from '@/data-visualization/chart/components/js/util' @@ -19,7 +20,7 @@ import { TableSheet, ViewMeta } from '@antv/s2' -import { cloneDeep, isNumber } from 'lodash-es' +import { isNumber } from 'lodash-es' import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common' const { t } = useI18n() @@ -37,8 +38,7 @@ export class TableNormal extends S2ChartView { ], 'basic-style-selector': [ ...TABLE_EDITOR_PROPERTY_INNER['basic-style-selector'], - 'showSummary', - 'summaryLabel', + 'tablePageMode', 'showHoverStyle' ], 'table-cell-selector': [ @@ -46,7 +46,8 @@ export class TableNormal extends S2ChartView { 'tableFreeze', 'tableColumnFreezeHead', 'tableRowFreezeHead' - ] + ], + 'summary-selector': ['showSummary', 'summaryLabel'] } axis: AxisType[] = ['xAxis', 'yAxis', 'drill', 'filter'] axisConfig: AxisConfig = { @@ -66,7 +67,7 @@ export class TableNormal extends S2ChartView { } drawChart(drawOption: S2DrawOptions): TableSheet { - const { container, chart, action, resizeAction } = drawOption + const { container, chart, action, pageInfo, resizeAction } = drawOption const containerDom = document.getElementById(container) if (!containerDom) return @@ -92,18 +93,18 @@ export class TableNormal extends S2ChartView { fields.splice(drillFieldIndex, 0, ...curDrillField) } const axisMap = [...chart.xAxis, ...chart.yAxis].reduce((pre, cur) => { - pre[cur.dataeaseName] = cur + pre[cur.gisbiName] = cur return pre }, {}) // add drill list fields.forEach(ele => { - const f = axisMap[ele.dataeaseName] + const f = axisMap[ele.gisbiName] if (f?.hide === true) { return } - columns.push(ele.dataeaseName) + columns.push(ele.gisbiName) meta.push({ - field: ele.dataeaseName, + field: ele.gisbiName, name: ele.chartShowName ?? ele.name, formatter: function (value) { if (!f) { @@ -112,7 +113,7 @@ export class TableNormal extends S2ChartView { if (value === null || value === undefined) { return value } - if (![2, 3].includes(f.deType) || !isNumber(value)) { + if (![2, 3, 4].includes(f.deType) || !isNumber(value)) { return value } let formatCfg = f.formatterCfg @@ -160,19 +161,6 @@ export class TableNormal extends S2ChartView { s2Options.frozenColCount = tableCell.tableColumnFreezeHead ?? 0 s2Options.frozenRowCount = tableCell.tableRowFreezeHead ?? 0 } - // 开启序号之后,第一列就是序号列,修改 label 即可 - if (s2Options.showSeriesNumber) { - let indexLabel = tableHeader.indexLabel - if (!indexLabel) { - indexLabel = '' - } - s2Options.layoutCoordinate = (_, __, col) => { - if (col.colIndex === 0 && col.rowIndex === 0) { - col.label = indexLabel - col.value = indexLabel - } - } - } // tooltip this.configTooltip(chart, s2Options) // 隐藏表头,保留顶部的分割线, 禁用表头横向 resize @@ -193,13 +181,12 @@ export class TableNormal extends S2ChartView { chart.container = container this.configHeaderInteraction(chart, s2Options) } - - // 总计 - configSummaryRow(chart, s2Options, newData, tableHeader, basicStyle, basicStyle.showSummary) + // 配置总计和序号列 + this.configSummaryRowAndIndex(chart, pageInfo, s2Options, s2DataConfig) // 开始渲染 const newChart = new TableSheet(containerDom, s2DataConfig, s2Options) // 总计紧贴在单元格后面 - summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary) + this.summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary) // 自适应铺满 if (basicStyle.tableColumnMode === 'adapt') { newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, () => { @@ -253,7 +240,7 @@ export class TableNormal extends S2ChartView { const cell = newChart.getCell(ev.target) const meta = cell.getMeta() as ViewMeta const nameIdMap = fields.reduce((pre, next) => { - pre[next['dataeaseName']] = next['id'] + pre[next['gisbiName']] = next['id'] return pre }, {}) @@ -281,19 +268,86 @@ export class TableNormal extends S2ChartView { if (show) { newChart.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(newChart, event, meta)) newChart.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(newChart, event, meta)) + // touch + this.configTouchEvent(newChart, drawOption, meta) } // header resize newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, ev => resizeAction(ev)) // right click newChart.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(newChart, event, meta)) - // touch - this.configTouchEvent(newChart, drawOption, meta) // theme const customTheme = this.configTheme(chart) newChart.setThemeCfg({ theme: customTheme }) return newChart } + + protected configSummaryRowAndIndex( + chart: Chart, + pageInfo: PageInfo, + s2Options: S2Options, + s2DataConfig: S2DataConfig + ) { + const { tableHeader, basicStyle } = parseJson(chart.customAttr) + // 开启序号之后,第一列就是序号列,修改 label 即可 + if (s2Options.showSeriesNumber) { + let indexLabel = tableHeader.indexLabel + if (!indexLabel) { + indexLabel = '' + } + s2Options.layoutCoordinate = (_, __, col) => { + if (col.colIndex === 0 && col.rowIndex === 0) { + col.label = indexLabel + col.value = indexLabel + } + } + } + const { showSummary, summaryLabel } = basicStyle + const data = s2DataConfig.data + const { xAxis, yAxis } = chart + if (showSummary && data?.length) { + // 设置汇总行高度和表头一致 + const heightByField = {} + heightByField[data.length] = tableHeader.tableTitleHeight + s2Options.style.rowCfg = { heightByField } + // 计算汇总加入到数据里,冻结最后一行 + s2Options.frozenTrailingRowCount = 1 + const summaryObj = getSummaryRow(data, yAxis, basicStyle.seriesSummary) as any + data.push(summaryObj) + } + s2Options.dataCell = viewMeta => { + // 总计行处理 + if (showSummary && viewMeta.rowIndex === data.length - 1) { + if (viewMeta.colIndex === 0) { + if (tableHeader.showIndex || xAxis?.length) { + viewMeta.fieldValue = summaryLabel ?? t('chart.total_show') + } + } + return new SummaryCell(viewMeta, viewMeta?.spreadsheet) + } + if (viewMeta.colIndex === 0 && s2Options.showSeriesNumber) { + viewMeta.fieldValue = pageInfo.pageSize * (pageInfo.currentPage - 1) + viewMeta.rowIndex + 1 + } + return new CustomDataCell(viewMeta, viewMeta?.spreadsheet) + } + } + + protected summaryRowStyle(newChart, newData, tableCell, tableHeader, showSummary) { + if (!showSummary || !newData.length) return + newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => { + const showHeader = tableHeader.showTableHeader === true + // 不显示表头时,减少一个表头的高度 + const headerAndSummaryHeight = showHeader ? 2 : 1 + const totalHeight = + tableHeader.tableTitleHeight * headerAndSummaryHeight + + tableCell.tableItemHeight * (newData.length - 1) + if (totalHeight < newChart.container.cfg.height) { + newChart.options.height = + totalHeight < newChart.container.cfg.height - 8 ? totalHeight + 8 : totalHeight + } + }) + } + constructor() { super('table-normal', []) } diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-pivot.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-pivot.ts index f7f695c..bf6fe22 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-pivot.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-pivot.ts @@ -13,7 +13,8 @@ import { TotalStatus, Aggregation, S2DataConfig, - MergedCell + MergedCell, + LayoutResult } from '@antv/s2' import { formatterItem, valueFormatter } from '../../../formatter' import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util' @@ -91,7 +92,8 @@ export class TablePivot extends S2ChartView { 'showColTooltip', 'showRowTooltip', 'showHorizonBorder', - 'showVerticalBorder' + 'showVerticalBorder', + 'rowHeaderFreeze' ], 'table-total-selector': ['row', 'col'], 'basic-style-selector': [ @@ -100,7 +102,9 @@ export class TablePivot extends S2ChartView { 'tableScrollBarColor', 'alpha', 'tableLayoutMode', - 'showHoverStyle' + 'showHoverStyle', + 'quotaPosition', + 'quotaColLabel' ] } axis: AxisType[] = ['xAxis', 'xAxisExt', 'yAxis', 'filter'] @@ -126,7 +130,7 @@ export class TablePivot extends S2ChartView { const { xAxisExt: columnFields, xAxis: rowFields, yAxis: valueFields } = chart const [c, r, v] = [columnFields, rowFields, valueFields].map(arr => - arr.map(i => i.dataeaseName) + arr.map(i => i.gisbiName) ) // fields @@ -146,14 +150,14 @@ export class TablePivot extends S2ChartView { ...chart.xAxisExt, ...chart.yAxis ].reduce((p, n) => { - p[n.dataeaseName] = n + p[n.gisbiName] = n return p }, {}) fields.forEach(ele => { - const f = valueFieldMap[ele.dataeaseName] - columns.push(ele.dataeaseName) + const f = valueFieldMap[ele.gisbiName] + columns.push(ele.gisbiName) meta.push({ - field: ele.dataeaseName, + field: ele.gisbiName, name: ele.chartShowName ?? ele.name, formatter: value => { if (!f) { @@ -162,7 +166,7 @@ export class TablePivot extends S2ChartView { if (value === null || value === undefined) { return value } - if (![2, 3].includes(f.deType) || !isNumber(value)) { + if (![2, 3, 4].includes(f.deType) || !isNumber(value)) { return value } if (f.formatterCfg) { @@ -175,7 +179,7 @@ export class TablePivot extends S2ChartView { }) // total config - const { basicStyle, tooltip, tableTotal } = parseJson(chart.customAttr) + const { basicStyle, tooltip, tableTotal, tableHeader } = parseJson(chart.customAttr) if (!tableTotal.row.subTotalsDimensionsNew || tableTotal.row.subTotalsDimensions == undefined) { tableTotal.row.subTotalsDimensions = r } @@ -183,6 +187,7 @@ export class TablePivot extends S2ChartView { // 解析合计、小计排序 const sortParams = [] + let rowTotalSort = false if ( tableTotal.row.totalSort && tableTotal.row.totalSort !== 'none' && @@ -190,16 +195,20 @@ export class TablePivot extends S2ChartView { tableTotal.row.showGrandTotals && v.indexOf(tableTotal.row.totalSortField) > -1 ) { - const sort = { - sortFieldId: c[0], - sortMethod: tableTotal.row.totalSort.toUpperCase(), - sortByMeasure: TOTAL_VALUE, - query: { - [EXTRA_FIELD]: tableTotal.row.totalSortField + c.forEach(i => { + const sort = { + sortFieldId: i, + sortMethod: tableTotal.row.totalSort.toUpperCase(), + sortByMeasure: TOTAL_VALUE, + query: { + [EXTRA_FIELD]: tableTotal.row.totalSortField + } } - } - sortParams.push(sort) + sortParams.push(sort) + }) + rowTotalSort = true } + let colTotalSort = false if ( tableTotal.col.totalSort && tableTotal.col.totalSort !== 'none' && @@ -207,15 +216,18 @@ export class TablePivot extends S2ChartView { tableTotal.col.showGrandTotals && v.indexOf(tableTotal.col.totalSortField) > -1 ) { - const sort = { - sortFieldId: r[0], - sortMethod: tableTotal.col.totalSort.toUpperCase(), - sortByMeasure: TOTAL_VALUE, - query: { - [EXTRA_FIELD]: tableTotal.col.totalSortField + r.forEach(i => { + const sort = { + sortFieldId: i, + sortMethod: tableTotal.col.totalSort.toUpperCase(), + sortByMeasure: TOTAL_VALUE, + query: { + [EXTRA_FIELD]: tableTotal.col.totalSortField + } } - } - sortParams.push(sort) + sortParams.push(sort) + }) + colTotalSort = true } //列维度为空,行排序按照指标列来排序,取第一个有排序设置的指标 if (!columnFields?.length) { @@ -226,7 +238,7 @@ export class TablePivot extends S2ChartView { sortMethod: sortField.sort.toUpperCase(), sortByMeasure: TOTAL_VALUE, query: { - [EXTRA_FIELD]: sortField.dataeaseName + [EXTRA_FIELD]: sortField.gisbiName } } sortParams.push(sort) @@ -244,15 +256,29 @@ export class TablePivot extends S2ChartView { col: chart.xAxisExt, quota: chart.yAxis } - //树形模式下,列维度为空,行小计会变成列总计,特殊处理下 - if (basicStyle.tableLayoutMode === 'tree' && !chart.xAxisExt?.length) { - tableTotal.col.calcTotals = tableTotal.row.calcSubTotals + // 沒有列维度需要特殊处理 + if (!chart.xAxisExt?.length) { + //树形模式下,列维度为空,行小计的配置会变成列总计 + if (basicStyle.tableLayoutMode === 'tree') { + tableTotal.col.calcTotals = tableTotal.row.calcSubTotals + if (!tableTotal.col.calcTotals.cfg?.length) { + tableTotal.col.calcTotals.cfg = chart.yAxis.map(y => { + return { + gisbiName: y.gisbiName, + aggregation: 'SUM' + } + }) + } + } else { + // 列总计设置为空 + tableTotal.col.calcTotals.calcFunc = () => '-' + } } totals.forEach(total => { if (total.cfg?.length) { delete total.aggregation const totalCfgMap = total.cfg.reduce((p, n) => { - p[n.dataeaseName] = n + p[n.gisbiName] = n return p }, {}) total.calcFunc = (query, data, _, status) => { @@ -262,12 +288,93 @@ export class TablePivot extends S2ChartView { }) // 空值处理 const newData = this.configEmptyDataStrategy(chart) + // 行列维度排序 + if (!rowTotalSort) { + c?.forEach((f, i) => { + if (valueFieldMap[f]?.sort === 'none') { + return + } + const sort = { + sortFieldId: f + } + const sortMethod = valueFieldMap[f]?.sort?.toUpperCase() + if (sortMethod === 'CUSTOM_SORT') { + sort.sortBy = valueFieldMap[f].customSort + } else { + if (i === 0) { + sort.sortMethod = sortMethod + } else { + const fieldValues = newData.map(item => item[f]) + const uniqueValues = [...new Set(fieldValues)] + + // 根据配置动态决定排序顺序 + uniqueValues.sort((a, b) => { + if ([2, 3, 4].includes(valueFieldMap[f]?.deType)) { + return sortMethod === 'ASC' ? a - b : b - a + } + if (!a && !b) { + return 0 + } + if (!a) { + return sortMethod === 'ASC' ? -1 : 1 + } + if (!b) { + return sortMethod === 'ASC' ? 1 : -1 + } + return sortMethod === 'ASC' ? a.localeCompare(b) : b.localeCompare(a) + }) + sort.sortBy = uniqueValues + } + } + sortParams.push(sort) + }) + } + if (!colTotalSort) { + r?.forEach((f, i) => { + if (valueFieldMap[f]?.sort === 'none') { + return + } + const sort = { + sortFieldId: f + } + const sortMethod = valueFieldMap[f]?.sort?.toUpperCase() + if (sortMethod === 'CUSTOM_SORT') { + sort.sortBy = valueFieldMap[f].customSort + } else { + if (i === 0) { + sort.sortMethod = sortMethod + } else { + const fieldValues = newData.map(item => item[f]) + const uniqueValues = [...new Set(fieldValues)] + // 根据配置动态决定排序顺序 + uniqueValues.sort((a, b) => { + if ([2, 3, 4].includes(valueFieldMap[f]?.deType)) { + return sortMethod === 'ASC' ? a - b : b - a + } + if (!a && !b) { + return 0 + } + if (!a) { + return sortMethod === 'ASC' ? -1 : 1 + } + if (!b) { + return sortMethod === 'ASC' ? 1 : -1 + } + return sortMethod === 'ASC' ? a.localeCompare(b) : b.localeCompare(a) + }) + sort.sortBy = uniqueValues + } + } + sortParams.push(sort) + }) + } // data config const s2DataConfig: S2DataConfig = { fields: { rows: r, columns: c, - values: v + values: v, + valueInCols: !(basicStyle.quotaPosition === 'row') }, meta: meta, data: newData, @@ -277,6 +384,7 @@ export class TablePivot extends S2ChartView { width: containerDom.offsetWidth, height: containerDom.offsetHeight, totals: tableTotal as Totals, + cornerExtraFieldText: basicStyle.quotaColLabel ?? t('dataset.value'), conditions: this.configConditions(chart), tooltip: { getContainer: () => containerDom @@ -288,21 +396,204 @@ export class TablePivot extends S2ChartView { }, dataCell: meta => { return new CustomDataCell(meta, meta.spreadsheet) - } + }, + frozenRowHeader: !(tableHeader.rowHeaderFreeze === false) } // options s2Options.style = this.configStyle(chart, s2DataConfig) - s2Options.style.hierarchyCollapse = true + // 默认展开层级 + if (basicStyle.tableLayoutMode === 'tree') { + const { defaultExpandLevel } = basicStyle + if (isNumber(defaultExpandLevel)) { + if (defaultExpandLevel >= chart.xAxis.length) { + s2Options.style.rowExpandDepth = defaultExpandLevel + } else { + s2Options.style.rowExpandDepth = defaultExpandLevel - 2 + } + } + if (defaultExpandLevel === 'all') { + s2Options.style.rowExpandDepth = chart.xAxis.length + } + if (!defaultExpandLevel) { + s2Options.style.hierarchyCollapse = true + } + } + // 列汇总别名 + if (!(basicStyle.quotaPosition === 'row' && basicStyle.tableLayoutMode === 'tree')) { + if ( + basicStyle.quotaPosition !== 'row' && + chart.xAxisExt?.length && + chart.yAxis?.length > 1 && + tableTotal.col.showGrandTotals && + tableTotal.col.calcTotals?.cfg?.length + ) { + const colTotalCfgMap = tableTotal.col.calcTotals.cfg.reduce((p, n) => { + p[n.gisbiName] = n + return p + }, {}) + s2Options.layoutCoordinate = (_, __, col) => { + if (col?.isGrandTotals) { + if (colTotalCfgMap[col.value]?.label) { + col.label = colTotalCfgMap[col.value].label + } + } + } + } + if ( + basicStyle.quotaPosition === 'row' && + chart.xAxisExt?.length && + chart.yAxis?.length > 1 && + tableTotal.row.showGrandTotals && + tableTotal.row.calcTotals?.cfg?.length + ) { + const rowTotalCfgMap = tableTotal.row.calcTotals.cfg.reduce((p, n) => { + p[n.gisbiName] = n + return p + }, {}) + // eslint-disable-next-line + s2Options.layoutCoordinate = (_, row, __) => { + if (row?.isGrandTotals) { + if (rowTotalCfgMap[row.value]?.label) { + row.label = rowTotalCfgMap[row.value].label + } + } + } + } + } // tooltip this.configTooltip(chart, s2Options) // 开始渲染 const s2 = new PivotSheet(containerDom, s2DataConfig, s2Options as unknown as S2Options) + // 自适应铺满 + if (basicStyle.tableColumnMode === 'adapt') { + s2.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, () => { + s2.store.set('lastLayoutResult', s2.facet.layoutResult) + }) + // 平铺模式行头resize + s2.on(S2Event.LAYOUT_RESIZE_ROW_WIDTH, () => { + s2.store.set('lastLayoutResult', s2.facet.layoutResult) + }) + // 树形模式行头resize + s2.on(S2Event.LAYOUT_RESIZE_TREE_WIDTH, () => { + s2.store.set('lastLayoutResult', s2.facet.layoutResult) + }) + s2.on(S2Event.LAYOUT_AFTER_HEADER_LAYOUT, (ev: LayoutResult) => { + const lastLayoutResult = s2.store.get('lastLayoutResult') as LayoutResult + if (lastLayoutResult) { + // 拖动 col 表头 resize + const colWidthByFieldValue = s2.options.style?.colCfg?.widthByFieldValue + // 平铺模式拖动 row 表头 resize + const rowWidthByField = s2.options.style?.rowCfg?.widthByField + // 树形模式拖动 row 表头 resize + const treeRowWidth = + s2.options.style?.treeRowsWidth || lastLayoutResult.rowsHierarchy.width + const colWidthMap = + lastLayoutResult.colLeafNodes.reduce((p, n) => { + p[n.id] = colWidthByFieldValue?.[n.value] ?? n.width + return p + }, {}) || {} + const totalColWidth = ev.colLeafNodes.reduce((p, n) => { + n.width = colWidthMap[n.id] || n.width + n.x = p + return p + n.width + }, 0) + ev.colNodes.forEach(n => { + if (n.isLeaf) { + return + } + n.width = this.getColWidth(n) + n.x = this.getLeftChild(n).x + }) + if (basicStyle.tableLayoutMode === 'tree') { + ev.rowNodes.forEach(n => { + n.width = treeRowWidth + }) + ev.rowsHierarchy.width = treeRowWidth + ev.colsHierarchy.width = totalColWidth + } else { + const rowWidthMap = + lastLayoutResult.rowNodes.reduce((p, n) => { + p[n.id] = rowWidthByField?.[n.field] ?? n.width + return p + }, {}) || {} + ev.rowNodes.forEach(n => { + n.x = 0 + n.width = rowWidthMap[n.id] || n.width + let tmp = n + while (tmp.parent.id !== 'root') { + n.x += tmp.parent.width + tmp = tmp.parent + } + }) + const totlaRowWidth = ev.rowsHierarchy.sampleNodesForAllLevels.reduce((p, n) => { + return p + n.width + }, 0) + const maxRowLevel = ev.rowsHierarchy.maxLevel + ev.rowNodes.forEach(n => { + // 总计和中间层级的小计需要重新计算宽度 + if (n.isTotalRoot || (n.isSubTotals && n.level < maxRowLevel)) { + let width = 0 + for (let i = n.level; i <= maxRowLevel; i++) { + width += ev.rowsHierarchy.sampleNodesForAllLevels[i].width + } + n.width = width + } + }) + ev.rowsHierarchy.width = totlaRowWidth + ev.colsHierarchy.width = totalColWidth + } + s2.store.set('lastLayoutResult', undefined) + return + } + const containerWidth = containerDom.getBoundingClientRect().width + const scale = containerWidth / (ev.colsHierarchy.width + ev.rowsHierarchy.width) + if (scale <= 1) { + return + } + const totalRowWidth = Math.round(ev.rowsHierarchy.width * scale) + ev.rowNodes.forEach(n => { + n.width = Math.round(n.width * scale) + }) + if (basicStyle.tableLayoutMode !== 'tree') { + ev.rowNodes.forEach(n => { + n.x = 0 + let tmp = n + while (tmp.parent.id !== 'root') { + n.x += tmp.parent.width + tmp = tmp.parent + } + }) + } + let totalColWidth = ev.colLeafNodes.reduce((p, n) => { + n.width = Math.round(n.width * scale) + n.x = p + return p + n.width + }, 0) + ev.colNodes.forEach(n => { + if (n.isLeaf) { + return + } + n.width = this.getColWidth(n) + n.x = this.getLeftChild(n).x + }) + const totalWidth = totalColWidth + totalRowWidth + if (totalWidth > containerWidth) { + // 从最后一列减掉 + ev.colLeafNodes[ev.colLeafNodes.length - 1].width -= totalWidth - containerWidth + totalColWidth = totalColWidth - (totalWidth - containerWidth) + } + ev.colsHierarchy.width = totalColWidth + ev.rowsHierarchy.width = totalRowWidth + }) + } // tooltip const { show } = tooltip if (show) { s2.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(s2, event, meta)) s2.on(S2Event.ROW_CELL_HOVER, event => this.showTooltip(s2, event, meta)) s2.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(s2, event, meta)) + // touch + this.configTouchEvent(s2, drawOption, meta) } // empty data tip configEmptyDataStyle(s2, newData) @@ -312,19 +603,34 @@ export class TablePivot extends S2ChartView { s2.on(S2Event.COL_CELL_CLICK, ev => this.headerCellClickAction(chart, ev, s2, action)) // right click s2.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(s2, event, meta)) - // touch - this.configTouchEvent(s2, drawOption, meta) // theme const customTheme = this.configTheme(chart) s2.setThemeCfg({ theme: customTheme }) return s2 } + private getColWidth(node) { + let width = 0 + if (node.children?.length) { + node.children.forEach(child => { + width += this.getColWidth(child) + }) + } else { + width = node.width + } + return width + } + private getLeftChild(node) { + if (!node.children?.length) { + return node + } + return this.getLeftChild(node.children[0]) + } private dataCellClickAction(chart: Chart, ev, s2Instance: PivotSheet, callback) { const cell = s2Instance.getCell(ev.target) const meta = cell.getMeta() const nameIdMap = chart.data.fields.reduce((pre, next) => { - pre[next['dataeaseName']] = next['id'] + pre[next['gisbiName']] = next['id'] return pre }, {}) const rowData = { ...meta.rowQuery, ...meta.colQuery } @@ -352,7 +658,7 @@ export class TablePivot extends S2ChartView { const meta = cell.getMeta() const rowData = meta.query const nameIdMap = chart.data.fields.reduce((pre, next) => { - pre[next['dataeaseName']] = next['id'] + pre[next['gisbiName']] = next['id'] return pre }, {}) const dimensionList = [] @@ -522,7 +828,7 @@ export class TablePivot extends S2ChartView { } function customCalcFunc(query, data, status, chart, totalCfgMap, axisMap, customCalc) { if (!data?.length || !query[EXTRA_FIELD]) { - return 0 + return '-' } const aggregation = totalCfgMap[query[EXTRA_FIELD]]?.aggregation || 'SUM' switch (aggregation) { @@ -549,10 +855,13 @@ function customCalcFunc(query, data, status, chart, totalCfgMap, axisMap, custom }) return result?.[query[EXTRA_FIELD]] } + case 'NONE': { + return '-' + } case 'CUSTOM': { const val = getCustomCalcResult(query, axisMap, chart, status, customCalc || {}) - if (val === '') { - return val + if (val === '' || val === undefined) { + return '-' } return parseFloat(val) } @@ -593,11 +902,17 @@ function getTreeCustomCalcResult(query, axisMap, status: TotalStatus, customCalc // 列小计 if (status.isColSubTotal && !status.isRowTotal && !status.isRowSubTotal) { const { colSubTotal } = customCalc - const subLevel = getSubLevel(query, col) + const subColLevel = getSubLevel(query, col) + const subRowLevel = getSubLevel(query, row) const rowPath = getTreePath(query, row) const colPath = getTreePath(query, col) const path = [...rowPath, ...colPath] - const data = colSubTotal?.[subLevel]?.data + let data = colSubTotal?.[subColLevel]?.data + // 列小计里面的行小计 + if (rowPath.length < row.length) { + const { rowSubInColSub } = customCalc + data = rowSubInColSub?.[subRowLevel]?.[subColLevel]?.data + } let val if (path.length && data) { path.push(quotaField) @@ -647,7 +962,7 @@ function getTreeCustomCalcResult(query, axisMap, status: TotalStatus, customCalc if (status.isRowTotal && status.isColSubTotal) { const { colSubInRowTotal } = customCalc const colLevel = getSubLevel(query, col) - const { data } = colSubInRowTotal?.[colLevel] + const data = colSubInRowTotal?.[colLevel]?.data const colPath = getTreePath(query, col) let val if (colPath.length && colSubInRowTotal) { @@ -669,23 +984,7 @@ function getTreeCustomCalcResult(query, axisMap, status: TotalStatus, customCalc } return val } - // 列小计里面的行小计 - if (status.isColSubTotal && status.isRowSubTotal) { - const { rowSubInColSub } = customCalc - const rowSubLevel = getSubLevel(query, row) - const colSubLevel = getSubLevel(query, col) - const data = rowSubInColSub?.[rowSubLevel]?.[colSubLevel]?.data - const rowPath = getTreePath(query, row) - const colPath = getTreePath(query, col) - const path = [...rowPath, ...colPath] - let val - if (path.length && rowSubInColSub) { - path.push(quotaField) - val = get(data, path) - } - return val - } - return NaN + return '-' } function getGridCustomCalcResult(query, axisMap, status: TotalStatus, customCalc) { @@ -759,7 +1058,7 @@ function getGridCustomCalcResult(query, axisMap, status: TotalStatus, customCalc if (status.isRowTotal && status.isColSubTotal) { const { colSubInRowTotal } = customCalc const colLevel = getSubLevel(query, col) - const { data } = colSubInRowTotal?.[colLevel] + const data = colSubInRowTotal?.[colLevel]?.data const colPath = getTreePath(query, col) let val if (colPath.length && colSubInRowTotal) { @@ -807,7 +1106,7 @@ function getCustomCalcResult(query, axisMap, chart: ChartObj, status: TotalStatu } function getSubLevel(query, axis) { - const fields: [] = axis.map(a => a.dataeaseName) + const fields: [] = axis.map(a => a.gisbiName) let subLevel = -1 const queryFields = keys(query) for (let i = fields.length - 1; i >= 0; i--) { @@ -824,9 +1123,9 @@ function getTreePath(query, axis) { const path = [] const fields = keys(query) axis.forEach(a => { - const index = fields.findIndex(f => f === a.dataeaseName) + const index = fields.findIndex(f => f === a.gisbiName) if (index !== -1) { - path.push(query[a.dataeaseName]) + path.push(query[a.gisbiName]) } }) return path diff --git a/frontend/src/data-visualization/chart/components/js/panel/common/common_antv.ts b/frontend/src/data-visualization/chart/components/js/panel/common/common_antv.ts index 8bc7d32..aec9f36 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/common/common_antv.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/common/common_antv.ts @@ -33,10 +33,23 @@ import { PositionType } from '@antv/l7-core' import { centroid } from '@turf/centroid' import type { Plot } from '@antv/g2plot' import type { PickOptions } from '@antv/g2plot/lib/core/plot' -import { defaults } from 'lodash-es' +import { defaults, find } from 'lodash-es' import { useI18n } from '@/data-visualization/hooks/web/useI18n' -const { t: tI18n } = useI18n() import { isMobile } from '@/data-visualization/utils/utils' +import { GaodeMap, TMap, TencentMap } from '@antv/l7-maps' +import { + gaodeMapStyleOptions, + qqMapStyleOptions, + tdtMapStyleOptions +} from '@/data-visualization/chart/components/js/panel/charts/map/common' +import ChartCarouselTooltip, { + isPie, + isColumn, + isMix, + isSupport +} from '@/data-visualization/chart/components/js/g2plot_tooltip_carousel' + +const { t: tI18n } = useI18n() export function getPadding(chart: Chart): number[] { if (chart.drill) { @@ -137,14 +150,22 @@ export function getTheme(chart: Chart) { }, 'g2-tooltip-list-item': { display: 'flex', - 'align-items': 'center' + 'align-items': 'flex-start', + 'justify-content': 'space-between', + 'line-height': tooltipFontsize + 'px' }, 'g2-tooltip-name': { display: 'inline-block', - 'line-height': tooltipFontsize + 'px', - flex: 1 + 'line-height': tooltipFontsize + 'px' + }, + 'g2-tooltip-value': { + flex: 1, + display: 'inline-block', + 'text-align': 'end', + 'line-height': tooltipFontsize + 'px' }, 'g2-tooltip-marker': { + 'margin-top': (tooltipFontsize - 8) / 2 + 'px', 'min-width': '8px', 'min-height': '8px' } @@ -469,7 +490,8 @@ export function getXAxis(chart: Chart) { style: { fill: a.axisLabel.color, fontSize: a.axisLabel.fontSize, - textAlign: textAlign + textAlign: textAlign, + fontFamily: chart.fontFamily }, formatter: value => { return chart.type === 'bidirectional-bar' && value.length > a.axisLabel.lengthLimit @@ -574,7 +596,8 @@ export function getYAxis(chart: Chart) { fill: yAxis.axisLabel.color, fontSize: yAxis.axisLabel.fontSize, textBaseline, - textAlign + textAlign, + fontFamily: chart.fontFamily }, formatter: value => { return value.length > yAxis.axisLabel.lengthLimit @@ -603,7 +626,7 @@ export function getYAxisExt(chart: Chart) { return false } const title = - yAxis.name && yAxis.name !== '' + yAxis.nameShow && yAxis.name && yAxis.name !== '' ? { text: yAxis.name, style: { @@ -629,14 +652,16 @@ export function getYAxisExt(chart: Chart) { ? { style: { stroke: axisCfg.lineStyle.color, - lineWidth: axisCfg.lineStyle.width + lineWidth: axisCfg.lineStyle.width, + lineDash: getLineDash(axisCfg.lineStyle.style) } } : null const tickLine = axisCfg.show ? { style: { - stroke: axisCfg.lineStyle.color + stroke: axisCfg.lineStyle.color, + lineWidth: axisCfg.lineStyle.width } } : null @@ -673,7 +698,8 @@ export function getYAxisExt(chart: Chart) { fill: yAxis.axisLabel.color, fontSize: yAxis.axisLabel.fontSize, textBaseline, - textAlign + textAlign, + fontFamily: chart.fontFamily } } : null @@ -821,10 +847,9 @@ export function getAnalyseHorizontal(chart: Chart) { const assistLineArr = senior.assistLineCfg.assistLine if (assistLineArr?.length > 0) { const customStyle = parseJson(chart.customStyle) - let xAxisPosition, axisFormatterCfg + let axisFormatterCfg if (customStyle.xAxis) { const a = JSON.parse(JSON.stringify(customStyle.xAxis)) - xAxisPosition = transAxisPosition(a.position) axisFormatterCfg = a.axisLabelFormatter ? a.axisLabelFormatter : DEFAULT_XAXIS_STYLE.axisLabelFormatter @@ -894,7 +919,9 @@ export function getLineDash(type) { */ export function setGradientColor(rawColor: string, show = false, angle = 0, start = 0) { const item = rawColor.split(',') - item.splice(3, 1, '0.3)') + const alpha = parseFloat(item[3].replace(')', '')) + const startAlpha = alpha * 0.3 + item.splice(3, 1, `${startAlpha})`) let color: string if (start == 0) { color = `l(${angle}) 0:${item.join(',')} 1:${rawColor}` @@ -993,6 +1020,9 @@ export function configL7Tooltip(chart: Chart): TooltipOptions { return result } const head = originalItem.properties + if (!head) { + return result + } const formatter = formatterMap[head.quotaList?.[0]?.id] if (!isEmpty(formatter)) { const originValue = parseFloat(head.value as string) @@ -1152,13 +1182,27 @@ export class CustomZoom extends Zoom { 'l7-button-control', container, () => { - if (this.controlOption['bounds']) { - this.mapsService.fitBounds(this.controlOption['bounds'], { animate: true }) + if (this.mapsService.map?.deMapProvider == 'qq') { + if (this.mapsService.map.deMapAutoFit) { + this.mapsService.setZoomAndCenter(this.mapsService.map.deMapAutoZoom, [ + this.mapsService.map.deMapAutoLng, + this.mapsService.map.deMapAutoLat + ]) + } else { + this.mapsService.setZoomAndCenter( + this.controlOption['initZoom'], + this.controlOption['center'] + ) + } } else { - this.mapsService.setZoomAndCenter( - this.controlOption['initZoom'], - this.controlOption['center'] - ) + if (this.controlOption['bounds']) { + this.mapsService.fitBounds(this.controlOption['bounds'], { animate: true }) + } else { + this.mapsService.setZoomAndCenter( + this.controlOption['initZoom'], + this.controlOption['center'] + ) + } } } ) @@ -1208,7 +1252,11 @@ export class CustomZoom extends Zoom { } as IZoomControlOption } } -export function configL7Zoom(chart: Chart, scene: Scene) { +export function configL7Zoom( + chart: Chart, + scene: Scene, + mapKey?: { key: string; securityCode: string; mapType: string } +) { const { basicStyle } = parseJson(chart.customAttr) const zoomOption = scene?.getControlByName('zoom') if (zoomOption) { @@ -1220,20 +1268,56 @@ export function configL7Zoom(chart: Chart, scene: Scene) { if (!scene?.getControlByName('zoom')) { if (!scene.map) { scene.once('loaded', () => { - scene.map.on('complete', () => { - const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom() - const center = - basicStyle.autoFit === false - ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] - : [scene.map.getCenter().lng, scene.map.getCenter().lat] - const newZoomOptions = { - initZoom: initZoom, - center: center, - buttonColor: basicStyle.zoomButtonColor, - buttonBackground: basicStyle.zoomBackground - } as any - scene.addControl(new CustomZoom(newZoomOptions)) - }) + switch (mapKey?.mapType) { + case 'tianditu': + //天地图 + { + const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom() + const center = + basicStyle.autoFit === false + ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] + : [scene.map.getCenter().getLng(), scene.map.getCenter().getLat()] + const newZoomOptions = { + initZoom: initZoom, + center: center, + buttonColor: basicStyle.zoomButtonColor, + buttonBackground: basicStyle.zoomBackground + } as any + scene.addControl(new CustomZoom(newZoomOptions)) + } + break + case 'qq': + { + const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom() + const center = + basicStyle.autoFit === false + ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] + : [scene.map.getCenter().lng, scene.map.getCenter().lat] + const newZoomOptions = { + initZoom: initZoom, + center: center, + buttonColor: basicStyle.zoomButtonColor, + buttonBackground: basicStyle.zoomBackground + } as any + scene.addControl(new CustomZoom(newZoomOptions)) + } + break + default: + scene.map.on('complete', () => { + const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom() + const center = + basicStyle.autoFit === false + ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] + : [scene.map.getCenter().lng, scene.map.getCenter().lat] + const newZoomOptions = { + initZoom: initZoom, + center: center, + buttonColor: basicStyle.zoomButtonColor, + buttonBackground: basicStyle.zoomBackground + } as any + scene.addControl(new CustomZoom(newZoomOptions)) + }) + } }) } else { const newZoomOptions = { @@ -1250,19 +1334,19 @@ export function configL7Zoom(chart: Chart, scene: Scene) { const endAxis = chart.xAxisExt if (startAxis?.length === 2) { chart.data?.tableRow?.forEach(row => { - coordinates.push([row[startAxis[0].dataeaseName], row[startAxis[1].dataeaseName]]) + coordinates.push([row[startAxis[0].gisbiName], row[startAxis[1].gisbiName]]) }) } if (endAxis?.length === 2) { chart.data?.tableRow?.forEach(row => { - coordinates.push([row[endAxis[0].dataeaseName], row[endAxis[1].dataeaseName]]) + coordinates.push([row[endAxis[0].gisbiName], row[endAxis[1].gisbiName]]) }) } } else { const axis = chart.xAxis if (axis?.length === 2) { chart.data?.tableRow?.forEach(row => { - coordinates.push([row[axis[0].dataeaseName], row[axis[1].dataeaseName]]) + coordinates.push([row[axis[0].gisbiName], row[axis[1].gisbiName]]) }) } } @@ -1335,6 +1419,18 @@ export function mapRendering(dom: HTMLElement | string) { dom.classList.add('de-map-rendering') } +export function qqMapRendered(scene?: Scene) { + if (scene?.map && scene.map.deMapProvider === 'qq') { + setTimeout(() => { + if (scene.map) { + scene.map.deMapAutoZoom = scene.map.getZoom() + scene.map.deMapAutoLng = scene.map.getCenter().getLng() + scene.map.deMapAutoLat = scene.map.getCenter().getLat() + } + }, 1000) + } +} + export function mapRendered(dom: HTMLElement | string) { if (typeof dom === 'string') { dom = document.getElementById(dom) @@ -1342,6 +1438,213 @@ export function mapRendered(dom: HTMLElement | string) { dom.classList.add('de-map-rendered') } +export function getMapCenter(basicStyle: ChartBasicStyle) { + let center: [number, number] + if (basicStyle.autoFit === false) { + const longitude = basicStyle?.mapCenter?.longitude ?? DEFAULT_BASIC_STYLE.mapCenter.longitude + const latitude = basicStyle?.mapCenter?.latitude ?? DEFAULT_BASIC_STYLE.mapCenter.latitude + center = [longitude, latitude] + } else { + center = undefined + } + return center +} + +export function getMapStyle( + mapKey: { key: string; securityCode: string; mapType: string }, + basicStyle: ChartBasicStyle +) { + let mapStyle: string + switch (mapKey.mapType) { + case 'tianditu': + if (!find(tdtMapStyleOptions, s => s.value === basicStyle.mapStyle)) { + mapStyle = 'normal' + } else { + mapStyle = basicStyle.mapStyle + } + break + case 'qq': + if ( + !find(qqMapStyleOptions, s => s.value === basicStyle.mapStyle) || + basicStyle.mapStyle === 'normal' + ) { + mapStyle = 'normal' + } else { + mapStyle = basicStyle.mapStyleUrl + } + break + default: + if (!find(gaodeMapStyleOptions, s => s.value === basicStyle.mapStyle)) { + basicStyle.mapStyle = 'normal' + } + mapStyle = basicStyle.mapStyleUrl + if (basicStyle.mapStyle !== 'custom') { + mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` + } + break + } + return mapStyle +} + +export async function getMapScene( + chart: Chart, + scene: Scene, + container: string, + mapKey: { key: string; securityCode: string; mapType: string }, + basicStyle: ChartBasicStyle, + miscStyle: ChartMiscAttr, + mapStyle: string, + center?: [number, number] +) { + if (!scene) { + scene = new Scene({ + id: container, + logoVisible: false, + map: getMapObject(mapKey, basicStyle, miscStyle, mapStyle, center) + }) + } else { + if (mapKey.mapType === 'tianditu') { + scene.map?.checkResize() + } + if (scene.getLayers()?.length) { + await scene.removeAllLayer() + try { + scene.setPitch(miscStyle.mapPitch) + } catch (e) {} + if (mapKey.mapType === 'tianditu') { + if (mapStyle === 'normal') { + scene.map?.removeStyle() + } else { + scene.setMapStyle(mapStyle) + } + } else { + scene.setMapStyle(mapStyle) + } + + scene.map.showLabel = !(basicStyle.showLabel === false) + if (mapKey.mapType === 'qq') { + scene.map.setBaseMap({ + //底图设置(参数为:VectorBaseMap对象) + type: 'vector', //类型:失量底图 + features: basicStyle.showLabel === false ? ['base', 'building2d'] : undefined + //仅渲染:道路及底面(base) + 2d建筑物(building2d),以达到隐藏文字的效果 + }) + } + } + if (basicStyle.autoFit === false) { + scene.setZoomAndCenter(basicStyle.zoomLevel, center) + if (mapKey.mapType === 'qq') { + scene.map.deMapAutoFit = false + scene.map.deMapZoom = basicStyle.zoomLevel + scene.map.deMapCenter = center + } + } + } + mapRendering(container) + scene.once('loaded', () => { + mapRendered(container) + if (mapKey.mapType === 'qq') { + scene.map.setBaseMap({ + //底图设置(参数为:VectorBaseMap对象) + type: 'vector', //类型:失量底图 + features: basicStyle.showLabel === false ? ['base', 'building2d'] : undefined + //仅渲染:道路及底面(base) + 2d建筑物(building2d),以达到隐藏文字的效果 + }) + scene.setMapStyle(mapStyle) + + scene.map.deMapProvider = 'qq' + scene.map.deMapAutoFit = !!basicStyle.autoFit + // scene.map.deMapAutoZoom = scene.map.getZoom() + // scene.map.deMapAutoLng = scene.map.getCenter().getLng() + // scene.map.deMapAutoLat = scene.map.getCenter().getLat() + } + // 去除天地图自己的缩放按钮 + if (mapKey.mapType === 'tianditu') { + if (mapStyle === 'normal') { + scene.map?.removeStyle() + } else { + scene.setMapStyle(mapStyle) + } + + const tdtControl = document.querySelector( + `#component${chart.id} .tdt-control-zoom.tdt-bar.tdt-control` + ) + if (tdtControl) { + tdtControl.style.display = 'none' + } + const tdtControlOuter = document.querySelectorAll( + `#wrapper-outer-id-${chart.id} .tdt-control-zoom.tdt-bar.tdt-control` + ) + if (tdtControlOuter && tdtControlOuter.length > 0) { + for (let i = 0; i < tdtControlOuter.length; i++) { + tdtControlOuter[i].style.display = 'none' + } + } + const tdtCopyrightControl = document.querySelector( + `#component${chart.id} .tdt-control-copyright.tdt-control` + ) + if (tdtCopyrightControl) { + tdtCopyrightControl.style.display = 'none' + } + const tdtCopyrightControlOuter = document.querySelectorAll( + `#wrapper-outer-id-${chart.id} .tdt-control-copyright.tdt-control` + ) + if (tdtCopyrightControlOuter && tdtCopyrightControlOuter.length > 0) { + for (let i = 0; i < tdtCopyrightControlOuter.length; i++) { + tdtCopyrightControlOuter[i].style.display = 'none' + } + } + } + }) + return scene +} + +export function getMapObject( + mapKey: { key: string; securityCode: string; mapType: string }, + basicStyle: ChartBasicStyle, + miscStyle: ChartMiscAttr, + mapStyle: string, + center?: [number, number] +) { + switch (mapKey.mapType) { + case 'tianditu': + return new TMap({ + token: mapKey?.key ?? undefined, + style: mapStyle, //不生效 + pitch: undefined, //不支持 + center, + zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, + showLabel: !(basicStyle.showLabel === false), //不支持 + WebGLParams: { + preserveDrawingBuffer: true + } + }) + case 'qq': + return new TencentMap({ + token: mapKey?.key ?? undefined, + style: mapStyle, + pitch: miscStyle.mapPitch, + center, + zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : 12, + showLabel: !(basicStyle.showLabel === false), + WebGLParams: { + preserveDrawingBuffer: true + } + }) + default: + return new GaodeMap({ + token: mapKey?.key ?? undefined, + style: mapStyle, + pitch: miscStyle.mapPitch, + center, + zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, + showLabel: !(basicStyle.showLabel === false), + WebGLParams: { + preserveDrawingBuffer: true + } + }) + } +} /** * 隐藏缩放控件 * @param basicStyle @@ -1358,6 +1661,8 @@ export function getTooltipContainer(id) { let wrapperDom = document.getElementById(G2_TOOLTIP_WRAPPER) if (!wrapperDom) { wrapperDom = document.createElement('div') + wrapperDom.style.position = 'absolute' + wrapperDom.style.zIndex = '9999' wrapperDom.id = G2_TOOLTIP_WRAPPER document.body.appendChild(wrapperDom) } @@ -1391,14 +1696,78 @@ export function getTooltipContainer(id) { } return g2Tooltip } + +/** + * 配置提示轮播 + * @param plot + * @param chart + */ +function configCarouselTooltip(plot, chart) { + const start = isSupport(chart.type) && !document.getElementById('multiplexingDrawer') + if (start) { + // 启用轮播 + plot.once('afterrender', () => { + const carousel = chart.customAttr?.tooltip?.carousel + ChartCarouselTooltip.manage(plot, chart, { + xField: 'field', + duration: carousel.enable ? carousel?.stayTime * 1000 : 2000, + interval: carousel.enable ? carousel?.intervalTime * 1000 : 2000 + }) + }) + } +} +/** + * 计算 Tooltip 的位置 + * @param {Chart} chart - 图表实例 + * @param {boolean} isCarousel - 是否为轮播模式 + * @param {object} tooltipCtl - Tooltip 控制器 + * @param {HTMLElement} chartElement - 图表元素 + * @param {Event} event - 事件对象 + * @param {boolean} enlargeElement - 放大弹窗 + * @returns {{x: number, y: number}} - 计算后的 x 和 y 坐标 + */ +function calculateTooltipPosition(chart, isCarousel, tooltipCtl, chartElement, event) { + // 辅助函数: 根据不同图表类型计算 Tooltip 的y位置 + const getTooltipY = () => { + const top = Number(chartElement.getBoundingClientRect().top) + if (isColumn(chart.type)) { + return top + chartElement.getBoundingClientRect().height / 2 + } + if (isMix(chart.type) || isPie(chart.type)) { + return top + tooltipCtl.point.y + } + return top + tooltipCtl.point.y + 60 + } + if (isCarousel) { + return { + x: tooltipCtl.point.x + Number(chartElement.getBoundingClientRect().left), + y: getTooltipY() + } + } else { + return { x: event.clientX, y: event.clientY } + } +} export function configPlotTooltipEvent>( chart: Chart, plot: P ) { const { tooltip } = parseJson(chart.customAttr) if (!tooltip.show) { + ChartCarouselTooltip.destroyByContainer(chart.container) return } + // 图表容器,用于计算 tooltip 的位置 + // 获取图表元素,优先顺序:放大 > 预览 > 公共连接页面 > 默认 + const chartElement = + document.getElementById('container-viewDialog-' + chart.id + '-common') || + document.getElementById('container-preview-' + chart.id + '-common') || + document.getElementById('enlarge-inner-content-' + chart.id) || + document.getElementById('shape-id-' + chart.id) + // 是否是放大弹窗 + const enlargeElement = chartElement?.id.includes('viewDialog') + // 轮播时tooltip的zIndex + const carousel_zIndex = enlargeElement ? '9999' : '1002' + configCarouselTooltip(plot, chart) // 鼠标可移入, 移入之后保持显示, 移出之后隐藏 plot.options.tooltip.container.addEventListener('mouseenter', e => { e.target.style.visibility = 'visible' @@ -1415,10 +1784,25 @@ export function configPlotTooltipEvent> if (!tooltipCtl) { return } + // 处理 tooltip 与下拉菜单的显示冲突问题 + const viewTrackBarElement = document.getElementById('view-track-bar-' + chart.id) const event = plot.chart.interactions.tooltip?.context?.event + // 是否时轮播模式 + const isCarousel = + chart.customAttr?.tooltip?.carousel && + (!event || // 事件触发时,使用event的client坐标 + ['plot:leave', 'plot:mouseleave'].includes(event?.type) || //鼠标离开时,使用tooltipCtl.point + ['pie', 'pie-rose', 'pie-donut'].includes(chart.type)) // 饼图时,使用tooltipCtl.point + plot.options.tooltip.showMarkers = isCarousel ? true : false + const wrapperDom = document.getElementById(G2_TOOLTIP_WRAPPER) + wrapperDom.style.zIndex = isCarousel && wrapperDom ? carousel_zIndex : '9999' if (tooltipCtl.tooltip) { // 处理视图放大后再关闭 tooltip 的 dom 被清除 const container = tooltipCtl.tooltip.cfg.container + // 当下拉菜单不显示时,移除tooltip的hidden-tooltip样式 + if (viewTrackBarElement?.getAttribute('aria-expanded') === 'false') { + container.classList.toggle('hidden-tooltip', false) + } container.style.display = 'block' const dom = document.getElementById(container.id) if (!dom) { @@ -1433,8 +1817,17 @@ export function configPlotTooltipEvent> } plot.chart.getOptions().tooltip.follow = false tooltipCtl.title = Math.random().toString() - plot.chart.getTheme().components.tooltip.x = event.clientX - plot.chart.getTheme().components.tooltip.y = event.clientY + // 当显示提示为事件触发时,使用event的client坐标,否则使用tooltipCtl.point 数据点的位置,在图表中,需要加上图表在绘制区的位置 + const { x, y } = calculateTooltipPosition( + chart, + isCarousel, + tooltipCtl, + chartElement, + event, + enlargeElement + ) + plot.chart.getTheme().components.tooltip.x = x + plot.chart.getTheme().components.tooltip.y = y }) // https://github.com/antvis/G2/blob/master/src/chart/controller/tooltip.ts#hideTooltip plot.on('plot:leave', () => { @@ -1457,14 +1850,22 @@ export function configPlotTooltipEvent> if (!tooltipCtl) { return } - const container = tooltipCtl.tooltip.cfg.container + const container = tooltipCtl.tooltip?.cfg.container for (const ele of wrapperDom.children) { - if (container.id !== ele.id) { + if (!container || container.id !== ele.id) { ele.style.display = 'none' } } } }) + plot.on('tooltip:hidden', () => { + const tooltipCtl = plot.chart.getController('tooltip') + if (!tooltipCtl) { + return + } + const container = tooltipCtl.tooltip?.cfg.container + container && (container.style.display = 'none') + }) } export const TOOLTIP_TPL = @@ -1699,10 +2100,12 @@ export function configYaxisTitleLengthLimit(chart, plot) { ? wrappedTitle.slice(0, wrappedTitle.length - 2) + '...' : wrappedTitle + '...' } - // 更新Y轴标题的原始文本和截断后的文本 - ev.view.options.axes.yAxisExt.title.originalText = yAxis.name - ev.view.options.axes.yAxisExt.title.text = wrappedTitle + const { title } = ev.view.options.axes.yAxisExt + if (title) { + title.originalText = yAxis.name + title.text = wrappedTitle + } }) } @@ -1731,7 +2134,7 @@ export const addConditionsStyleColorToData = (chart: Chart, options) => { }) } else if (item.quotaList?.length) { const quotaList = item.quotaList.map(q => q.id) ?? [] - quotaList.forEach((q, index) => { + quotaList.forEach(q => { // 定义后,在 handleConditionsStyle 函数中使用 let currentValue = item[valueField] if (chart.type === 'progress-bar') { @@ -1798,7 +2201,7 @@ const getColorByConditions = (quotaList: [], values: number | number[], chart) = * @param chart * @param options */ -export function handleConditionsStyle(chart: Chart, options: O) { +export function handleConditionsStyle(chart: Chart, options) { const { threshold } = parseJson(chart.senior) if (!threshold.enable) return options const { basicStyle } = parseJson(chart.customAttr) @@ -1810,8 +2213,6 @@ export function handleConditionsStyle(chart: Chart, options: O) { // 辅助函数:配置柱条样式颜色,条形图为barStyle,柱形图为columnStyle const columnStyle = data => { return { - ...options.columnStyle, - ...options.barStyle, ...(data[colorField]?.[0] ? { fill: data[colorField][0] } : {}) } } @@ -1825,8 +2226,8 @@ export function handleConditionsStyle(chart: Chart, options: O) { const tmpOption = { ...options, rawFields, - columnStyle: columnStyle, - barStyle: columnStyle, + ...configRoundAngle(chart, 'columnStyle', columnStyle), + ...configRoundAngle(chart, 'barStyle', columnStyle), tooltip: { ...options.tooltip, ...(options.tooltip['customItems'] @@ -1934,7 +2335,7 @@ export const getTooltipItemConditionColor = item => { * @param newData * @param container */ -export const configEmptyDataStyle = (newChart, newData, container) => { +export const configEmptyDataStyle = (newData, container, newChart?, content?) => { /** * 辅助函数:移除空数据dom */ @@ -1949,15 +2350,121 @@ export const configEmptyDataStyle = (newChart, newData, container) => { if (!newData.length) { const emptyDom = document.createElement('div') emptyDom.id = container + '_empty' - emptyDom.textContent = tI18n('data_set.no_data') + emptyDom.textContent = content || tI18n('data_set.no_data') emptyDom.setAttribute( 'style', `position: absolute; - left: 45%; - top: 50%;` + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + color: darkgray; + textAlign: center;` ) const parent = document.getElementById(container) parent.insertBefore(emptyDom, parent.firstChild) - newChart.destroy() + newChart?.destroy() + } +} + +export const numberToChineseUnderHundred = (num: number): string => { + // 合法性检查 + if (num <= 0 || num > 99 || !Number.isInteger(num)) { + throw new Error('请输入1-99之间的整数') + } + + const digits = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九'] + + // 处理个位数 + if (num < 10) return digits[num] + + const tens = Math.floor(num / 10) + const ones = num % 10 + + // 处理整十 + if (ones === 0) { + return tens === 1 ? '十' : digits[tens] + '十' + } + + // 处理其他两位数 + return tens === 1 ? '十' + digits[ones] : digits[tens] + '十' + digits[ones] +} + +/** + * 配置柱条图的圆角 + * @param styleName + * @param callBack 自定义其他属性函数 + */ +export const configRoundAngle = (chart: Chart, styleName: string, callBack?: (datum) => {}) => { + const { basicStyle } = parseJson(chart.customAttr) + if (['roundAngle', 'topRoundAngle'].includes(basicStyle.radiusColumnBar)) { + const radius = Array(2).fill(basicStyle.columnBarRightAngleRadius) + const topRadius = [0, 0, ...radius] + const bottomRadius = [...radius, 0, 0] + const finalRadius = [...radius, ...radius] + if (chart.type.includes('-stack')) { + return { + [styleName]: datum => { + if (!datum.value) return { radius: [], ...(callBack ? callBack(datum) : {}) } + return { radius: finalRadius, ...(callBack ? callBack(datum) : {}) } + } + } + } + const isTopRound = basicStyle.radiusColumnBar === 'topRoundAngle' + // 对称条形图 + if (chart.type === 'bidirectional-bar') { + const valueField = basicStyle.layout === 'vertical' ? 'valueExt' : 'value' + return { + [styleName]: datum => ({ + radius: datum[valueField] && isTopRound ? topRadius : isTopRound ? radius : finalRadius, + ...(callBack ? callBack(datum) : {}) + }) + } + } + // 进度条 + if (chart.type === 'progress-bar') { + return { + [styleName]: datum => { + return { + radius: isTopRound ? bottomRadius : finalRadius, + ...(callBack ? callBack(datum) : {}) + } + } + } + } + // 区间条形图 + if (chart.type === 'bar-range') { + return { + [styleName]: datum => { + return { + radius: + datum?.values[0] < datum?.values[1] + ? isTopRound + ? bottomRadius + : finalRadius + : isTopRound + ? topRadius + : finalRadius, + ...(callBack ? callBack(datum) : {}) + } + } + } + } + // 配置柱条样式 + const style = datum => { + if (isTopRound) { + return { radius, ...(callBack ? callBack(datum) : {}) } + } + if (!isTopRound) { + return { radius: finalRadius, ...(callBack ? callBack(datum) : {}) } + } + } + return { + [styleName]: style + } + } + return { + [styleName]: datum => { + return { ...(callBack ? callBack(datum) : {}) } + } } } diff --git a/frontend/src/data-visualization/chart/components/js/panel/common/common_table.ts b/frontend/src/data-visualization/chart/components/js/panel/common/common_table.ts index 16fb754..2741b75 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/common/common_table.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/common/common_table.ts @@ -5,8 +5,10 @@ import { isAlphaColor, isTransparent, parseJson, - resetRgbOpacity -} from '../../util' + resetRgbOpacity, + safeDecimalSum, + safeDecimalMean +} from '../..//util' import { DEFAULT_BASIC_STYLE, DEFAULT_TABLE_CELL, @@ -43,13 +45,31 @@ import { updateShapeAttr, ViewMeta } from '@antv/s2' -import { cloneDeep, filter, find, intersection, keys, merge, repeat } from 'lodash-es' +import { + cloneDeep, + filter, + find, + intersection, + keys, + map, + maxBy, + meanBy, + merge, + minBy, + repeat, + sumBy, + size, + sum +} from 'lodash-es' import { createVNode, render } from 'vue' import TableTooltip from '@/data-visualization/chart/components/editor/common/TableTooltip.vue' import Exceljs from 'exceljs' import { saveAs } from 'file-saver' import { ElMessage } from 'element-plus-secondary' import { useI18n } from '@/data-visualization/hooks/web/useI18n' +import Decimal from 'decimal.js' + + const { t: i18nt } = useI18n() export function getCustomTheme(chart: Chart): S2Theme { @@ -401,8 +421,7 @@ export function getCustomTheme(chart: Chart): S2Theme { }, dataCell: { cell: { - crossBackgroundColor: - enableTableCrossBG && !tableCell.mergeCells ? tableItemSubBgColor : tableItemBgColor, + crossBackgroundColor: enableTableCrossBG ? tableItemSubBgColor : tableItemBgColor, backgroundColor: tableItemBgColor }, bolderText: { @@ -504,8 +523,8 @@ export function getStyle(chart: Chart, dataConfig: S2DataConfig): Style { item => item.id === chart.drillFilters[0].fieldId ) const drillEnterField = xAxis[drillEnterFieldIndex] - fieldMap[curDrillField.dataeaseName] = { - width: fieldMap[drillEnterField.dataeaseName]?.width + fieldMap[curDrillField.gisbiName] = { + width: fieldMap[drillEnterField.gisbiName]?.width } } // 铺满 @@ -579,7 +598,7 @@ export function getCurrentField(valueFieldList: Axis[], field: ChartViewField) { if (list) { for (let i = 0; i < list.length; i++) { const f = list[i] - if (field.dataeaseName === f.dataeaseName) { + if (field.gisbiName === f.gisbiName) { res = f break } @@ -600,10 +619,10 @@ export function getConditions(chart: Chart) { } const conditions = threshold.tableThreshold ?? [] - const dimFields = [...chart.xAxis, ...chart.xAxisExt].map(i => i.dataeaseName) + const dimFields = [...chart.xAxis, ...chart.xAxisExt].map(i => i.gisbiName) if (conditions?.length > 0) { const { tableCell, basicStyle, tableHeader } = parseJson(chart.customAttr) - // 合并单元格时,班马纹失效 + // 合并单元格时斑马纹失效 const enableTableCrossBG = chart.type === 'table-info' ? tableCell.enableTableCrossBG && !tableCell.mergeCells @@ -626,12 +645,12 @@ export function getConditions(chart: Chart) { let defaultValueColor = valueColor let defaultBgColor = valueBgColor // 透视表表头颜色配置 - if (chart.type === 'table-pivot' && dimFields.includes(field.field.dataeaseName)) { + if (chart.type === 'table-pivot' && dimFields.includes(field.field.gisbiName)) { defaultValueColor = headerValueColor defaultBgColor = headerValueBgColor } res.text.push({ - field: field.field.dataeaseName, + field: field.field.gisbiName, mapping(value, rowData) { // 总计小计 if (rowData?.isTotals) { @@ -647,7 +666,7 @@ export function getConditions(chart: Chart) { } }) res.background.push({ - field: field.field.dataeaseName, + field: field.field.gisbiName, mapping(value, rowData) { if (rowData?.isTotals) { return null @@ -783,6 +802,9 @@ export function mappingColor(value, defaultColor, field, type, filedValueMap?, r } } else { // time + if (!tv || !value) { + break + } const fc = field.conditions[i] tv = new Date(tv.replace(/-/g, '/') + ' GMT+8').getTime() const v = new Date(value.replace(/-/g, '/') + ' GMT+8').getTime() @@ -842,7 +864,7 @@ function getFieldValueMap(view) { function getValue(field, filedValueMap, rowData) { if (field.summary === 'value') { - return rowData ? rowData[field.field?.dataeaseName] : undefined + return rowData ? rowData[field.field?.gisbiName] : undefined } else { return filedValueMap[field.summary + '-' + field.fieldId] } @@ -880,6 +902,7 @@ export function handleTableEmptyStrategy(chart: Chart) { } return newData } + export class SortTooltip extends BaseTooltip { show(showOptions) { const { iconName } = showOptions @@ -934,6 +957,7 @@ export class SortTooltip extends BaseTooltip { }) } } + const SORT_DEFAULT = '' const SORT_UP = @@ -1063,7 +1087,14 @@ export function copyContent(s2Instance: SpreadSheet, event, fieldMeta) { if (cells.length === 1) { const curCell = cells[0] if (cell.getMeta().id === curCell.id) { - copyString(cellMeta.value + '', true) + const cellMeta = cell.getMeta() + const value = cellMeta.data?.[cellMeta.valueField] + const metaObj = find(fieldMeta, m => m.field === cellMeta.valueField) + let fieldVal = value?.toString() + if (metaObj) { + fieldVal = metaObj.formatter(value) + } + copyString(fieldVal, true) } s2Instance.interaction.clearState() return @@ -1189,7 +1220,7 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) { const { meta, fields } = instance.dataCfg const rowLength = fields?.rows?.length || 0 const colLength = fields?.columns?.length || 0 - const colNums = layoutResult.colLeafNodes.length + rowLength + 1 + const colNums = layoutResult.colLeafNodes.length + rowLength if (colNums > 16384) { ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed')) return @@ -1346,9 +1377,180 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) { if (fieldValue === 0 || fieldValue) { const meta = metaMap[dataCellMeta.valueField] const cell = worksheet.getCell(rowIndex + maxColHeight + 1, rowLength + colIndex + 1) - const value = meta?.formatter?.(fieldValue) || fieldValue.toString() + const value = meta?.formatter?.(fieldValue) || fieldValue cell.alignment = { vertical: 'middle', horizontal: 'center' } - cell.value = value + cell.value = isNumeric(value) ? parseFloat(value) : value + } + } + } + const buffer = await workbook.xlsx.writeBuffer() + const dataBlob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8' + }) + saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`) +} + +export async function exportRowQuotaGridPivot(instance: PivotSheet, chart: ChartObj) { + const { layoutResult } = instance.facet + const { meta, fields } = instance.dataCfg + const rowLength = fields?.rows?.length || 0 + const colLength = fields?.columns?.length || 0 + const colNums = layoutResult.colLeafNodes.length + rowLength + if (colNums > 16384) { + ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed')) + return + } + const workbook = new Exceljs.Workbook() + const worksheet = workbook.addWorksheet(i18nt('chart.chart_data')) + const metaMap: Record = meta?.reduce((p, n) => { + if (n.field) { + p[n.field] = n + } + return p + }, {}) + // 角头 + if (colLength > 1) { + fields.columns.forEach((column: string, index) => { + if (index >= colLength - 1) { + return + } + const cell = worksheet.getCell(index + 1, 1) + cell.value = metaMap[column]?.name ?? column + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } } + } + worksheet.mergeCells(index + 1, 1, index + 1, rowLength + 1) + }) + } + fields?.rows?.forEach((row, index) => { + const cell = worksheet.getCell(colLength === 0 ? 1 : colLength, index + 1) + cell.value = metaMap[row]?.name ?? row + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { bottom: { style: 'thick', color: { argb: '00000000' } } } + }) + const quotaColLabel = chart.customAttr.basicStyle.quotaColLabel ?? t('dataset.value') + const quotaColHeadCell = worksheet.getCell(colLength === 0 ? 1 : colLength, rowLength + 1) + quotaColHeadCell.value = quotaColLabel + quotaColHeadCell.alignment = { vertical: 'middle', horizontal: 'center' } + quotaColHeadCell.border = { + bottom: { style: 'thick', color: { argb: '00000000' } }, + right: { style: 'thick', color: { argb: '00000000' } } + } + // 行头 + const { rowLeafNodes, rowNodes } = layoutResult + const notLeafNodeHeightMap: Record = {} + rowLeafNodes.forEach(node => { + // 行头的高度由子节点相加决定,也就是行头子节点中包含的叶子节点数量 + let curNode = node.parent + while (curNode) { + const height = notLeafNodeHeightMap[curNode.id] ?? 0 + notLeafNodeHeightMap[curNode.id] = height + 1 + curNode = curNode.parent + } + const { rowIndex } = node + const writeRowIndex = rowIndex + 2 + (colLength === 0 ? 1 : colLength - 1) + const writeColIndex = node.level + 1 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + let value = node.label + if (node.field === '$$extra$$' && metaMap[value]?.name) { + value = metaMap[value].name + } + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } } + } + }) + + const getNodeStartRowIndex = (node: Node) => { + if (!node.children?.length) { + return node.rowIndex + 1 + } else { + return getNodeStartRowIndex(node.children[0]) + } + } + rowNodes?.forEach(node => { + if (node.isLeaf) { + return + } + const rowIndex = getNodeStartRowIndex(node) + const height = notLeafNodeHeightMap[node.id] + const writeRowIndex = rowIndex + 1 + (colLength === 0 ? 1 : colLength - 1) + const mergeColCount = node.children[0].level - node.level + const cell = worksheet.getCell(writeRowIndex, node.level + 1) + cell.value = node.label + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (mergeColCount > 1 || height > 1) { + worksheet.mergeCells( + writeRowIndex, + node.level + 1, + writeRowIndex + height - 1, + node.level + mergeColCount + ) + } + }) + + // 列头 + const { colLeafNodes, colNodes, colsHierarchy } = layoutResult + const maxColHeight = colsHierarchy.maxLevel + 1 + const notLeafNodeWidthMap: Record = {} + colLeafNodes.forEach(node => { + // 列头的宽度由子节点相加决定,也就是列头子节点中包含的叶子节点数量 + let curNode = node.parent + while (curNode) { + const width = notLeafNodeWidthMap[curNode.id] ?? 0 + notLeafNodeWidthMap[curNode.id] = width + 1 + curNode = curNode.parent + } + const { colIndex } = node + const writeRowIndex = node.level + 1 + const writeColIndex = colIndex + rowLength + 2 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + const value = node.label + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (writeRowIndex < maxColHeight) { + worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex) + } + cell.border = { + bottom: { style: 'thick', color: { argb: '00000000' } } + } + }) + const getNodeStartColIndex = (node: Node) => { + if (!node.children?.length) { + return node.colIndex + 1 + } else { + return getNodeStartColIndex(node.children[0]) + } + } + colNodes.forEach(node => { + if (node.isLeaf) { + return + } + const colIndex = getNodeStartColIndex(node) + const width = notLeafNodeWidthMap[node.id] + const writeRowIndex = node.level + 1 + const value = node.label + const writeColIndex = colIndex + rowLength + 1 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (width > 1) { + worksheet.mergeCells(writeRowIndex, writeColIndex, writeRowIndex, writeColIndex + width - 1) + } + }) + // 单元格数据 + for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) { + for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) { + const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex) + const { fieldValue } = dataCellMeta + if (fieldValue === 0 || fieldValue) { + const meta = metaMap[dataCellMeta.valueField] + const cell = worksheet.getCell(rowIndex + maxColHeight + 1, rowLength + colIndex + 2) + const value = meta?.formatter?.(fieldValue) || fieldValue + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.value = isNumeric(value) ? parseFloat(value) : value } } } @@ -1361,7 +1563,7 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) { export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) { const layoutResult = instance.facet.layoutResult - if (layoutResult.colLeafNodes.length + 2 > 16384) { + if (layoutResult.colLeafNodes.length + 1 > 16384) { ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed')) return } @@ -1468,9 +1670,9 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) { if (fieldValue === 0 || fieldValue) { const meta = metaMap[dataCellMeta.valueField] const cell = worksheet.getCell(rowIndex + maxColHeight + 1, colIndex + 1 + 1) - const value = meta?.formatter?.(fieldValue) || fieldValue.toString() + const value = meta?.formatter?.(fieldValue) || fieldValue cell.alignment = { vertical: 'middle', horizontal: 'center' } - cell.value = value + cell.value = isNumeric(value) ? parseFloat(value) : value } } } @@ -1480,6 +1682,135 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) { }) saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`) } + +export async function exportRowQuotaTreePivot(instance: PivotSheet, chart: ChartObj) { + const layoutResult = instance.facet.layoutResult + if (layoutResult.colLeafNodes.length + 1 > 16384) { + ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed')) + return + } + const { meta, fields } = instance.dataCfg + const colLength = fields?.columns?.length || 0 + const workbook = new Exceljs.Workbook() + const worksheet = workbook.addWorksheet(i18nt('chart.chart_data')) + const metaMap: Record = meta?.reduce((p, n) => { + if (n.field) { + p[n.field] = n + } + return p + }, {}) + + // 角头 + fields.columns?.forEach((column, index) => { + if (index >= fields.columns.length - 1) { + return + } + const cell = worksheet.getCell(index + 1, 1) + cell.value = metaMap[column]?.name ?? column + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } } + } + }) + const quotaColLabel = chart.customAttr.basicStyle.quotaColLabel ?? t('dataset.value') + const maxColHeight = layoutResult.colsHierarchy.maxLevel + 1 + const rowName = fields?.rows + ?.map(row => metaMap[row]?.name ?? row) + .concat(quotaColLabel) + .join('/') + const cell = worksheet.getCell(colLength, 1) + cell.value = rowName + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } }, + bottom: { style: 'thick', color: { argb: '00000000' } } + } + //行头 + const { rowLeafNodes } = layoutResult + rowLeafNodes.forEach((node, index) => { + const cell = worksheet.getCell(maxColHeight + index + 1, 1) + let value = node.label + if (node.field === '$$extra$$' && metaMap[value]?.name) { + value = metaMap[value].name + } + cell.value = repeat(' ', node.level) + value + cell.alignment = { vertical: 'middle', horizontal: 'left' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } } + } + }) + // 列头 + const notLeafNodeWidthMap: Record = {} + const { colLeafNodes } = layoutResult + colLeafNodes.forEach(node => { + let curNode = node.parent + while (curNode) { + const width = notLeafNodeWidthMap[curNode.id] ?? 0 + notLeafNodeWidthMap[curNode.id] = width + 1 + curNode = curNode.parent + } + const { colIndex } = node + const writeRowIndex = node.level + 1 + const writeColIndex = colIndex + 2 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = node.label + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (writeRowIndex < maxColHeight) { + worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex) + } + cell.border = { + bottom: { style: 'thick', color: { argb: '00000000' } } + } + }) + const colNodes = layoutResult.colNodes + const getNodeStartIndex = (node: Node) => { + if (!node.children?.length) { + return node.colIndex + 1 + } else { + return getNodeStartIndex(node.children[0]) + } + } + colNodes.forEach(node => { + if (node.isLeaf) { + return + } + const colIndex = getNodeStartIndex(node) + const width = notLeafNodeWidthMap[node.id] + const writeRowIndex = node.level + 1 + const writeColIndex = colIndex + 1 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = node.label + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (width > 1) { + worksheet.mergeCells(writeRowIndex, writeColIndex, writeRowIndex, writeColIndex + width - 1) + } + }) + // 单元格数据 + for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) { + for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) { + const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex) + const { fieldValue } = dataCellMeta + if (fieldValue === 0 || fieldValue) { + const meta = metaMap[dataCellMeta.valueField] + const cell = worksheet.getCell(rowIndex + maxColHeight + 1, colIndex + 2) + const value = meta?.formatter?.(fieldValue) || fieldValue + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.value = isNumeric(value) ? parseFloat(value) : value + } + } + } + const buffer = await workbook.xlsx.writeBuffer() + const dataBlob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8' + }) + saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`) +} + + +function isNumeric(value: string): boolean { + return /^[+-]?\d+(\.\d+)?$/.test(value) +} + export async function exportPivotExcel(instance: PivotSheet, chart: ChartObj) { const { fields } = instance.dataCfg const rowLength = fields?.rows?.length || 0 @@ -1488,10 +1819,19 @@ export async function exportPivotExcel(instance: PivotSheet, chart: ChartObj) { ElMessage.warning(i18nt('chart.pivot_export_invalid_field')) return } + const { quotaPosition } = chart.customAttr.basicStyle if (chart.customAttr.basicStyle.tableLayoutMode !== 'tree') { - exportGridPivot(instance, chart) + if (quotaPosition === 'row') { + exportRowQuotaGridPivot(instance, chart) + } else { + exportGridPivot(instance, chart) + } } else { - exportTreePivot(instance, chart) + if (quotaPosition === 'row') { + exportRowQuotaTreePivot(instance, chart) + } else { + exportTreePivot(instance, chart) + } } } @@ -1504,7 +1844,7 @@ export function configMergeCells(chart: Chart, options: S2Options, dataConfig: S const fields = chart.data.fields || [] const fieldsMap = fields.reduce((p, n) => { - p[n.dataeaseName] = n + p[n.gisbiName] = n return p }, {}) || {} const quotaIndex = dataConfig.meta.findIndex(m => fieldsMap[m.field]?.groupType === 'q') @@ -1571,6 +1911,7 @@ export function configMergeCells(chart: Chart, options: S2Options, dataConfig: S if (showIndex && meta.colIndex === 0) { meta.fieldValue = getRowIndex(mergedCellsInfo, meta) } + meta.deFieldType = fieldsMap[meta.valueField]?.deType return new CustomMergedCell(sheet, cells, meta) } } @@ -1598,12 +1939,13 @@ export function getRowIndex(mergedCellsInfo: MergedCellInfo[][], meta: ViewMeta) }, 0) return curRangeStartIndex - lostCells + 1 } + class CustomMergedCell extends MergedCell { protected drawBackgroundShape() { const allPoints = getPolygonPoints(this.cells) // 处理条件样式,这里没有用透明度 // 因为合并的单元格是单独的图层,透明度降低的话会显示底下未合并的单元格,需要单独处理被覆盖的单元格 - const { backgroundColor: fill, backgroundColorOpacity: fillOpacity } = this.getBackgroundColor() + const { backgroundColor: fill } = this.getBackgroundColor() const cellTheme = this.theme.dataCell.cell this.backgroundShape = renderPolygon(this, { points: allPoints, @@ -1612,6 +1954,14 @@ class CustomMergedCell extends MergedCell { lineHeight: cellTheme.horizontalBorderWidth }) } + + drawTextShape(): void { + if (this.meta.deFieldType === 7) { + drawImage.apply(this) + } else { + super.drawTextShape() + } + } } export class CustomDataCell extends TableDataCell { @@ -1847,96 +2197,95 @@ const getWrapTextHeight = (wrapText, textStyle, spreadsheet, maxLines) => { return Math.min(lines, maxLines) * maxHeight } -/** - * 设置汇总行 - * @param chart - * @param s2Options - * @param newData - * @param tableHeader - * @param basicStyle - * @param showSummary - */ -export const configSummaryRow = ( - chart, - s2Options, - newData, - tableHeader, - basicStyle, - showSummary -) => { - if (!showSummary || !newData.length) return - // 设置汇总行高度和表头一致 - const heightByField = {} - heightByField[newData.length] = tableHeader.tableTitleHeight - s2Options.style.rowCfg = { heightByField } - // 计算汇总加入到数据里,冻结最后一行 - s2Options.frozenTrailingRowCount = 1 - const yAxis = chart.yAxis - const xAxis = chart.xAxis - const summaryObj = newData.reduce( - (p, n) => { - if (chart.type === 'table-info') { - xAxis - .filter(axis => [2, 3, 4].includes(axis.deType)) - .forEach(axis => { - p[axis.dataeaseName] = - (parseFloat(n[axis.dataeaseName]) || 0) + (parseFloat(p[axis.dataeaseName]) || 0) +// 导出获取汇总行的函数 +export function getSummaryRow(data, axis, sumCon = []) { + const summaryObj = { SUMMARY: true } + for (let i = 0; i < axis.length; i++) { + const a = axis[i].gisbiName + let savedAxis = find(sumCon, s => s.field === a) + if (savedAxis) { + if (savedAxis.summary == undefined) { + savedAxis.summary = 'sum' // 默认汇总方式为求和 + } + if (savedAxis.show == undefined) { + savedAxis.show = true // 默认显示汇总结果 + } + } else { + savedAxis = { + field: a, + summary: 'sum', + show: true + } + } + // 如果配置为不显示,则跳过该字段 + if (!savedAxis.show) { + continue + } + // 根据汇总方式处理数据 + switch (savedAxis.summary) { + case 'sum': + // 计算字段的总和 + summaryObj[a] = safeDecimalSum(data, a) + break + case 'avg': + // 计算字段的平均值 + summaryObj[a] = safeDecimalMean(data, a) + break + case 'max': + // 计算字段的最大值 + summaryObj[a] = maxBy( + filter(data, d => parseFloat(d[a]) !== undefined), + d => parseFloat(d[a]) // 提取数值 + )[a] + break + case 'min': + // 计算字段的最小值 + summaryObj[a] = minBy( + filter(data, d => parseFloat(d[a]) !== undefined), + d => parseFloat(d[a]) // 提取数值 + )[a] + break + case 'var_pop': + // 计算总体方差(需要至少2个数据点) + if (data.length < 2) { + continue + } else { + const mean = safeDecimalMean(data, a) // 计算平均值 + // 计算每个数据点与平均值的差的平方 + const squaredDeviations = map(data, d => { + const value = new Decimal(d[a] ?? 0) // 获取字段值,如果不存在则使用0 + const dev = value.minus(mean) // 计算差值 + return dev.times(dev) // 计算平方 }) - } else { - yAxis.forEach(axis => { - p[axis.dataeaseName] = - (parseFloat(n[axis.dataeaseName]) || 0) + (parseFloat(p[axis.dataeaseName]) || 0) - }) - } - return p - }, - { SUMMARY: true } - ) - newData.push(summaryObj) - s2Options.dataCell = viewMeta => { - // 配置文本自动换行参数 - viewMeta.autoWrap = basicStyle.autoWrap - viewMeta.maxLines = basicStyle.maxLines - if (viewMeta.rowIndex !== newData.length - 1) { - return new CustomDataCell(viewMeta, viewMeta.spreadsheet) - } - if (viewMeta.colIndex === 0) { - if (tableHeader.showIndex) { - viewMeta.fieldValue = basicStyle.summaryLabel ?? i18nt('chart.total_show') - } else { - if (xAxis.length) { - viewMeta.fieldValue = basicStyle.summaryLabel ?? i18nt('chart.total_show') + // 计算方差(平方差的平均值) + const variance = squaredDeviations.reduce((acc, val) => acc.plus(val), new Decimal(0)) + summaryObj[a] = variance.dividedBy(data.length - 1).toNumber() // 计算总体方差 } - } + break + case 'stddev_pop': + // 计算总体标准差(需要至少2个数据点) + if (data.length < 2) { + continue + } else { + const mean = safeDecimalMean(data, a) // 计算平均值 + // 计算每个数据点与平均值的差的平方 + const squaredDeviations = map(data, d => { + const value = new Decimal(d[a] ?? 0) // 获取字段值,如果不存在则使用0 + const dev = value.minus(mean) // 计算差值 + return dev.times(dev) // 计算平方 + }) + // 计算方差(平方差的平均值) + const variance = squaredDeviations.reduce((acc, val) => acc.plus(val), new Decimal(0)) + summaryObj[a] = variance.dividedBy(data.length - 1).sqrt().toNumber() // 计算总体标准差 + } + break } - return new SummaryCell(viewMeta, viewMeta.spreadsheet) } + + // 返回汇总结果对象 + return summaryObj } -/** - * 汇总行样式,紧贴在单元格后面 - * @param newChart - * @param newData - * @param tableCell - * @param tableHeader - * @param showSummary - */ -export const summaryRowStyle = (newChart, newData, tableCell, tableHeader, showSummary) => { - if (!showSummary || !newData.length) return - newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => { - const showHeader = tableHeader.showTableHeader === true - // 不显示表头时,减少一个表头的高度 - const headerAndSummaryHeight = showHeader ? 2 : 1 - const totalHeight = - tableHeader.tableTitleHeight * headerAndSummaryHeight + - tableCell.tableItemHeight * (newData.length - 1) - if (totalHeight < newChart.options.height) { - // 6 是阴影高度 - newChart.options.height = - totalHeight < newChart.options.height - 6 ? totalHeight + 6 : totalHeight - } - }) -} export class SummaryCell extends CustomDataCell { getTextStyle() { @@ -1944,6 +2293,7 @@ export class SummaryCell extends CustomDataCell { textStyle.textAlign = this.theme.dataCell.text.textAlign return textStyle } + getBackgroundColor() { const { backgroundColor, backgroundColorOpacity } = this.theme.colCell.cell return { backgroundColor, backgroundColorOpacity } @@ -2019,3 +2369,27 @@ export const getColumns = (fields, cols: Array) => { } return result } + +export function drawImage() { + const img = new Image() + const { x, y, width, height, fieldValue } = this.meta + img.src = fieldValue as string + img.setAttribute('crossOrigin', 'anonymous') + img.onload = () => { + !this.cfg.children && (this.cfg.children = []) + const { width: imgWidth, height: imgHeight } = img + const ratio = Math.max(imgWidth / width, imgHeight / height) + // 不铺满,部分留白 + const imgShowWidth = (imgWidth / ratio) * 0.8 + const imgShowHeight = (imgHeight / ratio) * 0.8 + this.textShape = this.addShape('image', { + attrs: { + x: x + (imgShowWidth < width ? (width - imgShowWidth) / 2 : 0), + y: y + (imgShowHeight < height ? (height - imgShowHeight) / 2 : 0), + width: imgShowWidth, + height: imgShowHeight, + img + } + }) + } +} diff --git a/frontend/src/data-visualization/chart/components/js/panel/types/impl/g2plot.ts b/frontend/src/data-visualization/chart/components/js/panel/types/impl/g2plot.ts index c8269b5..dd32468 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/types/impl/g2plot.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/types/impl/g2plot.ts @@ -170,7 +170,7 @@ export abstract class G2PlotChartView< public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] { return setupSeriesColor(chart, data) } - + // eslint-disable-next-line public setupSubSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] { return undefined } @@ -191,8 +191,8 @@ export abstract class G2PlotChartView< return addConditionsStyleColorToData(chart, data) } - protected configEmptyDataStyle(newChart, newData: any[], container: string) { - configEmptyDataStyle(newChart, newData, container) + protected configEmptyDataStyle(newData, container, newChart?, content?) { + configEmptyDataStyle(newData, container, newChart, content) } /** diff --git a/frontend/src/data-visualization/chart/components/js/panel/types/impl/l7.ts b/frontend/src/data-visualization/chart/components/js/panel/types/impl/l7.ts index 24bd6aa..093ffd1 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/types/impl/l7.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/types/impl/l7.ts @@ -107,8 +107,8 @@ export abstract class L7ChartView< return options } - protected configZoomButton(chart: Chart, plot: S) { - configL7Zoom(chart, plot) + protected configZoomButton(chart: Chart, plot: S, mapKey?: any) { + configL7Zoom(chart, plot, mapKey) } protected configLabel(chart: Chart, options: O): O { diff --git a/frontend/src/data-visualization/chart/components/js/panel/types/impl/s2.ts b/frontend/src/data-visualization/chart/components/js/panel/types/impl/s2.ts index 3bcc669..2ec21e4 100644 --- a/frontend/src/data-visualization/chart/components/js/panel/types/impl/s2.ts +++ b/frontend/src/data-visualization/chart/components/js/panel/types/impl/s2.ts @@ -136,18 +136,18 @@ export abstract class S2ChartView

extends AntVAbstractCha if (duration > 300) { return } + const canvasPosition = canvas.getBoundingClientRect() + const touchPosition = [e.changedTouches[0].pageX, e.changedTouches[0].pageY] + const relativePosition = [ + touchPosition[0] - canvasPosition.x, + touchPosition[1] - canvasPosition.y + ] + const shape = s2Instance.container.getShape(relativePosition[0], relativePosition[1]) + // 图片单元格,表头排序图标点击放大图片 + if (shape.cfg?.type === 'image') { + return + } const callback = () => { - const canvasPosition = canvas.getBoundingClientRect() - const touchPosition = [e.changedTouches[0].pageX, e.changedTouches[0].pageY] - const relativePosition = [ - touchPosition[0] - canvasPosition.x, - touchPosition[1] - canvasPosition.y - ] - const shape = s2Instance.container.getShape(relativePosition[0], relativePosition[1]) - // 图片单元格点击放大图片 - if (shape.cfg?.parent.constructor.name === 'ImageCell') { - return - } e.preventDefault() e.stopPropagation() if (shape) { diff --git a/frontend/src/data-visualization/chart/components/js/util.ts b/frontend/src/data-visualization/chart/components/js/util.ts index 33984be..1e7af19 100644 --- a/frontend/src/data-visualization/chart/components/js/util.ts +++ b/frontend/src/data-visualization/chart/components/js/util.ts @@ -1,4 +1,4 @@ -import { isEmpty, isNumber } from 'lodash-es' +import { isNumber } from 'lodash-es' import { DEFAULT_TITLE_STYLE } from '../editor/util/chart' import { equalsAny, includesAny } from '../editor/util/StringUtils' import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types' @@ -12,6 +12,8 @@ import { ElMessage } from 'element-plus-secondary' import { useI18n } from '@/data-visualization/hooks/web/useI18n' import { useLinkStoreWithOut } from '@/data-visualization/store/modules/link' import { useAppStoreWithOut } from '@/data-visualization/store/modules/app' +import { Decimal } from 'decimal.js' + const appStore = useAppStoreWithOut() const isDataEaseBi = computed(() => appStore.getIsDataEaseBi) @@ -283,17 +285,23 @@ export function handleEmptyDataStrategy(chart: Chart, opt } return options } - const { yAxis, xAxisExt, extStack } = chart + const { yAxis, xAxisExt, extStack, extBubble } = chart const multiDimension = yAxis?.length >= 2 || xAxisExt?.length > 0 || extStack?.length > 0 switch (strategy) { case 'breakLine': { - if (multiDimension) { - // 多维度保持空 - if (isChartMix) { - for (let i = 0; i < data.length; i++) { - handleBreakLineMultiDimension(data[i] as Record[]) + if (isChartMix) { + if (data[0]) { + if (xAxisExt?.length > 0 || extStack?.length > 0) { + handleBreakLineMultiDimension(data[0] as Record[]) } - } else { + } + if (data[1]) { + if (extBubble?.length > 0) { + handleBreakLineMultiDimension(data[1] as Record[]) + } + } + } else { + if (multiDimension) { handleBreakLineMultiDimension(data) } } @@ -303,22 +311,27 @@ export function handleEmptyDataStrategy(chart: Chart, opt } } case 'setZero': { - if (multiDimension) { - // 多维度置0 - if (isChartMix) { - for (let i = 0; i < data.length; i++) { - handleSetZeroMultiDimension(data[i] as Record[]) + if (isChartMix) { + if (data[0]) { + if (xAxisExt?.length > 0 || extStack?.length > 0) { + handleSetZeroMultiDimension(data[0] as Record[]) + } else { + handleSetZeroSingleDimension(data[0] as Record[]) + } + } + if (data[1]) { + if (extBubble?.length > 0) { + handleSetZeroMultiDimension(data[1] as Record[], true) + } else { + handleSetZeroSingleDimension(data[1] as Record[], true) } - } else { - handleSetZeroMultiDimension(data) } } else { - // 单维度置0 - if (isChartMix) { - for (let i = 0; i < data.length; i++) { - handleSetZeroSingleDimension(data[i] as Record[]) - } + if (multiDimension) { + // 多维度置0 + handleSetZeroMultiDimension(data) } else { + // 单维度置0 handleSetZeroSingleDimension(data) } } @@ -364,7 +377,7 @@ function handleBreakLineMultiDimension(data) { }) } -function handleSetZeroMultiDimension(data: Record[]) { +function handleSetZeroMultiDimension(data: Record[], isExt = false) { const dimensionInfoMap = new Map() const subDimensionSet = new Set() const quotaMap = new Map() @@ -372,6 +385,9 @@ function handleSetZeroMultiDimension(data: Record[]) { const item = data[i] if (item.value === null) { item.value = 0 + if (isExt) { + item.valueExt = 0 + } } const dimensionInfo = dimensionInfoMap.get(item.field) if (dimensionInfo) { @@ -388,12 +404,17 @@ function handleSetZeroMultiDimension(data: Record[]) { let subInsertIndex = 0 subDimensionSet.forEach(dimension => { if (!dimensionInfo.set.has(dimension)) { - data.splice(dimensionInfo.index + insertCount + subInsertIndex, 0, { + const _temp = { field, value: 0, category: dimension, quotaList: quotaMap.get(dimension as string) - }) + } as any + if (isExt) { + _temp.valueExt = 0 + } + + data.splice(dimensionInfo.index + insertCount + subInsertIndex, 0, _temp) } subInsertIndex++ }) @@ -402,10 +423,14 @@ function handleSetZeroMultiDimension(data: Record[]) { }) } -function handleSetZeroSingleDimension(data: Record[]) { +function handleSetZeroSingleDimension(data: Record[], isExt = false) { data.forEach(item => { if (item.value === null) { - item.value = 0 + if (!isExt) { + item.value = 0 + } else { + item.valueExt = 0 + } } }) } @@ -489,7 +514,7 @@ const getExcelDownloadRequest = (data, type?) => { const tableRow = JSON.parse(JSON.stringify(data.tableRow)) const excelHeader = fields.map(item => item.chartShowName ?? item.name) const excelTypes = fields.map(item => item.deType) - const excelHeaderKeys = fields.map(item => item.dataeaseName) + const excelHeaderKeys = fields.map(item => item.gisbiName) let excelData = tableRow.map(item => excelHeaderKeys.map(i => item[i])) let detailFields = [] if (data.detailFields?.length) { @@ -497,7 +522,7 @@ const getExcelDownloadRequest = (data, type?) => { return { name: item.name, deType: item.deType, - dataeaseName: item.dataeaseName + gisbiName: item.gisbiName } }) excelData = tableRow.map(item => { @@ -505,7 +530,7 @@ const getExcelDownloadRequest = (data, type?) => { if (i === 'detail' && !item[i] && Array.isArray(item['details'])) { const arr = item['details'] if (arr?.length) { - return arr.map(ele => detailFields.map(field => ele[field.dataeaseName])) + return arr.map(ele => detailFields.map(field => ele[field.gisbiName])) } return null } @@ -522,8 +547,20 @@ const getExcelDownloadRequest = (data, type?) => { } } -export const exportExcelDownload = (chart, callBack?) => { - const excelName = chart.title +function getChartExcelTitle(preFix, viewTitle) { + const now = new Date() + const pad = n => n.toString().padStart(2, '0') + const year = now.getFullYear() + const month = pad(now.getMonth() + 1) // 月份从 0 开始 + const day = pad(now.getDate()) + const hour = pad(now.getHours()) + const minute = pad(now.getMinutes()) + const second = pad(now.getSeconds()) + return `${preFix}_${viewTitle}_${year}${month}${day}_${hour}${minute}${second}` +} + +export const exportExcelDownload = (chart, preFix, callBack?) => { + const excelName = getChartExcelTitle(preFix, chart.title) let request: any = { proxy: null, dvId: chart.sceneId, @@ -586,18 +623,21 @@ export const exportExcelDownload = (chart, callBack?) => { } export const copyString = (content: string, notify = false) => { - const clipboard = navigator.clipboard || { - writeText: data => { - return new Promise(resolve => { - const textareaDom = document.createElement('textarea') - textareaDom.setAttribute('style', 'z-index: -1;position: fixed;opacity: 0;') - textareaDom.value = data - document.body.appendChild(textareaDom) - textareaDom.select() - document.execCommand('copy') - textareaDom.remove() - resolve() - }) + let clipboard = navigator.clipboard as Pick + if (!clipboard || window.top !== window.self) { + clipboard = { + writeText: data => { + return new Promise(resolve => { + const textareaDom = document.createElement('textarea') + textareaDom.setAttribute('style', 'z-index: -1;position: fixed;opacity: 0;') + textareaDom.value = data + document.body.appendChild(textareaDom) + textareaDom.select() + document.execCommand('copy') + textareaDom.remove() + resolve() + }) + } } } clipboard.writeText(content).then(() => { @@ -779,7 +819,7 @@ export function getColor(chart: Chart) { } } -export function setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] { +export function setupSeriesColor(chart: ChartObj): ChartBasicStyle['seriesColor'] { const result: ChartBasicStyle['seriesColor'] = [] const seriesSet = new Set() const colors = chart.customAttr.basicStyle.colors @@ -1152,8 +1192,10 @@ export function getLineLabelColorByCondition(conditions, value, fieldId) { if (fieldConditions.length) { fieldConditions.some(item => { if ( - (item.term === 'lt' && value <= item.value) || - (item.term === 'gt' && value >= item.value) || + (item.term === 'lt' && value < item.value) || + (item.term === 'le' && value <= item.value) || + (item.term === 'gt' && value > item.value) || + (item.term === 'ge' && value >= item.value) || (item.term === 'between' && value >= item.min && value <= item.max) ) { color = item.color @@ -1207,3 +1249,27 @@ export const hexToRgba = (hex, alpha = 1) => { // 返回 RGBA 格式 return `rgba(${r}, ${g}, ${b}, ${a})` } + +// 安全计算数值字段的总和,使用 Decimal 避免浮点数精度问题 +export function safeDecimalSum(data, field) { + // 使用 reduce 累加所有行的指定字段值 + return data + .reduce((acc, row) => { + // 将字段值转换为 Decimal 类型并累加到累加器 + return acc.plus(new Decimal(row[field] ?? 0)) + }, new Decimal(0)) + .toNumber() // 最终结果转换为普通数字返回 +} + +// 安全计算数值字段的平均值,使用 Decimal 避免浮点数精度问题 +export function safeDecimalMean(data, field) { + // 如果数据为空,直接返回 0 + if (!data.length) return 0 + // 计算所有行的指定字段值的总和 + const sum = data.reduce((acc, row) => { + // 将字段值转换为 Decimal 类型并累加到累加器 + return acc.plus(new Decimal(row[field] ?? 0)) + }, new Decimal(0)) + // 将总和除以数据行数,得到平均值,并转换为普通数字返回 + return sum.dividedBy(data.length).toNumber() +} diff --git a/frontend/src/data-visualization/chart/components/views/components/ChartComponentG2Plot.vue b/frontend/src/data-visualization/chart/components/views/components/ChartComponentG2Plot.vue index 850343f..df0eef1 100644 --- a/frontend/src/data-visualization/chart/components/views/components/ChartComponentG2Plot.vue +++ b/frontend/src/data-visualization/chart/components/views/components/ChartComponentG2Plot.vue @@ -28,7 +28,8 @@ import { isDashboard, trackBarStyleCheck } from '@/data-visualization/utils/canv import { useEmitt } from '@/data-visualization/hooks/web/useEmitt' import { L7ChartView } from '@/data-visualization/chart/components/js/panel/types/impl/l7' import { useI18n } from '@/data-visualization/hooks/web/useI18n' -import { ExportImage,Scale } from '@antv/l7' +import { ExportImage, Scale, Fullscreen, Control, Scene, TileLayer } from '@antv/l7' +import { GaodeMap } from '@antv/l7-maps'; const { t } = useI18n() const dvMainStore = dvMainStoreWithOut() const { nowPanelTrackInfo, nowPanelJumpInfo, mobileInPc, embeddedCallBack, inMobile } = @@ -111,7 +112,8 @@ const state = reactive({ }, linkageActiveParam: null, pointParam: null, - data: { fields: [] } // 图表数据 + data: { fields: [] }, // 图表数据 + satelliteVisible: false, // 新增卫星图层状态 }) let chartData = shallowRef>({ fields: [] @@ -333,35 +335,100 @@ const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView, callback) => { - mapL7Timer && clearTimeout(mapL7Timer) + mapL7Timer && clearTimeout(mapL7Timer); mapL7Timer = setTimeout(async () => { myChart = await chartView.drawChart({ chartObj: myChart, container: containerId, chart: chart, action - }) - - // 清除已有比例尺 - if (scaleControl) { - myChart.getScene()?.removeControl(scaleControl) - scaleControl = null + }); + + // 清除已有比例尺 + if (!scaleControl) { + scaleControl = new Scale({ + position: 'bottomleft', + imperial: false + }); + myChart.getScene()?.addControl(scaleControl); } // 创建并添加新比例尺 - scaleControl = new Scale({ - position: 'bottomleft', - imperial: false - }) - myChart.getScene()?.addControl(scaleControl) - myChart?.render() - callback?.() - emit('resetLoading') - }, 500) -} + + // 添加全屏控件 + if (fullscreenControl) { + + } else { + fullscreenControl = new Fullscreen({ + position: 'bottomright', + }); + myChart.getScene()?.addControl(fullscreenControl, 'bottomright'); + } + + + // ====== 使用高德地图原生API实现卫星图层切换 ====== + let satelliteLayer: any = null; + let isSatelliteVisible = false; + + class SatelliteControl extends Control { + protected onAdd() { + const btn = document.createElement('button'); + btn.className = 'l7-control-button l7-satellite-control'; + btn.innerHTML = '卫星'; + // btn.title = '切换到卫星视图'; + btn.style.backgroundColor = '#000'; + btn.style.color = '#fff'; + btn.style.padding = '2px'; + btn.style.borderRadius = '4px'; + btn.style.cursor = 'pointer' + btn.style.fontSize = '11px'; + const scene = myChart.getScene() + // 确保地图加载完成 + scene.on('loaded', () => { + // 创建高德卫星图层 + satelliteLayer = new window.AMap.TileLayer.Satellite(); + btn.onclick = () => { + isSatelliteVisible = !isSatelliteVisible; + + if (isSatelliteVisible) { + // 使用 scene.addLayer 方法添加卫星图层 + btn.style.backgroundColor = '#409eff'; + scene.map.add(satelliteLayer) + } else { + // 使用 scene.removeLayer 方法移除卫星图层 + btn.style.backgroundColor = '#000'; + scene.map.remove(satelliteLayer) + + } + }; + }); + + return btn; + } + } + + // 添加控件到地图 + // 移除之前的卫星控件(如果存在) + if (satelliteControlInstance) { + + } else { + // 添加新的卫星控件到地图 + satelliteControlInstance = new SatelliteControl({ position: 'bottomright' }); + myChart.getScene()?.addControl(satelliteControlInstance); + } + + + // ====== 修复完成 ====== + + myChart?.render(); + callback?.(); + emit('resetLoading'); + }, 500); +}; const pointClickTrans = () => { if (embeddedCallBack.value === 'yes') { trackClick('pointClick') @@ -706,15 +773,8 @@ onBeforeUnmount(() => {