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
47 changes: 47 additions & 0 deletions .trae/rules/project-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# VChart 项目规则 for TRAE

为了确保在 VChart Monorepo 环境中高效、正确地执行任务,你(TRAE 智能体)必须遵循以下规则。

## 核心规则

1. **【禁止】直接运行通用测试命令**
- **禁止**在仓库的**任何目录**(尤其是根目录)下,直接执行 `npm test`, `pnpm test`, `yarn test`, `npx jest`, `npm run test` 等通用测试命令。这些命令会因缺少 Monorepo 上下文而失败。

2. **【必须】使用 Rush 执行测试**
- 如需执行测试,**必须**使用 `rush` 命令,并在指定包的上下文中进行。
- **标准命令格式**: `rush run -p <package-name> -s test`
- **示例**: `rush run -p @visactor/vchart -s test`
- **测试特定文件**: 如需测试单个文件,**必须**使用 `--` 将文件路径作为参数传递。
- **示例**: `rush run -p @visactor/vchart -s test -- __tests__/unit/some.test.ts`

3. **【应该】在明确指示时才运行测试**
- **默认禁止**自动运行任何测试。只在我明确要求“运行测试”、“验证代码”或类似指令时,才执行测试流程。

4. **【必须】使用 Rush 管理依赖与构建**
- **依赖安装**: **必须**只使用 `rush install` 命令。
- **项目构建**: 如需构建,**必须**使用 `rush build`。可以配合 `-p <package-name>` 参数指定构建目标。

5. **【应该】在不确定时提问**
- 如果不确定**包名** (`@visactor/vchart` 是否正确)、**测试文件路径**、或**是否需要构建**,**应该**先向我提问确认,而不是自行猜测。

6. **【必须】提供详细的失败报告**
- 如果任何命令执行失败,**必须**向我汇报以下信息:
- **Node.js 版本**: (`node -v` 的输出)
- **执行的完整命令**: (例如: `rush run -p @visactor/vchart ...`)
- **执行目录**: (确认是在项目根目录)
- **退出码 (Exit Code)**: (如果可用)
- **关键日志片段**: (包含 `ERROR`, `FAIL` 或堆栈跟踪的核心部分)

## Auto Run 黑名单建议

为了防止智能体在 `SOLO` 或 `Builder` 模式下自动执行错误的测试命令,建议项目管理员或用户在 TRAE 的设置中采取以下措施:

- **选项一 (推荐)**: 在智能体的 "Auto Run" 配置中,将以下命令前缀**加入黑名单**:
- `npm test`
- `pnpm test`
- `yarn test`
- `npx jest`

- **选项二**: 直接**关闭 "Auto Run" 功能**,所有由 AI 生成的命令都需要用户手动点击“运行”来确认。

采纳这些建议可以有效避免因环境不匹配导致的自动测试失败,提升协作效率。
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- [ ] `computeLinearDomain` 中优先判断 `customDistribution`
- [ ] `computeLinearDomain` 中正确将 `breaks` 转换为 `customDistribution` 格式
- [ ] `getNewScaleRange` 中移除 `breaks` 逻辑
- [ ] 验证仅配置 `breaks` 时,坐标轴断点效果正常
- [ ] 验证同时配置 `breaks` 和 `customDistribution` 时,`breaks` 不生效
40 changes: 40 additions & 0 deletions .trae/specs/refactor-axis-breaks-to-custom-distribution/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 优化坐标轴 Breaks 和 CustomDistribution 逻辑

## Why
目前的 `breaks` 和 `customDistribution` 是两套独立的逻辑,虽然都用于控制坐标轴的分布,但缺乏统一性。`customDistribution` 功能更通用,应该能够包含 `breaks` 的场景。通过将 `breaks` 转化为 `customDistribution`,可以简化代码逻辑,并明确两者的优先级关系。

## What Changes
1. **优先级调整**:明确 `customDistribution` 的优先级高于 `breaks`。当用户配置了 `customDistribution` 时,忽略 `breaks` 配置。
2. **统一实现**:不再独立处理 `breaks` 的逻辑,而是将其转换为 `customDistribution` 的配置(`domain` 和 `ratio`),复用 `customDistribution` 的处理流程。
3. **代码重构**:
- 在 `linear-axis-mixin.ts` 的 `computeLinearDomain` 方法中,如果存在 `breaks` 且无 `customDistribution`,则调用 `breakData` 计算分段,并将结果转换为 `customDistribution` 格式。
- 在 `linear-axis.ts` 的 `getNewScaleRange` 方法中,移除独立的 `breaks` 处理逻辑,完全依赖 `customDistribution`。

## Impact
- **Affected Specs**: `IAxis` 接口虽然不变,但在运行时 `breaks` 的表现将通过 `customDistribution` 实现。
- **Affected Code**:
- `packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts`
- `packages/vchart/src/component/axis/cartesian/linear-axis.ts`
- **Breaking Changes**: 无。对于仅使用 `breaks` 的用户,表现应保持一致。对于同时配置了 `breaks` 和 `customDistribution` 的用户,现在明确以 `customDistribution` 为准(此前行为可能未定义或混合)。

## ADDED Requirements
### Requirement: CustomDistribution Priority
系统 SHALL 优先使用 `customDistribution` 配置。

#### Scenario: Both Configured
- **WHEN** 用户同时配置了 `breaks` 和 `customDistribution`
- **THEN** 系统忽略 `breaks` 配置,仅应用 `customDistribution`。

### Requirement: Breaks as CustomDistribution
系统 SHALL 将 `breaks` 配置转换为 `customDistribution` 配置。

#### Scenario: Only Breaks Configured
- **WHEN** 用户仅配置了 `breaks`
- **THEN** 系统计算断点分段,生成对应的 `domain` 和 `ratio`,并应用 `customDistribution` 逻辑。

## MODIFIED Requirements
### Requirement: Linear Domain Computation
修改 `computeLinearDomain` 方法,整合 `breaks` 到 `customDistribution` 的转换逻辑。

### Requirement: Scale Range Computation
修改 `getNewScaleRange` 方法,移除对 `breaks` 的独立处理。
11 changes: 11 additions & 0 deletions .trae/specs/refactor-axis-breaks-to-custom-distribution/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Tasks
- [ ] Task 1: 重构 `linear-axis-mixin.ts` 中的 `computeLinearDomain` 方法
- [ ] SubTask 1.1: 优先判断 `customDistribution`,若存在则跳过 `breaks` 处理
- [ ] SubTask 1.2: 仅在无 `customDistribution` 且有 `breaks` 时,计算断点数据
- [ ] SubTask 1.3: 将 `breaks` 的计算结果(`breakDomains` 和 `breakScopes`)转换为 `customDistribution` 的 `domain` 和 `ratio`
- [ ] Task 2: 重构 `linear-axis.ts` 中的 `getNewScaleRange` 方法
- [ ] SubTask 2.1: 移除 `getNewScaleRange` 中对 `breaks` 的独立判断逻辑
- [ ] SubTask 2.2: 确保 `customDistribution` 逻辑能正确处理从 `breaks` 转换来的配置(包含间隙)
- [ ] Task 3: 验证修改
- [ ] SubTask 3.1: 验证 `breaks` 独立配置是否正常工作
- [ ] SubTask 3.2: 验证 `customDistribution` 优先级是否高于 `breaks`
11 changes: 11 additions & 0 deletions common/changes/@visactor/vchart/feat-area-enlargement.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@visactor/vchart",
"comment": "feat: support area enlargement (linear axis custom distribution)",
"type": "minor"
}
],
"packageName": "@visactor/vchart",
"email": "lixuef1313@163.com"
}
39 changes: 39 additions & 0 deletions docs/assets/demos/builtin-theme/charts/area-enlargement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IChartInfo } from './interface';

const spec = {
type: 'line',
data: [
{
id: 'line',
values: [
{ x: '1', y: 1 },
{ x: '2', y: 5 },
{ x: '3', y: 7 },
{ x: '4', y: 8 },
{ x: '5', y: 8.5 },
{ x: '6', y: 9 },
{ x: '7', y: 9.5 },
{ x: '8', y: 10 }
]
}
],
xField: 'x',
yField: 'y',
axes: [
{
orient: 'left',
type: 'linear',
customDistribution: {
domain: [0, 7, 9, 10],
ratio: [0.2, 0.6, 0.2]
}
}
]
};

const areaEnlargement: IChartInfo = {
name: 'Area Enlargement',
spec
};

export default areaEnlargement;
14 changes: 14 additions & 0 deletions docs/assets/option/en/component/axis-common/linear-axis.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,17 @@ Truncation graphic rotation angle configuration.
###${prefix} style(Object)

The style configuration of the truncation graphic, you can configure the line width (`lineWidth`), color (`stroke`), etc.

#${prefix} customDistribution(Array)

Supported since version **2.0.16**

Applies only when the axis is a linear axis. Custom interval distribution configuration, used to define the visual proportion of specific data intervals on the axis.

##${prefix} domain(number[])

The data interval [min, max].

##${prefix} ratio(number)

The proportion of the visual range this interval should occupy, value between 0 and 1.
14 changes: 14 additions & 0 deletions docs/assets/option/zh/component/axis-common/linear-axis.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,17 @@
###${prefix} style(Object)

截断图形的样式配置,可以配置线宽(`lineWidth`)、颜色(`stroke`)等。

#${prefix} customDistribution(Array)

自**2.0.16**版本开始支持

仅当轴为线性轴时生效。自定义区间分布配置,用于定义特定数据区间在轴上的视觉占比。

##${prefix} domain(number[])

数据区间 [min, max]。

##${prefix} ratio(number)

该区间在视觉范围内所占的比例,取值范围 0 到 1。
Original file line number Diff line number Diff line change
Expand Up @@ -1506,16 +1506,16 @@ describe('VChart', () => {

expect(la.getScale().domain()).toEqual([-5000, 60, 80, 30000]);
const range = la.getScale().range();
expect(range[0]).toBeCloseTo(426);
expect(range[1]).toBeCloseTo(423.8873352657334);
expect(range[2]).toBeCloseTo(423.58867525684406);
expect(range[0]).toBeCloseTo(428);
expect(range[1]).toBeCloseTo(362.3141916563702);
expect(range[2]).toBeCloseTo(362.1041481383814);
expect(range[3]).toBeCloseTo(0);

const rightDomain = ra.getScale().domain();

expect(rightDomain[0]).toBeCloseTo(-393970724.0726612);
expect(rightDomain[0]).toBeCloseTo(-14300889717.332338);
expect(rightDomain[1]).toBeCloseTo(80000000000);
expect(ra.getScale().range()).toEqual([426, 0]);
expect(ra.getScale().range()).toEqual([428, 0]);
});

it('change domain with axis sync and scopeType = "count"', async () => {
Expand Down Expand Up @@ -3005,13 +3005,13 @@ describe('VChart', () => {
expect(la.getScale().domain()).toEqual([-5000, 60, 80, 30000]);
const range = la.getScale().range();
expect(range[0]).toBeCloseTo(428);
expect(range[1]).toBeCloseTo(145.51999999999998);
expect(range[2]).toBeCloseTo(132.68);
expect(range[1]).toBeCloseTo(166.06400000000002);
expect(range[2]).toBeCloseTo(157.07600000000005);
expect(range[3]).toBeCloseTo(0);

const rightDomain = ra.getScale().domain();

expect(rightDomain[0]).toBeCloseTo(-150000000000);
expect(rightDomain[0]).toBeCloseTo(-122399999999.99997);
expect(rightDomain[1]).toBeCloseTo(80000000000);
expect(ra.getScale().range()).toEqual([428, 0]);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { GlobalScale } from '../../../../../src/scale/global-scale';
import { DataSet, csvParser } from '@visactor/vdataset';
import { dimensionStatistics } from '../../../../../src/data/transforms/dimension-statistics';
import type { CartesianLinearAxis } from '../../../../../src/index';
// eslint-disable-next-line no-duplicate-imports
import { CartesianAxis } from '../../../../../src/index';
import { ComponentTypeEnum, type IComponent, type IComponentOption } from '../../../../../src/component/interface';
import { EventDispatcher } from '../../../../../src/event/event-dispatcher';
import { getTestCompiler } from '../../../../util/factory/compiler';
import { getTheme, initChartDataSet } from '../../../../util/context';
import { getCartesianAxisInfo } from '../../../../../src/component/axis/cartesian/util';

const dataSet = new DataSet();
initChartDataSet(dataSet);
dataSet.registerParser('csv', csvParser);
dataSet.registerTransform('dimensionStatistics', dimensionStatistics);

const ctx: IComponentOption = {
type: ComponentTypeEnum.cartesianLinearAxis,
eventDispatcher: new EventDispatcher({} as any, { addEventListener: () => {} } as any) as any,
dataSet,
map: new Map(),
mode: 'desktop-browser',
globalInstance: {
isAnimationEnable: () => true,
getContainer: () => ({}),
getTooltipHandlerByUser: (() => undefined) as () => undefined
} as any,
getCompiler: getTestCompiler,
getAllRegions: () => [],
getRegionsInIndex: () => [],
getChart: () => ({ getSpec: () => ({}) } as any),
getRegionsInIds: () => [],
getRegionsInUserIdOrIndex: () => [],
getAllSeries: () => [],
getSeriesInIndex: () => [],
getSeriesInIds: () => [],
getSeriesInUserIdOrIndex: () => [],
getAllComponents: () => [],
getComponentByIndex: () => undefined,
getComponentsByKey: () => [],
getComponentsByType: () => [],
getChartLayoutRect: () => ({ width: 0, height: 0, x: 0, y: 0 }),
getChartViewRect: () => ({ width: 500, height: 500 } as any),
globalScale: new GlobalScale([], { getAllSeries: () => [] as any[] } as any),
getTheme: getTheme,
getComponentByUserId: () => undefined,
animation: false,
onError: () => {},
getSeriesData: () => undefined
};

const getAxisSpec = (spec: any) => ({
sampling: 'simple',
...spec
});

describe('LinearAxis piecewise', () => {
beforeAll(() => {
// @ts-ignore
jest.spyOn(CartesianAxis.prototype, 'collectData').mockImplementation(() => {
return [{ min: 0, max: 10 }];
});
});

test('should create piecewise domain and range from piecewise', () => {
// Mock getNewScaleRange to return [0, 100]
// @ts-ignore
jest.spyOn(CartesianAxis.prototype, 'getNewScaleRange').mockReturnValue([0, 100]);

let spec = getAxisSpec({
orient: 'left',
piecewise: {
domain: [0, 5, 10],
ratio: [0.8, 0.2]
}
});

const transformer = new CartesianAxis.transformerConstructor({
type: 'cartesianAxis-linear',
getTheme: getTheme,
mode: 'desktop-browser'
});
spec = transformer.transformSpec(spec, {}).spec;
const linearAxis = CartesianAxis.createComponent(
{
type: getCartesianAxisInfo(spec).componentName,
spec
},
ctx
) as CartesianLinearAxis;

linearAxis.created();
linearAxis.init({});

// Test Domain
// @ts-ignore
linearAxis.updateScaleDomain();
const scale = linearAxis.getScale();
expect(scale.domain()).toEqual([0, 5, 10]);

// Test Range
// @ts-ignore
const newRange = linearAxis.getNewScaleRange();
// 0 -> 0
// 5 -> 0 + 0.8 * 100 = 80
// 10 -> 80 + 0.2 * 100 = 100
expect(newRange).toEqual([0, 80, 100]);
});

test('should handle gaps in piecewise', () => {
let spec = getAxisSpec({
orient: 'left',
piecewise: {
domain: [0, 5, 8, 10],
ratio: [0.4, 0.4]
}
});

const transformer = new CartesianAxis.transformerConstructor({
type: 'cartesianAxis-linear',
getTheme: getTheme,
mode: 'desktop-browser'
});
spec = transformer.transformSpec(spec, {}).spec;
const linearAxis = CartesianAxis.createComponent(
{
type: getCartesianAxisInfo(spec).componentName,
spec
},
ctx
) as CartesianLinearAxis;

linearAxis.created();
linearAxis.init({});

// @ts-ignore
linearAxis.updateScaleDomain();
const scale = linearAxis.getScale();
expect(scale.domain()).toEqual([0, 5, 8, 10]);
});
});
Loading
Loading