From 8d16c85352f55ef94a7b9b46b9648eec19bb29a1 Mon Sep 17 00:00:00 2001 From: bingling-sama Date: Wed, 28 May 2025 15:45:24 +0800 Subject: [PATCH 1/2] refactor(chart-advisor): refactor whole chart advisor module --- .../src/__tests__/scorers/bar.test.ts | 43 ++++++++++++ .../src/__tests__/scorers/line.test.ts | 65 +++++++++++++++++++ .../src/__tests__/scorers/pie.test.ts | 43 ++++++++++++ packages/chart-advisor/src/rules/common.ts | 57 ++++++++++++++++ packages/chart-advisor/src/rules/compose.ts | 28 ++++++++ packages/chart-advisor/src/rules/config.ts | 16 +++++ packages/chart-advisor/src/scorers/bar.ts | 29 +++++++++ packages/chart-advisor/src/scorers/line.ts | 29 +++++++++ packages/chart-advisor/src/scorers/pie.ts | 29 +++++++++ packages/chart-advisor/src/types/index.ts | 42 ++++++++++++ .../chart-advisor/src/utils/calculation.ts | 19 ++++++ .../chart-advisor/src/utils/validation.ts | 16 +++++ 12 files changed, 416 insertions(+) create mode 100644 packages/chart-advisor/src/__tests__/scorers/bar.test.ts create mode 100644 packages/chart-advisor/src/__tests__/scorers/line.test.ts create mode 100644 packages/chart-advisor/src/__tests__/scorers/pie.test.ts create mode 100644 packages/chart-advisor/src/rules/common.ts create mode 100644 packages/chart-advisor/src/rules/compose.ts create mode 100644 packages/chart-advisor/src/rules/config.ts create mode 100644 packages/chart-advisor/src/scorers/bar.ts create mode 100644 packages/chart-advisor/src/scorers/line.ts create mode 100644 packages/chart-advisor/src/scorers/pie.ts create mode 100644 packages/chart-advisor/src/types/index.ts create mode 100644 packages/chart-advisor/src/utils/calculation.ts create mode 100644 packages/chart-advisor/src/utils/validation.ts diff --git a/packages/chart-advisor/src/__tests__/scorers/bar.test.ts b/packages/chart-advisor/src/__tests__/scorers/bar.test.ts new file mode 100644 index 00000000..737f44ff --- /dev/null +++ b/packages/chart-advisor/src/__tests__/scorers/bar.test.ts @@ -0,0 +1,43 @@ +import { createBarScorer } from '../../scorers/bar'; +import { defaultConfig } from '../../rules/config'; + +describe('Bar Scorer', () => { + const scorer = createBarScorer(defaultConfig); + + it('should return full score for valid bar chart data', () => { + const data = { + bars: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }], + dimensions: ['dim1'], + metrics: ['metric1'], + chartType: 'bar' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBe(1); + expect(result.details.dataRange).toBe(true); + expect(result.details.dimensionMetric).toBe(true); + }); + + it('should return zero score for invalid bar count', () => { + const data = { + bars: [], + dimensions: ['dim1'], + metrics: ['metric1'], + chartType: 'bar' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBeLessThan(1); + expect(result.details.dataRange).toBe(false); + }); + + it('should return zero score for missing dimensions/metrics', () => { + const data = { + bars: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }], + dimensions: [], + metrics: [], + chartType: 'bar' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBeLessThan(1); + expect(result.details.dimensionMetric).toBe(false); + }); +}); diff --git a/packages/chart-advisor/src/__tests__/scorers/line.test.ts b/packages/chart-advisor/src/__tests__/scorers/line.test.ts new file mode 100644 index 00000000..a0a0d566 --- /dev/null +++ b/packages/chart-advisor/src/__tests__/scorers/line.test.ts @@ -0,0 +1,65 @@ +import { createLineScorer } from '../../scorers/line'; +import { defaultConfig } from '../../rules/config'; + +describe('Line Scorer', () => { + const scorer = createLineScorer(defaultConfig); + + it('should return full score for valid line chart data', () => { + const data = { + bars: [ + { value: 1 }, + { value: 2 }, + { value: 3 }, + { value: 4 }, + { value: 5 }, + { value: 6 }, + { value: 7 }, + { value: 8 }, + { value: 9 }, + { value: 10 } + ], + dimensions: ['dim1'], + metrics: ['metric1'], + chartType: 'line' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBe(1); + expect(result.details.dataRange).toBe(true); + expect(result.details.dimensionMetric).toBe(true); + }); + + it('should return zero score for invalid bar count', () => { + const data = { + bars: [], + dimensions: ['dim1'], + metrics: ['metric1'], + chartType: 'line' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBeLessThan(1); + expect(result.details.dataRange).toBe(false); + }); + + it('should return zero score for missing dimensions/metrics', () => { + const data = { + bars: [ + { value: 1 }, + { value: 2 }, + { value: 3 }, + { value: 4 }, + { value: 5 }, + { value: 6 }, + { value: 7 }, + { value: 8 }, + { value: 9 }, + { value: 10 } + ], + dimensions: [], + metrics: [], + chartType: 'line' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBeLessThan(1); + expect(result.details.dimensionMetric).toBe(false); + }); +}); diff --git a/packages/chart-advisor/src/__tests__/scorers/pie.test.ts b/packages/chart-advisor/src/__tests__/scorers/pie.test.ts new file mode 100644 index 00000000..93339429 --- /dev/null +++ b/packages/chart-advisor/src/__tests__/scorers/pie.test.ts @@ -0,0 +1,43 @@ +import { createPieScorer } from '../../scorers/pie'; +import { defaultConfig } from '../../rules/config'; + +describe('Pie Scorer', () => { + const scorer = createPieScorer(defaultConfig); + + it('should return full score for valid pie chart data', () => { + const data = { + bars: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }], + dimensions: ['dim1'], + metrics: ['metric1'], + chartType: 'pie' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBe(1); + expect(result.details.dataRange).toBe(true); + expect(result.details.dimensionMetric).toBe(true); + }); + + it('should return zero score for invalid bar count', () => { + const data = { + bars: [], + dimensions: ['dim1'], + metrics: ['metric1'], + chartType: 'pie' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBeLessThan(1); + expect(result.details.dataRange).toBe(false); + }); + + it('should return zero score for missing dimensions/metrics', () => { + const data = { + bars: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }], + dimensions: [], + metrics: [], + chartType: 'pie' + }; + const result = scorer(data, defaultConfig); + expect(result.score).toBeLessThan(1); + expect(result.details.dimensionMetric).toBe(false); + }); +}); diff --git a/packages/chart-advisor/src/rules/common.ts b/packages/chart-advisor/src/rules/common.ts new file mode 100644 index 00000000..c3b448ba --- /dev/null +++ b/packages/chart-advisor/src/rules/common.ts @@ -0,0 +1,57 @@ +import { Rule, ScoringConfig, ChartData } from '../types'; +import { coefficientOfVariation } from '../utils/calculation'; + +// 数据量范围检查 +export const dataRangeRule = (config: ScoringConfig): Rule => ({ + name: 'dataRange', + weight: config.weights.dataRange, + check: (data: ChartData) => ({ + passed: Array.isArray(data.bars) + ? data.bars.length >= config.thresholds.minBarNumber && data.bars.length <= config.thresholds.maxBarNumber + : false, + score: 1, + details: `Bar count: ${Array.isArray(data.bars) ? data.bars.length : 0}` + }) +}); + +// 维度指标检查 +export const dimensionMetricRule = (config: ScoringConfig): Rule => ({ + name: 'dimensionMetric', + weight: config.weights.dimensionCheck, + check: (data: ChartData) => ({ + passed: (data.dimensions?.length ?? 0) >= 1 && (data.metrics?.length ?? 0) >= 1, + score: 1, + details: `Dimensions: ${data.dimensions?.length ?? 0}, Metrics: ${data.metrics?.length ?? 0}` + }) +}); + +// 数据分布检查(使用变异系数) +export const dataDistributionRule = (config: ScoringConfig): Rule => ({ + name: 'dataDistribution', + weight: config.weights.dataDistribution, + check: (data: ChartData) => { + // 假设 bars 中有 value 字段 + const values = Array.isArray(data.bars) + ? data.bars.map((d: any) => d.value).filter((v: any) => typeof v === 'number') + : []; + const coef = coefficientOfVariation(values); + // 以 0.2 作为分布合理的阈值(可根据实际需求调整) + const passed = coef >= 0.2; + return { + passed, + score: passed ? 1 : 0, + details: `Coefficient of variation: ${coef}` + }; + } +}); + +// 用户目的匹配规则(示例,具体实现可后续补充) +export const userPurposeRule = (config: ScoringConfig): Rule => ({ + name: 'userPurpose', + weight: config.weights.userPurpose, + check: (data: ChartData) => ({ + passed: true, // 先占位 + score: 1, + details: 'User purpose check passed' + }) +}); diff --git a/packages/chart-advisor/src/rules/compose.ts b/packages/chart-advisor/src/rules/compose.ts new file mode 100644 index 00000000..c994d41c --- /dev/null +++ b/packages/chart-advisor/src/rules/compose.ts @@ -0,0 +1,28 @@ +import { Rule, ScorerFn, ChartData, ScoringConfig, ScoreResult } from '../types'; + +// 组合多个评分规则 +export const composeRules = + (rules: Rule[]): ScorerFn => + (data: ChartData, config: ScoringConfig): ScoreResult => { + const results = rules.map(rule => ({ + ...rule.check(data, config), + weight: rule.weight, + name: rule.name + })); + + const totalWeight = results.reduce((sum, r) => sum + r.weight, 0); + const totalScore = results.reduce((sum, r) => sum + (r.passed ? r.score * r.weight : 0), 0); + + return { + score: totalWeight > 0 ? totalScore / totalWeight : 0, + details: results.reduce( + (details, r) => ({ + ...details, + [r.name]: r.passed + }), + {} + ), + ruleResults: results, + chartType: data.chartType || 'unknown' + }; + }; diff --git a/packages/chart-advisor/src/rules/config.ts b/packages/chart-advisor/src/rules/config.ts new file mode 100644 index 00000000..9c6f42f4 --- /dev/null +++ b/packages/chart-advisor/src/rules/config.ts @@ -0,0 +1,16 @@ +import { ScoringConfig } from '../types'; + +export const defaultConfig: ScoringConfig = { + thresholds: { + maxBarNumber: 30, + minBarNumber: 2, + maxDataRange: 1000, + minDataRatio: 0.01 + }, + weights: { + dimensionCheck: 1.0, + dataRange: 3.0, + dataDistribution: 2.0, + userPurpose: 1.0 + } +}; diff --git a/packages/chart-advisor/src/scorers/bar.ts b/packages/chart-advisor/src/scorers/bar.ts new file mode 100644 index 00000000..8ba1f7a6 --- /dev/null +++ b/packages/chart-advisor/src/scorers/bar.ts @@ -0,0 +1,29 @@ +import { ChartData, ScoringConfig, ScorerFn } from '../types'; +import { dataRangeRule, dimensionMetricRule, dataDistributionRule, userPurposeRule } from '../rules/common'; +import { composeRules } from '../rules/compose'; + +// 柱状图特有规则(如有,可补充) +const barSpecificRules = (config: ScoringConfig) => [ + // 示例:可根据实际需求添加 + // { + // name: 'barSpacing', + // weight: 1.0, + // check: (data: ChartData) => ({ + // passed: true, + // score: 1, + // details: 'Bar spacing check passed' + // }) + // } +]; + +// 创建柱状图评分器 +export const createBarScorer = (config: ScoringConfig): ScorerFn => { + const rules = [ + dataRangeRule(config), + dimensionMetricRule(config), + dataDistributionRule(config), + userPurposeRule(config), + ...barSpecificRules(config) + ]; + return composeRules(rules); +}; diff --git a/packages/chart-advisor/src/scorers/line.ts b/packages/chart-advisor/src/scorers/line.ts new file mode 100644 index 00000000..75d9737e --- /dev/null +++ b/packages/chart-advisor/src/scorers/line.ts @@ -0,0 +1,29 @@ +import { ChartData, ScoringConfig, ScorerFn } from '../types'; +import { dataRangeRule, dimensionMetricRule, dataDistributionRule, userPurposeRule } from '../rules/common'; +import { composeRules } from '../rules/compose'; + +// 折线图特有规则(如有,可补充) +const lineSpecificRules = (config: ScoringConfig) => [ + // 示例:可根据实际需求添加 + // { + // name: 'lineContinuity', + // weight: 1.0, + // check: (data: ChartData) => ({ + // passed: true, + // score: 1, + // details: 'Line continuity check passed' + // }) + // } +]; + +// 创建折线图评分器 +export const createLineScorer = (config: ScoringConfig): ScorerFn => { + const rules = [ + dataRangeRule(config), + dimensionMetricRule(config), + dataDistributionRule(config), + userPurposeRule(config), + ...lineSpecificRules(config) + ]; + return composeRules(rules); +}; diff --git a/packages/chart-advisor/src/scorers/pie.ts b/packages/chart-advisor/src/scorers/pie.ts new file mode 100644 index 00000000..e000b92b --- /dev/null +++ b/packages/chart-advisor/src/scorers/pie.ts @@ -0,0 +1,29 @@ +import { ChartData, ScoringConfig, ScorerFn } from '../types'; +import { dataRangeRule, dimensionMetricRule, dataDistributionRule, userPurposeRule } from '../rules/common'; +import { composeRules } from '../rules/compose'; + +// 饼图特有规则(如有,可补充) +const pieSpecificRules = (config: ScoringConfig) => [ + // 示例:可根据实际需求添加 + // { + // name: 'pieSegmentCount', + // weight: 1.0, + // check: (data: ChartData) => ({ + // passed: Array.isArray(data.bars) && data.bars.length <= 10, + // score: 1, + // details: `Pie segment count: ${Array.isArray(data.bars) ? data.bars.length : 0}` + // }) + // } +]; + +// 创建饼图评分器 +export const createPieScorer = (config: ScoringConfig): ScorerFn => { + const rules = [ + dataRangeRule(config), + dimensionMetricRule(config), + dataDistributionRule(config), + userPurposeRule(config), + ...pieSpecificRules(config) + ]; + return composeRules(rules); +}; diff --git a/packages/chart-advisor/src/types/index.ts b/packages/chart-advisor/src/types/index.ts new file mode 100644 index 00000000..6073d300 --- /dev/null +++ b/packages/chart-advisor/src/types/index.ts @@ -0,0 +1,42 @@ +export interface RuleResult { + passed: boolean; + score: number; + details?: string; +} + +export interface Rule { + name: string; + weight: number; + check: (data: ChartData, config: ScoringConfig) => RuleResult; +} + +export type ScorerFn = (data: ChartData, config: ScoringConfig) => ScoreResult; + +export interface ScoreResult { + score: number; + details: Record; + ruleResults: RuleResult[]; + chartType: string; +} + +// 你可以根据实际数据结构补充 ChartData 类型 +export interface ChartData { + // 示例字段 + bars?: any[]; + [key: string]: any; +} + +export interface ScoringConfig { + thresholds: { + maxBarNumber: number; + minBarNumber: number; + maxDataRange: number; + minDataRatio: number; + }; + weights: { + dimensionCheck: number; + dataRange: number; + dataDistribution: number; + userPurpose: number; + }; +} diff --git a/packages/chart-advisor/src/utils/calculation.ts b/packages/chart-advisor/src/utils/calculation.ts new file mode 100644 index 00000000..4f9e48be --- /dev/null +++ b/packages/chart-advisor/src/utils/calculation.ts @@ -0,0 +1,19 @@ +// 计算标准差 +export function standardDeviation(arr: number[]): number { + if (!arr.length) return 0; + const mean = arr.reduce((a, b) => a + b, 0) / arr.length; + const variance = arr.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / arr.length; + return Math.sqrt(variance); +} + +// 计算极差 +export function range(arr: number[]): number { + if (!arr.length) return 0; + return Math.max(...arr) - Math.min(...arr); +} + +// 计算变异系数 +export function coefficientOfVariation(arr: number[]): number { + const mean = arr.reduce((a, b) => a + b, 0) / arr.length; + return mean === 0 ? 0 : standardDeviation(arr) / mean; +} diff --git a/packages/chart-advisor/src/utils/validation.ts b/packages/chart-advisor/src/utils/validation.ts new file mode 100644 index 00000000..8d3f0340 --- /dev/null +++ b/packages/chart-advisor/src/utils/validation.ts @@ -0,0 +1,16 @@ +import { ChartData } from '../types'; + +// 校验 bars 是否为有效数组且长度大于0 +export function validateBars(data: ChartData): boolean { + return Array.isArray(data.bars) && data.bars.length > 0; +} + +// 校验维度和指标 +export function validateDimensionsMetrics(data: ChartData): boolean { + return ( + Array.isArray(data.dimensions) && + data.dimensions.length > 0 && + Array.isArray(data.metrics) && + data.metrics.length > 0 + ); +} From d2b7672fa1b54e83f990bd0f67f7fc4f4ea21c7a Mon Sep 17 00:00:00 2001 From: bingling-sama Date: Sat, 21 Jun 2025 21:21:21 +0800 Subject: [PATCH 2/2] refactor(chart-advisor): complete scorer refactor while preserving API --- .../src/__tests__/scorers/bar.test.ts | 6 +- .../src/__tests__/scorers/line.test.ts | 46 +-- .../src/__tests__/scorers/pie.test.ts | 6 +- packages/chart-advisor/src/index.ts | 131 +++++-- packages/chart-advisor/src/rules/common.ts | 17 +- packages/chart-advisor/src/types/index.ts | 336 ++++++++++++++++-- 6 files changed, 445 insertions(+), 97 deletions(-) diff --git a/packages/chart-advisor/src/__tests__/scorers/bar.test.ts b/packages/chart-advisor/src/__tests__/scorers/bar.test.ts index 737f44ff..911d5d5d 100644 --- a/packages/chart-advisor/src/__tests__/scorers/bar.test.ts +++ b/packages/chart-advisor/src/__tests__/scorers/bar.test.ts @@ -6,7 +6,7 @@ describe('Bar Scorer', () => { it('should return full score for valid bar chart data', () => { const data = { - bars: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }], + data: [{ metric1: 1 }, { metric1: 2 }, { metric1: 3 }, { metric1: 4 }, { metric1: 5 }], dimensions: ['dim1'], metrics: ['metric1'], chartType: 'bar' @@ -19,7 +19,7 @@ describe('Bar Scorer', () => { it('should return zero score for invalid bar count', () => { const data = { - bars: [], + data: [], dimensions: ['dim1'], metrics: ['metric1'], chartType: 'bar' @@ -31,7 +31,7 @@ describe('Bar Scorer', () => { it('should return zero score for missing dimensions/metrics', () => { const data = { - bars: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }], + data: [{ metric1: 1 }, { metric1: 2 }, { metric1: 3 }, { metric1: 4 }, { metric1: 5 }], dimensions: [], metrics: [], chartType: 'bar' diff --git a/packages/chart-advisor/src/__tests__/scorers/line.test.ts b/packages/chart-advisor/src/__tests__/scorers/line.test.ts index a0a0d566..91f23a14 100644 --- a/packages/chart-advisor/src/__tests__/scorers/line.test.ts +++ b/packages/chart-advisor/src/__tests__/scorers/line.test.ts @@ -6,17 +6,17 @@ describe('Line Scorer', () => { it('should return full score for valid line chart data', () => { const data = { - bars: [ - { value: 1 }, - { value: 2 }, - { value: 3 }, - { value: 4 }, - { value: 5 }, - { value: 6 }, - { value: 7 }, - { value: 8 }, - { value: 9 }, - { value: 10 } + data: [ + { metric1: 1 }, + { metric1: 2 }, + { metric1: 3 }, + { metric1: 4 }, + { metric1: 5 }, + { metric1: 6 }, + { metric1: 7 }, + { metric1: 8 }, + { metric1: 9 }, + { metric1: 10 } ], dimensions: ['dim1'], metrics: ['metric1'], @@ -30,7 +30,7 @@ describe('Line Scorer', () => { it('should return zero score for invalid bar count', () => { const data = { - bars: [], + data: [], dimensions: ['dim1'], metrics: ['metric1'], chartType: 'line' @@ -42,17 +42,17 @@ describe('Line Scorer', () => { it('should return zero score for missing dimensions/metrics', () => { const data = { - bars: [ - { value: 1 }, - { value: 2 }, - { value: 3 }, - { value: 4 }, - { value: 5 }, - { value: 6 }, - { value: 7 }, - { value: 8 }, - { value: 9 }, - { value: 10 } + data: [ + { metric1: 1 }, + { metric1: 2 }, + { metric1: 3 }, + { metric1: 4 }, + { metric1: 5 }, + { metric1: 6 }, + { metric1: 7 }, + { metric1: 8 }, + { metric1: 9 }, + { metric1: 10 } ], dimensions: [], metrics: [], diff --git a/packages/chart-advisor/src/__tests__/scorers/pie.test.ts b/packages/chart-advisor/src/__tests__/scorers/pie.test.ts index 93339429..ea872b25 100644 --- a/packages/chart-advisor/src/__tests__/scorers/pie.test.ts +++ b/packages/chart-advisor/src/__tests__/scorers/pie.test.ts @@ -6,7 +6,7 @@ describe('Pie Scorer', () => { it('should return full score for valid pie chart data', () => { const data = { - bars: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }], + data: [{ metric1: 1 }, { metric1: 2 }, { metric1: 3 }, { metric1: 4 }, { metric1: 5 }], dimensions: ['dim1'], metrics: ['metric1'], chartType: 'pie' @@ -19,7 +19,7 @@ describe('Pie Scorer', () => { it('should return zero score for invalid bar count', () => { const data = { - bars: [], + data: [], dimensions: ['dim1'], metrics: ['metric1'], chartType: 'pie' @@ -31,7 +31,7 @@ describe('Pie Scorer', () => { it('should return zero score for missing dimensions/metrics', () => { const data = { - bars: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }], + data: [{ metric1: 1 }, { metric1: 2 }, { metric1: 3 }, { metric1: 4 }, { metric1: 5 }], dimensions: [], metrics: [], chartType: 'pie' diff --git a/packages/chart-advisor/src/index.ts b/packages/chart-advisor/src/index.ts index b756189b..ca253ac6 100644 --- a/packages/chart-advisor/src/index.ts +++ b/packages/chart-advisor/src/index.ts @@ -1,21 +1,99 @@ -import { DimensionDataset, MeasureDataset, ChartType, ScreenSize, UserPurpose } from './type'; -import type { +import { + DimensionDataset, + MeasureDataset, + ChartType, + ScreenSize, + UserPurpose, AdviseResult, - Scorer, AdviserParams, - ScoreResult, DataTypeName, MeasureField, - DimensionField -} from './type'; -import { scorer as defaultScorer } from './score'; + DimensionField, + OldScorer, + ChartData, + OldScoreResult +} from './types'; + import * as dataUtils from './dataUtil'; import { isNil } from '@visactor/vutils'; import { isNaN } from './dataUtil'; +import { createBarScorer } from './scorers/bar'; +import { createLineScorer } from './scorers/line'; +import { createPieScorer } from './scorers/pie'; +import { defaultConfig } from './rules/config'; export { fold, omit } from './fieldUtils'; export { FOLD_NAME, FOLD_VALUE, COLOR_FIELD, FOLD_VALUE_MAIN, FOLD_VALUE_SUB, GROUP_FIELD } from './constant'; +function convertToChartData(params: any, chartType: string): ChartData { + const { inputDataSet, dimList, measureList, aliasMap = {} } = params; + + const dimensions = dimList.map((d: any) => aliasMap[d.uniqueID] ?? d.uniqueID); + const metrics = measureList.map((m: any) => aliasMap[m.uniqueID] ?? m.uniqueID); + + const data = inputDataSet.map((row: any) => { + const newRow: Record = {}; + for (const id in row) { + const name = aliasMap[id] ?? id; + newRow[name] = row[id]; + } + return newRow; + }); + + return { + data, + dimensions, + metrics, + chartType + }; +} + +const scorerAdapter: OldScorer = params => { + const { purpose } = params; + + const calBar = (): OldScoreResult => { + const chartData = convertToChartData(params, 'bar'); + const scoreResult = createBarScorer(defaultConfig)(chartData, defaultConfig); + return { + chartType: ChartType.BAR, + score: scoreResult.score, + originScore: scoreResult.score, + fullMark: 1, // Simplified fullMark + scoreDetails: scoreResult.details as any // To be compatible with OldScoreResult + }; + }; + + const calLine = (): OldScoreResult => { + const chartData = convertToChartData(params, 'line'); + const scoreResult = createLineScorer(defaultConfig)(chartData, defaultConfig); + return { + chartType: ChartType.LINE, + score: scoreResult.score, + originScore: scoreResult.score, + fullMark: 1, + scoreDetails: scoreResult.details as any + }; + }; + + const calPie = (): OldScoreResult => { + const chartData = convertToChartData(params, 'pie'); + const scoreResult = createPieScorer(defaultConfig)(chartData, defaultConfig); + return { + chartType: ChartType.PIE, + score: scoreResult.score, + originScore: scoreResult.score, + fullMark: 1, + scoreDetails: scoreResult.details as any + }; + }; + + // More chart type calculators can be added here following the same pattern. + + const scoreCalculators: (() => OldScoreResult)[] = [calBar, calLine, calPie]; + + return scoreCalculators; +}; + export function chartAdvisor(params: AdviserParams): AdviseResult { const { originDataset, @@ -26,7 +104,7 @@ export function chartAdvisor(params: AdviserParams): AdviseResult { maxPivotColumn = 0, purpose = UserPurpose.NONE, screen = ScreenSize.LARGE, - scorer = defaultScorer + scorer = scorerAdapter } = params; const measureDatasets: MeasureDataset[] = []; @@ -47,7 +125,7 @@ export function chartAdvisor(params: AdviserParams): AdviseResult { measureSet.data.push(parseFloat(row[uniqueID])); } }); - const dataNotNull = measureSet.data.filter(each => !isNil(each) && !isNaN(each)); + const dataNotNull = measureSet.data.filter(each => !isNil(each) && !isNaN(Number(each))); measureSet.min = Math.min(...dataNotNull); measureSet.max = Math.max(...dataNotNull); measureSet.mean = dataUtils.calMean(measureSet); @@ -95,35 +173,21 @@ export function chartAdvisor(params: AdviserParams): AdviseResult { return score; }); + if (scores.length === 0) { + return { + chartType: ChartType.TABLE, + scores: [] + }; + } + scores.sort((chart1, chart2) => chart2.score - chart1.score); - // console.log(scores) + if (scores[0].score === 0) { return { chartType: ChartType.TABLE, scores: [] }; } - // console.log(scores) - - // scores.forEach(score => { - // let cell = score.cell - // if (!Array.isArray(cell)) { - // cell = [cell] - // } - // //将所有的key转换为string - // cell.forEach(cl => { - // Object.entries(cl).forEach(([k, v]) => { - // if (k === 'cartesianInfo' || k === 'foldInfo') { - // cl[k] = null - // } - // else { - // cl[k] = v.map(value => String(value)) - // } - // }) - // }) - - // score.cell = cell - // }) return { chartType: scores[0].chartType, @@ -139,11 +203,10 @@ export function chartAdvisor(params: AdviserParams): AdviseResult { } export { - Scorer, + OldScorer as Scorer, AdviserParams, - ScoreResult, - ChartType, AdviseResult, + ChartType, DataTypeName, MeasureField, DimensionField, diff --git a/packages/chart-advisor/src/rules/common.ts b/packages/chart-advisor/src/rules/common.ts index c3b448ba..57871795 100644 --- a/packages/chart-advisor/src/rules/common.ts +++ b/packages/chart-advisor/src/rules/common.ts @@ -1,4 +1,4 @@ -import { Rule, ScoringConfig, ChartData } from '../types'; +import { Rule, ScoringConfig, ChartData } from '../types/index'; import { coefficientOfVariation } from '../utils/calculation'; // 数据量范围检查 @@ -6,11 +6,11 @@ export const dataRangeRule = (config: ScoringConfig): Rule => ({ name: 'dataRange', weight: config.weights.dataRange, check: (data: ChartData) => ({ - passed: Array.isArray(data.bars) - ? data.bars.length >= config.thresholds.minBarNumber && data.bars.length <= config.thresholds.maxBarNumber + passed: Array.isArray(data.data) + ? data.data.length >= config.thresholds.minBarNumber && data.data.length <= config.thresholds.maxBarNumber : false, score: 1, - details: `Bar count: ${Array.isArray(data.bars) ? data.bars.length : 0}` + details: `Data count: ${Array.isArray(data.data) ? data.data.length : 0}` }) }); @@ -30,9 +30,12 @@ export const dataDistributionRule = (config: ScoringConfig): Rule => ({ name: 'dataDistribution', weight: config.weights.dataDistribution, check: (data: ChartData) => { - // 假设 bars 中有 value 字段 - const values = Array.isArray(data.bars) - ? data.bars.map((d: any) => d.value).filter((v: any) => typeof v === 'number') + if (!data.metrics || data.metrics.length === 0) { + return { passed: false, score: 0, details: 'No metrics available for distribution check' }; + } + const metricField = data.metrics[0]; + const values = Array.isArray(data.data) + ? data.data.map((d: any) => d[metricField]).filter((v: any) => typeof v === 'number') : []; const coef = coefficientOfVariation(values); // 以 0.2 作为分布合理的阈值(可根据实际需求调整) diff --git a/packages/chart-advisor/src/types/index.ts b/packages/chart-advisor/src/types/index.ts index 6073d300..5cf799ed 100644 --- a/packages/chart-advisor/src/types/index.ts +++ b/packages/chart-advisor/src/types/index.ts @@ -1,42 +1,324 @@ -export interface RuleResult { - passed: boolean; - score: number; - details?: string; -} - +/** + * Represents a single scoring rule for chart recommendation. + * Each rule evaluates a specific aspect of the data and contributes to the overall score of a chart type. + */ export interface Rule { + /** + * The unique name of the rule. + */ name: string; + /** + * The weight of the rule's score in the final calculation. + */ weight: number; - check: (data: ChartData, config: ScoringConfig) => RuleResult; + /** + * The function that performs the check. + * @param data The chart data to be evaluated. + * @param config The scoring configuration. + * @returns An object containing the result of the check. + */ + check: ( + data: ChartData, + config: ScoringConfig + ) => { + /** + * Whether the data passes the rule's check. + */ + passed: boolean; + /** + * The score assigned by the rule, typically between 0 and 1. + */ + score: number; + /** + * Optional details or reasons for the score. + */ + details?: string; + }; } -export type ScorerFn = (data: ChartData, config: ScoringConfig) => ScoreResult; - -export interface ScoreResult { - score: number; - details: Record; - ruleResults: RuleResult[]; - chartType: string; +export interface ScorerFn { + (data: ChartData, config: ScoringConfig): ScoreResult; } -// 你可以根据实际数据结构补充 ChartData 类型 +/** + * Represents the input data structure for chart recommendation. + * This is a generic representation of 2D data. + */ export interface ChartData { - // 示例字段 - bars?: any[]; - [key: string]: any; + /** + * The dataset for chart generation, represented as an array of objects. + * @example [{ product: 'A', sales: 100 }, { product: 'B', sales: 150 }] + */ + data: Record[]; + /** + * An array of field names to be treated as dimensions. + */ + dimensions: string[]; + /** + * An array of field names to be treated as metrics. + */ + metrics: string[]; + /** + * The target chart type for evaluation. + */ + chartType: string; } export interface ScoringConfig { + weights: { + [key: string]: number; + }; thresholds: { - maxBarNumber: number; - minBarNumber: number; - maxDataRange: number; - minDataRatio: number; + [key: string]: number; }; - weights: { - dimensionCheck: number; - dataRange: number; - dataDistribution: number; - userPurpose: number; +} + +export interface ScoreResult { + score: number; + details: { + [key: string]: any; + }; + /** + * An array of results from each individual rule check. + * Useful for debugging or providing detailed feedback. + */ + ruleResults?: { passed: boolean; score: number; details?: string }[]; + /** + * The chart type for which the score was calculated. + */ + chartType?: string; +} + +// Old types, with necessary renames to avoid conflicts + +//屏幕尺寸:小、中、大 +export enum ScreenSize { + LARGE = 0, + MEDIUM = 1, + SMALL = 2 +} + +//用户目的:对比、趋势、分布、排名、占比、组成、StoryTelling +export enum UserPurpose { + NONE = 0, //未指定目的 + COMPARISON = 1, + TREND = 2, + DISTRIBUTION = 3, + RANK = 4, + PROPORTION = 5, + COMPOSITION = 6, + STORYTELLING = 7 +} + +export interface DimensionField { + uniqueId: number; //该字段的id + type: DataTypeName; //该字段的类型 + isGeoField?: boolean; +} + +export interface MeasureField { + uniqueId: number; +} + +//measure数据集 +export interface MeasureDataset { + uniqueID?: number; + data: number[]; + min?: number; + max?: number; + mean?: number; //平均值 + standardDev?: number; //标准差 + coefficient?: number; //变异系数 + Q1?: number; //下四分位数 +} + +//dimension数据集 +export interface DimensionDataset { + uniqueID?: number; + data: string[]; + dataType?: DataTypeName; + dimensionName?: string; //字段名 + cardinal?: number; //基数(不同值的数量) + ratio?: number; //基数除以数据条数 + isGeoField?: boolean; // 是否为地理字段 +} + +export interface AutoChartCell { + x: UniqueId[]; + y: UniqueId[]; + row: UniqueId[]; //作为透视行的字段 + column: UniqueId[]; //作为透视列的字段 + color?: UniqueId[]; + size?: UniqueId[]; + angle?: UniqueId[]; + value?: UniqueId[]; + text?: UniqueId[]; + group?: UniqueId[]; + error?: boolean; + errMsg?: string; + // 维度展开的信息(笛卡尔积) + cartesianInfo?: CartesianInfo; + // 指标展开的信息(指标平坦化) + foldInfo?: FoldInfo; +} + +export interface CartesianInfo { + key: UniqueId; + fieldList: UniqueId[]; +} + +export interface FoldInfo { + key: UniqueId; + value: UniqueId; + foldMap: { + [key: number]: string; }; } + +export interface FieldTypeMap { + [key: number]: DataTypeName; +} + +export type DataItem = { [key: number]: string }; + +export type Dataset = DataItem[]; + +export type Datasets = DataItem[][][][] | Dataset; + +export type UniqueId = number | string; + +export type DataTypeName = 'number' | 'string' | 'date'; + +/** + * vqs 接口中 visData 中的 aliasMap + * 做字段名字映射 + */ +export type AliasMap = { + [key: number]: string; +}; + +/** + * 图表类型枚举 + */ +export enum ChartType { + /** 表格 */ + TABLE = 'table', + /** 明细表 */ + RAW_TABLE = 'raw_table', + /** 透视表 */ + PIVOT_TABLE = 'pivot_table', + + /** 柱状图 */ + COLUMN = 'column', + /** 百分比柱状图 */ + COLUMN_PERCENT = 'column_percent', + /** 并列柱状图 */ + COLUMN_PARALLEL = 'column_parallel', + /** 条形图 */ + BAR = 'bar', + /** 百分比条形图 */ + BAR_PERCENT = 'bar_percent', + /** 并列条形图 */ + BAR_PARALLEL = 'bar_parallel', + + /** 折线图 */ + LINE = 'line', + /** 面积图 */ + AREA = 'area', + /** 百分比面积图 */ + AREA_PERCENT = 'area_percent', + + /** 饼图 */ + PIE = 'pie', + /** 环形饼图 */ + ANNULAR = 'annular', + /** 南丁格尔玫瑰图 */ + ROSE = 'rose', + + /** 散点图 */ + SCATTER = 'scatter', + /** 圆视图 */ + CIRCLE_VIEWS = 'circle_views', + + /** 双轴图 */ + DUAL_AXIS = 'double_axis', + /** 双向条形图 */ + BILATERAL = 'bilateral', + /** 组合图 */ + COMBINATION = 'combination', + + /** 填充地图 */ + MAP = 'map', + /** 标记地图 */ + SCATTER_MAP = 'scatter_map', + + /** 指标卡 */ + MEASURE_CARD = 'measure_card', + /** 对比指标卡 */ + COMPARATIVE_MEASURE_CARD = 'comparative_measure_card', + /** 词云 */ + WORD_CLOUD = 'word_cloud', + /** 直方图 */ + HISTOGRAM = 'histogram', + /** 漏斗图 */ + FUNNEL = 'funnel', + /** 雷达图 */ + RADAR = 'radar', + /** 桑基图 */ + SANKEY = 'sankey', + + /** 扩展自定义类型 */ + EXTEND = 'extend', + /** 单值百分比环形图 */ + PROGRESS = 'progress' +} + +export interface OldScoreResult { + chartType: ChartType; + originScore: number; + fullMark: number; + score: number; + scoreDetails: Array<{ name: string; score: number }>; + cell?: AutoChartCell | AutoChartCell[]; + dataset?: Datasets; + error?: any; +} + +export interface PivotTree { + field: UniqueId; + values: { + value: string; + child: PivotTree | null; + }[]; +} + +export interface AdviseResult { + chartType: ChartType; //vizData中的chartType + scores: OldScoreResult[]; + error?: any; +} + +interface ScorerParams { + inputDataSet: Dataset; + dimList: DimensionDataset[]; + measureList: MeasureDataset[]; + aliasMap?: AliasMap; + maxRowNum?: number; + maxColNum?: number; + purpose?: UserPurpose; + screen?: ScreenSize; +} + +export type OldScorer = (params: ScorerParams) => Array<() => OldScoreResult>; + +export interface AdviserParams { + originDataset: Dataset; + dimensionList: DimensionField[]; + measureList: MeasureField[]; + aliasMap?: AliasMap; + maxPivotRow?: number; + maxPivotColumn?: number; + purpose?: UserPurpose; + screen?: ScreenSize; + scorer?: OldScorer; +}