Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/chart-advisor/src/__tests__/scorers/bar.test.ts
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 }],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

这里为啥改了scorer 的输入类型?

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);
});
});
65 changes: 65 additions & 0 deletions packages/chart-advisor/src/__tests__/scorers/line.test.ts
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);
});
});
43 changes: 43 additions & 0 deletions packages/chart-advisor/src/__tests__/scorers/pie.test.ts
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);
});
});
57 changes: 57 additions & 0 deletions packages/chart-advisor/src/rules/common.ts
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'
})
});
28 changes: 28 additions & 0 deletions packages/chart-advisor/src/rules/compose.ts
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'
};
};
16 changes: 16 additions & 0 deletions packages/chart-advisor/src/rules/config.ts
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
}
};
29 changes: 29 additions & 0 deletions packages/chart-advisor/src/scorers/bar.ts
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);
};
29 changes: 29 additions & 0 deletions packages/chart-advisor/src/scorers/line.ts
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);
};
29 changes: 29 additions & 0 deletions packages/chart-advisor/src/scorers/pie.ts
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);
};
42 changes: 42 additions & 0 deletions packages/chart-advisor/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export interface RuleResult {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

类型定义补充一下具体的注释

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[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为什么要限制有 bars 这个字段,我们的数据可能是任意的二维表

[key: string]: any;
}

export interface ScoringConfig {
thresholds: {
maxBarNumber: number;
minBarNumber: number;
maxDataRange: number;
minDataRatio: number;
};
weights: {
dimensionCheck: number;
dataRange: number;
dataDistribution: number;
userPurpose: number;
};
}
Loading