Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 = {
data: [{ metric1: 1 }, { metric1: 2 }, { metric1: 3 }, { metric1: 4 }, { metric1: 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 = {
data: [],
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 = {
data: [{ metric1: 1 }, { metric1: 2 }, { metric1: 3 }, { metric1: 4 }, { metric1: 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 = {
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'],
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 = {
data: [],
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 = {
data: [
{ metric1: 1 },
{ metric1: 2 },
{ metric1: 3 },
{ metric1: 4 },
{ metric1: 5 },
{ metric1: 6 },
{ metric1: 7 },
{ metric1: 8 },
{ metric1: 9 },
{ metric1: 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 = {
data: [{ metric1: 1 }, { metric1: 2 }, { metric1: 3 }, { metric1: 4 }, { metric1: 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 = {
data: [],
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 = {
data: [{ metric1: 1 }, { metric1: 2 }, { metric1: 3 }, { metric1: 4 }, { metric1: 5 }],
dimensions: [],
metrics: [],
chartType: 'pie'
};
const result = scorer(data, defaultConfig);
expect(result.score).toBeLessThan(1);
expect(result.details.dimensionMetric).toBe(false);
});
});
131 changes: 97 additions & 34 deletions packages/chart-advisor/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> = {};
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,
Expand All @@ -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[] = [];
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -139,11 +203,10 @@ export function chartAdvisor(params: AdviserParams): AdviseResult {
}

export {
Scorer,
OldScorer as Scorer,
AdviserParams,
ScoreResult,
ChartType,
AdviseResult,
ChartType,
DataTypeName,
MeasureField,
DimensionField,
Expand Down
60 changes: 60 additions & 0 deletions packages/chart-advisor/src/rules/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Rule, ScoringConfig, ChartData } from '../types/index';
import { coefficientOfVariation } from '../utils/calculation';

// 数据量范围检查
export const dataRangeRule = (config: ScoringConfig): Rule => ({
name: 'dataRange',
weight: config.weights.dataRange,
check: (data: ChartData) => ({
passed: Array.isArray(data.data)
? data.data.length >= config.thresholds.minBarNumber && data.data.length <= config.thresholds.maxBarNumber
: false,
score: 1,
details: `Data count: ${Array.isArray(data.data) ? data.data.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) => {
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 作为分布合理的阈值(可根据实际需求调整)
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'
})
});
Loading
Loading