-
Notifications
You must be signed in to change notification settings - Fork 84
refactor(chart-advisor): refactor whole chart advisor module #260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' | ||
| }) | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, any>; | ||
| 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; | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里为啥改了scorer 的输入类型?