Skip to content

Conversation

@xuefei1313
Copy link
Contributor

🤔 这个分支是...

  • 新功能
  • Bug fix
  • Ts 类型更新
  • 打包优化
  • 性能优化
  • 功能增强
  • 重构
  • 依赖版本更新
  • 代码优化
  • 测试 case 更新
  • 分支合并
  • 发布
  • 网站/文档更新
  • demo 更新
  • Workflow
  • 其他 (具体是什么,请补充?)

🔗 相关 issue 链接

close #4413

🔗 相关的 PR 链接

🐞 Bugserver 用例 id

💡 问题的背景&解决方案

支持线性轴的区域放大(Area Enlargement)功能。
通过在 linear 轴配置中增加 customDistribution 属性,允许用户自定义 domain 区间在 range 上的分布比例,从而实现对特定数据区间的视觉放大。

使用示例:

axes: [
  {
    orient: 'left',
    type: 'linear',
    customDistribution: [
      { domain: [0, 7], ratio: 0.2 },
      { domain: [7, 9], ratio: 0.6 }, // 重点放大
      { domain: [9, 10], ratio: 0.2 }
    ]
  }
];

📝 Changelog

Language Changelog
🇺🇸 English feat: support area enlargement (linear axis custom distribution)
🇨🇳 Chinese feat: 支持线性轴区域放大(自定义区间分布)

☑️ 自测

⚠️ 在提交 PR 之前,请检查一下内容. ⚠️

  • 文档提供了,或者更新,或者不需要
  • Demo 提供了,或者更新,或者不需要
  • Ts 类型定义提供了,或者更新,或者不需要
  • Changelog 提供了,或者不需要

🚀 Summary

copilot:summary

🔍 Walkthrough

copilot:walkthrough

@xuefei1313 xuefei1313 force-pushed the 003-area-enlargement branch 2 times, most recently from d443a1f to eee1aa3 Compare February 2, 2026 08:20

export interface IIntervalRatio {
domain: [number, number];
ratio: number;
Copy link
Contributor

Choose a reason for hiding this comment

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

ratio 用在哪里了?好像没看到

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements “area enlargement” for linear axes by allowing users to specify a custom distribution of domain intervals onto the axis range, so important value ranges (like 7–9) can visually occupy more space while preserving global context.

Changes:

  • Add the IIntervalRatio type and customDistribution option to the linear axis spec and public documentation.
  • Implement customDistribution handling in LinearAxisMixin.computeLinearDomain and CartesianLinearAxis.getNewScaleRange to build piecewise domains/ranges from user-defined intervals and ratios.
  • Add unit tests, internal specs/plan/checklists, and a demo chart illustrating area enlargement, plus a changefile entry for @visactor/vchart.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
specs/003-area-enlargement/spec.md Defines the feature’s user stories, requirements, and success criteria for area-enlargement on linear axes.
specs/003-area-enlargement/plan.md Records the implementation plan to reuse LinearScale with axis-level customDistribution instead of a new scale type.
specs/003-area-enlargement/research.md Captures earlier research and design decisions (now partially superseded) around a dedicated LinearIntervalScale.
specs/003-area-enlargement/tasks.md Lists implementation tasks (type additions, axis logic, tests, docs) for the area-enlargement feature.
specs/003-area-enlargement/data-model.md Documents the data model for customDistribution and IIntervalRatio with example configuration.
specs/003-area-enlargement/quickstart.md Provides an internal quickstart example showing how to configure customDistribution on a linear axis.
specs/003-area-enlargement/checklists/requirements.md Adds a requirements checklist asserting spec quality and readiness for planning.
packages/vchart/src/typings/scale.ts Introduces the IIntervalRatio interface for interval [domain, ratio] pairs used by linear axes.
packages/vchart/src/component/axis/interface/spec.ts Extends ILinearAxisSpec with the customDistribution?: IIntervalRatio[] configuration.
packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts Updates computeLinearDomain and niceDomain to construct a piecewise domain from customDistribution and to disable nicifying when this option is used.
packages/vchart/src/component/axis/cartesian/linear-axis.ts Extends getNewScaleRange to compute a piecewise range array according to customDistribution ratios, including handling of uncovered domain gaps.
packages/vchart/tests/unit/component/cartesian/axis/linear-axis-distribution.test.ts Adds unit tests verifying that customDistribution produces the expected piecewise domain and basic range behavior.
docs/assets/option/zh/component/axis-common/linear-axis.md Documents the new customDistribution option and its domain/ratio fields in the Chinese linear-axis API docs.
docs/assets/option/en/component/axis-common/linear-axis.md Documents the new customDistribution option and its domain/ratio fields in the English linear-axis API docs.
docs/assets/demos/builtin-theme/charts/area-enlargement.ts Adds a demo line chart that uses customDistribution to visually emphasize the [7, 9] range on a linear Y-axis.
common/changes/@visactor/vchart/feat-area-enlargement.json Registers the changefile entry announcing the new area-enlargement feature as a minor release for @visactor/vchart.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +111 to +141
test('should handle gaps in customDistribution', () => {
let spec = getAxisSpec({
orient: 'left',
customDistribution: [
{ domain: [0, 5], ratio: 0.4 },
// Gap 5-8
{ domain: [8, 10], ratio: 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]);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The "should handle gaps in customDistribution" test verifies that the computed domain includes the gap boundary ([0, 5, 8, 10]), but it does not assert anything about the resulting range, so the gap allocation logic in getNewScaleRange (which is relatively complex) is effectively untested here. To strengthen coverage of the new behavior, consider extending this test (or adding a new one) to stub the base getNewScaleRange and assert the expected piecewise range values for a configuration with a gap, similar to how the first test asserts the range for the no-gap case.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +24
- [ ] T002 Ensure `IIntervalRatio` is defined in `packages/vchart/src/typings/scale.ts` and `customDistribution` in `packages/vchart/src/component/axis/interface/spec.ts`.

## Phase 2: Foundational (Axis Logic)

- [ ] T003 Implement `computeLinearDomain` update in `packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts` to handle `customDistribution` (construct piecewise domain).
- [ ] T004 Implement `getNewScaleRange` update in `packages/vchart/src/component/axis/cartesian/linear-axis.ts` to handle `customDistribution` (construct piecewise range based on ratios).
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The implementation tasks T002–T004 (defining IIntervalRatio, adding customDistribution to the axis spec, and updating computeLinearDomain/getNewScaleRange) appear to be completed in this PR, but the checklist still shows them as unchecked. To keep the task tracking document reliable, consider marking the finished tasks as done (and optionally adding any follow-up items, such as additional tests or docs) so that it reflects the current implementation status.

Suggested change
- [ ] T002 Ensure `IIntervalRatio` is defined in `packages/vchart/src/typings/scale.ts` and `customDistribution` in `packages/vchart/src/component/axis/interface/spec.ts`.
## Phase 2: Foundational (Axis Logic)
- [ ] T003 Implement `computeLinearDomain` update in `packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts` to handle `customDistribution` (construct piecewise domain).
- [ ] T004 Implement `getNewScaleRange` update in `packages/vchart/src/component/axis/cartesian/linear-axis.ts` to handle `customDistribution` (construct piecewise range based on ratios).
- [x] T002 Ensure `IIntervalRatio` is defined in `packages/vchart/src/typings/scale.ts` and `customDistribution` in `packages/vchart/src/component/axis/interface/spec.ts`.
## Phase 2: Foundational (Axis Logic)
- [x] T003 Implement `computeLinearDomain` update in `packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts` to handle `customDistribution` (construct piecewise domain).
- [x] T004 Implement `getNewScaleRange` update in `packages/vchart/src/component/axis/cartesian/linear-axis.ts` to handle `customDistribution` (construct piecewise range based on ratios).

Copilot uses AI. Check for mistakes.
{
orient: 'left',
type: 'linear',
domain: [0, 10], // Optional if customDistribution covers the data
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

This quickstart example configures the linear axis with a top-level domain: [0, 10], but the actual ILinearAxisSpec API only exposes min/max (and the deprecated nested range) rather than a domain array. To avoid confusing users and to keep the sample aligned with the real API, consider switching this example to use min and max (or clarifying that domain is just shorthand/pseudocode if you intend something else).

Suggested change
domain: [0, 10], // Optional if customDistribution covers the data
min: 0,
max: 10, // Optional if customDistribution covers the data

Copilot uses AI. Check for mistakes.
Comment on lines 157 to 167
// handle customDistribution
if ((this._spec as any).customDistribution?.length) {
const customDistribution = (this._spec as any).customDistribution;
const domainSet = new Set<number>();
customDistribution.forEach((item: any) => {
domainSet.add(item.domain[0]);
domainSet.add(item.domain[1]);
});
domain = Array.from(domainSet).sort((a, b) => a - b);
return domain;
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

Because this customDistribution branch returns early, any configured breaks on the same axis are effectively ignored: the code below that computes breakRanges and populates this._break never runs, so the break-specific range transformation in CartesianLinearAxis.getNewScaleRange is skipped. If breaks and customDistribution are meant to be mutually exclusive, it would be safer to validate and surface that conflict (or document it explicitly); otherwise, consider composing the two features instead of returning before the break-handling logic.

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +150
// Calculate total defined ratio and identify gaps
let totalDefinedRatio = 0;
customDistribution.forEach(item => (totalDefinedRatio += item.ratio));
const remainingRatio = 1 - totalDefinedRatio;
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The new getNewScaleRange logic assumes that the sum of all ratio values in customDistribution is well-formed (typically ≤ 1) and does not normalize or validate these values. If a user accidentally specifies ratios that sum to more than 1, remainingRatio becomes negative and the computed gap segments will be assigned negative visual span, which will distort the range mapping in a non-obvious way. It would be safer to either validate and reject/clip invalid configurations, or normalize the ratios so that the behavior is predictable even when the user provides inconsistent values.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +61
- **LinearIntervalScale**: A new or extended scale class that handles the piecewise linear mapping.
- **ScaleSpec**: The configuration interface extending the standard linear scale spec.
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The spec currently introduces LinearIntervalScale as a new scale class and describes extending the scale spec, but the actual implementation and the plan/tasks for this PR have switched to using customDistribution on the existing linear axis (without a new scale type). To avoid confusing future readers, consider updating this "Key Entities" section (or adding a note) so that it reflects the current design choice of reusing LinearScale with axis-level piecewise domain/range configuration instead of a dedicated LinearIntervalScale class.

Suggested change
- **LinearIntervalScale**: A new or extended scale class that handles the piecewise linear mapping.
- **ScaleSpec**: The configuration interface extending the standard linear scale spec.
- **LinearScale with `customDistribution`**: Reuses the existing linear scale type; piecewise linear mapping is configured via an axis-level `customDistribution` (or equivalent) option instead of a new scale class.
- **Axis / LinearScale config**: The configuration interface on the axis (or linear scale config) that exposes `customDistribution` while keeping `type: 'linear'`.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +30
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

This checklist marks both "No implementation details (languages, frameworks, APIs)" and "No implementation details leak into specification" as satisfied, but the referenced spec file already contains concrete implementation choices such as a named LinearIntervalScale class and file paths. To keep the SDD artifacts consistent, consider either moving those implementation details into research.md/plan.md or unchecking these items (and clarifying that the spec is still at "Draft" status) so that the checklist accurately reflects the state of the specification.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +47
### 1. Implementation Strategy: Custom Scale Class

**Decision**: Implement a new `LinearIntervalScale` class within VChart (`packages/vchart/src/scale/linear-interval-scale.ts`) instead of modifying `@visactor/vscale`.

**Rationale**:
- `@visactor/vscale` is an external dependency. Modifying it requires a separate release cycle and might not be feasible if I don't have write access or if it's a shared library.
- A custom scale in VChart allows rapid iteration and specific logic for this feature.
- The scale will implement the necessary interface to be used by `CartesianLinearAxis`.

**Alternatives Considered**:
- **Modify `LinearAxisMixin`**: Implement the mapping logic directly in the axis.
- *Pros*: No new scale class.
- *Cons*: Axis logic is already complex. Coupling scale logic into axis makes it harder to reuse (e.g., for legends or other components).
- **Subclass `LinearScale`**:
- *Pros*: Inherit existing methods.
- *Cons*: `LinearScale` might have private members or strict behavior that is hard to override for piecewise logic. Composition (implementing interface and delegating if needed) is safer.

### 2. Configuration API

**Decision**: Add `customDistribution` (or similar) to the scale spec.

**Schema**:
```typescript
interface ILinearIntervalScaleSpec extends ILinearScaleSpec {
type: 'linear-interval'; // or keep 'linear' and check for distribution? Better to use explicit type.
intervals: {
domain: [number, number]; // Sub-domain
range: [number, number]; // Proportion of the visual range (0-1)
}[];
}
```

**Rationale**: Explicit mapping of domain intervals to range proportions gives full control.

### 3. Axis Integration

**Decision**: Update `CartesianLinearAxis` to support `linear-interval` scale type.

**Rationale**: The axis component checks for `type`. We need to register the new scale and ensure the axis accepts it.
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

This research document records a final "Decision" to implement a new LinearIntervalScale class, but the current implementation and plan.md indicate that the chosen approach is to reuse LinearScale with axis-level customDistribution instead. It would be helpful to update this section (for example by marking the custom-scale approach as an evaluated-but-rejected option and referencing the final plan) so that readers are not misled into thinking a LinearIntervalScale class still exists in the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines 166 to 169
return domain;
}

if (data.length) {
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

When customDistribution is present, computeLinearDomain returns immediately after constructing the set of interval endpoints, so the usual domain post-processing (setSoftDomainMinMax, expandDomain, includeZero, setDomainMinMax) is completely skipped for this path. This means options like softMin/softMax and expand on the linear axis spec have no effect whenever customDistribution is configured, which is inconsistent with the behavior of other linear axis configurations (including breaks). Consider restructuring this method so that the custom-distribution domain is still passed through the same domain adjustment pipeline (while keeping niceDomain disabled for customDistribution as you already do), rather than returning early.

Suggested change
return domain;
}
if (data.length) {
} else if (data.length) {

Copilot uses AI. Check for mistakes.

currentPos += segmentRatio * totalRange;
resultRange.push(currentPos);
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

In the gap-handling path for customDistribution, when the configured intervals exactly cover the domain but the sum of their ratio values is less than 1, totalGapDomain stays 0, so all segments get only their configured share and currentPos ends before end; the subsequent correction that forces the last point to end implicitly inflates only the final segment. This makes the effective visual ratio of the last interval larger than its configured ratio, which can be surprising. Consider either normalizing ratios to sum to 1 when there are no gaps, or distributing the leftover proportion across all segments instead of only the last one, so that the effective behavior matches user expectations more closely.

Suggested change
}
}
// If there are no gaps but the defined ratios sum to less than 1,
// proportionally stretch all segments so that the final position reaches `end`.
if (totalGapDomain === 0 && totalDefinedRatio > 0 && totalDefinedRatio < 1) {
const occupiedRange = currentPos - start;
if (occupiedRange !== 0) {
const scale = totalRange / occupiedRange;
for (let i = 1; i < resultRange.length; i++) {
resultRange[i] = start + (resultRange[i] - start) * scale;
}
}
}

Copilot uses AI. Check for mistakes.
const mid = (dStart + dEnd) / 2;
const covered = customDistribution.some(item => mid >= item.domain[0] && mid <= item.domain[1]);
if (!covered) {
totalGapDomain += Math.abs(dEnd - dStart);
Copy link
Contributor

Choose a reason for hiding this comment

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

这个相当于 break 吗?

newRange = combineDomains(this._break.scope).map(val => newRange[0] + (last(newRange) - newRange[0]) * val);
}

if ((this._spec as any).customDistribution?.length && this._scale) {
Copy link
Contributor

Choose a reason for hiding this comment

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

customDistribution 和 break 不能同时使用吧

} else {
// Gap
if (totalGapDomain > 0) {
segmentRatio = remainingRatio * (Math.abs(dSpan) / totalGapDomain);
Copy link
Contributor

Choose a reason for hiding this comment

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

有点不懂这里的逻辑,对于gap 区间的话,range 不应该折叠吗,也就是说这个ratio 不应该是0吗

if (config) {
const configSpan = config.domain[1] - config.domain[0];
if (configSpan !== 0) {
segmentRatio = config.ratio * (Math.abs(dSpan) / Math.abs(configSpan));
Copy link
Contributor

Choose a reason for hiding this comment

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

这里为什么是两个比例相乘,感觉有问题啊,不应该按照ratio来计算range吗

@xuefei1313 xuefei1313 force-pushed the 003-area-enlargement branch from ef08412 to 6ccc014 Compare February 9, 2026 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 区域放大折线图

2 participants