Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/restore-bar-clip-bounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@visactor/vseed': patch
---

Restore bar stroke clipping for single-series charts, conditional corner styles, and dual-axis bars without configured stack corners. Preserve default rounding on unmatched bars and explicit per-bar corners on matched bars without expanding the rendered bar bounds. Keep moving bars unclipped during moveIn animations.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ VBI provides reusable packages and agent skills for AI-oriented business intelli
| Name | Description | Test Coverage | Npm Version |
| :------------------------------------------------------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ |
| [vbi](https://visactor.github.io/VBI) | HeadlessBI Interface | [![Coverage](https://img.shields.io/badge/Coverage-100%25-brightgreen.svg)](https://github.com/VisActor/VBI/actions/workflows/coverage.yml) | [![npm Version](https://img.shields.io/npm/v/@visactor/vbi.svg)](https://www.npmjs.com/package/@visactor/vbi) |
| [vseed](https://visactor.github.io/VBI/vseed/guide/quickStart) | Visualization Engine | [![Coverage](https://img.shields.io/badge/Coverage-90.47%25-brightgreen.svg)](https://github.com/VisActor/VSeed/actions/workflows/coverage.yml) | [![npm Version](https://img.shields.io/npm/v/@visactor/vseed.svg)](https://www.npmjs.com/package/@visactor/vseed) |
| [vseed](https://visactor.github.io/VBI/vseed/guide/quickStart) | Visualization Engine | [![Coverage](https://img.shields.io/badge/Coverage-90.56%25-brightgreen.svg)](https://github.com/VisActor/VSeed/actions/workflows/coverage.yml) | [![npm Version](https://img.shields.io/npm/v/@visactor/vseed.svg)](https://www.npmjs.com/package/@visactor/vseed) |
| [vquery](https://visactor.github.io/VBI/VQuery) | Lightweight Query Engine | [![Coverage](https://img.shields.io/badge/Coverage-100.00%25-brightgreen.svg)](https://github.com/VisActor/VSeed/actions/workflows/coverage.yml) | [![npm Version](https://img.shields.io/npm/v/@visactor/vquery.svg)](https://www.npmjs.com/package/@visactor/vquery) |
406 changes: 209 additions & 197 deletions apps/website/docs/public/storybook/de-DE/assets/iframe.js

Large diffs are not rendered by default.

120 changes: 89 additions & 31 deletions apps/website/docs/public/storybook/de-DE/assets/vendor.js

Large diffs are not rendered by default.

406 changes: 209 additions & 197 deletions apps/website/docs/public/storybook/en-US/assets/iframe.js

Large diffs are not rendered by default.

120 changes: 89 additions & 31 deletions apps/website/docs/public/storybook/en-US/assets/vendor.js

Large diffs are not rendered by default.

406 changes: 209 additions & 197 deletions apps/website/docs/public/storybook/fr-FR/assets/iframe.js

Large diffs are not rendered by default.

120 changes: 89 additions & 31 deletions apps/website/docs/public/storybook/fr-FR/assets/vendor.js

Large diffs are not rendered by default.

406 changes: 209 additions & 197 deletions apps/website/docs/public/storybook/id-ID/assets/iframe.js

Large diffs are not rendered by default.

120 changes: 89 additions & 31 deletions apps/website/docs/public/storybook/id-ID/assets/vendor.js

Large diffs are not rendered by default.

406 changes: 209 additions & 197 deletions apps/website/docs/public/storybook/ja-JP/assets/iframe.js

Large diffs are not rendered by default.

120 changes: 89 additions & 31 deletions apps/website/docs/public/storybook/ja-JP/assets/vendor.js

Large diffs are not rendered by default.

406 changes: 209 additions & 197 deletions apps/website/docs/public/storybook/ko-KR/assets/iframe.js

Large diffs are not rendered by default.

120 changes: 89 additions & 31 deletions apps/website/docs/public/storybook/ko-KR/assets/vendor.js

Large diffs are not rendered by default.

406 changes: 209 additions & 197 deletions apps/website/docs/public/storybook/vi-VN/assets/iframe.js

Large diffs are not rendered by default.

120 changes: 89 additions & 31 deletions apps/website/docs/public/storybook/vi-VN/assets/vendor.js

Large diffs are not rendered by default.

406 changes: 209 additions & 197 deletions apps/website/docs/public/storybook/zh-CN/assets/iframe.js

Large diffs are not rendered by default.

120 changes: 89 additions & 31 deletions apps/website/docs/public/storybook/zh-CN/assets/vendor.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/vseed/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ VSeed 支持在 DSL 中写动态过滤逻辑(code),在 build 前执行。
- 组合式 pipeline 构建图表类型
- 适配器模式(如 pivotAdapter)承接条件分支

`stackCornerRadius` 的裁剪同时限制柱体描边的外扩。即使未配置圆角、使用单系列或条件圆角,也应保留静态柱体的裁剪边界;匹配显式 `barRadius` 时,裁剪圆角不应大于图元圆角,避免把直角裁成圆角。`moveIn` 因图元尚未到达最终位置,使用图元圆角而不使用最终位置裁剪。

详见:

- `apps/website/docs/zh-CN/vseed/development/designPhilosophy/pipeline/pipelineDesign.md`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { IBarChartSpec } from '@visactor/vchart'
import { normalizePadding } from '@visactor/vutils'
import type { VChartSpecPipe, StackCornerRadius, BarStyle } from 'src/types'
import { selector, selectorWithDynamicFilter } from 'src/dataSelector'
import { createBarCornerRadius, createStackCornerRadius, hasMoveInAnimation } from './stackCornerRadiusUtils'

const hasBarMoveInAnimation = (spec: IBarChartSpec): boolean => {
Expand All @@ -11,22 +13,32 @@ const hasBarMoveInAnimation = (spec: IBarChartSpec): boolean => {
export const stackCornerRadius: VChartSpecPipe = (spec, context) => {
const { advancedVSeed, vseed } = context
const { chartType } = vseed
const stackCornerRadius = advancedVSeed.config?.[chartType as 'column']?.stackCornerRadius as StackCornerRadius
const stackCornerRadius = (advancedVSeed.config?.[chartType as 'column']?.stackCornerRadius ?? 0) as StackCornerRadius

const styles = advancedVSeed.markStyle?.barStyle
const rules = (Array.isArray(styles) ? styles : styles ? [styles] : []) as BarStyle[]
// A stack clip would trim explicit per-bar corners, including conditional rules.
if (stackCornerRadius == null || rules.some((rule) => rule.barRadius != null)) {
return spec
if (!hasBarMoveInAnimation(spec as IBarChartSpec)) {
const styles = advancedVSeed.markStyle?.barStyle
const rules = ((Array.isArray(styles) ? styles : styles ? [styles] : []) as BarStyle[])
.filter((rule) => rule.barRadius != null)
.reverse()
const defaultRadius = createStackCornerRadius(stackCornerRadius)
// Preserve the stroke bounds without rounding more than an explicit per-bar corner.
const clipRadius: typeof defaultRadius = rules.length
? (attributes, datum) => {
const radius = defaultRadius(attributes, datum)
const rule = rules.find((rule) =>
rule.dynamicFilter
? selectorWithDynamicFilter(datum, rule.dynamicFilter, rule.selector)
: selector(datum, rule.selector),
)
if (!rule) return radius
const barRadius = normalizePadding(rule.barRadius!)
return normalizePadding(radius).map((corner, index) => Math.min(corner, barRadius[index]))
}
: defaultRadius
return { ...spec, stackCornerRadius: clipRadius } as IBarChartSpec
}

const singleSeries = advancedVSeed.datasetReshapeInfo?.[0]?.unfoldInfo.colorItems.length === 1

if (!singleSeries && !hasBarMoveInAnimation(spec as IBarChartSpec)) {
return { ...spec, stackCornerRadius: createStackCornerRadius(stackCornerRadius) } as IBarChartSpec
}

// A single series needs no stack clip; a final-position clip also cuts off moveIn.
// A final-position clip would cut off moveIn before it reaches the bar bounds.
return {
...spec,
bar: {
Expand Down
37 changes: 37 additions & 0 deletions packages/vseed/tests/unit/builder/barClipping.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import VChart from '@visactor/vchart'
import { Builder, registerAll } from 'src/builder'
import type { VSeed } from 'src/types'
import columnCase from '../../integrations/columnParallel/markStyle/selectorBarDimensionCondition.json'
import barCase from '../../integrations/barParallel/markStyle/barStyleMeasureCondition.json'
import dualAxisCase from '../../integrations/dualAxis/combination/columnParallelLine.json'

beforeAll(registerAll)

test.each([columnCase, barCase, dualAxisCase])('$name keeps strokes within the native bar bounds', (fixture) => {
const spec = Builder.from(fixture.vseed as VSeed).build<any>()
const element = document.createElement('div')
document.body.append(element)
const chart = new VChart({ ...spec, width: 500, height: 500 }, { dom: element, animation: false })
try {
chart.renderSync()
for (const series of chart.getChart()!.getAllSeries()) {
const mark = series.getMarkInName('bar')
if (!mark) continue
const config = mark.getMarkConfig() as any
expect(config.clip).toBe(true)
const rectangles = config.clipPath()
const data = series.getViewData()!.latestData
expect(rectangles).toHaveLength(data.length)
const channels =
series.getSpec().direction === 'horizontal' ? ['x', 'x1', 'y', 'height'] : ['x', 'y', 'y1', 'width']
rectangles.forEach((rectangle: any, index: number) => {
for (const channel of channels) {
expect(rectangle.attribute[channel]).toBeCloseTo(mark.getAttribute(channel, data[index]) as number)
}
})
}
} finally {
chart.release()
element.remove()
}
})
2 changes: 1 addition & 1 deletion packages/vseed/tests/unit/builder/barGradient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ test.each(['column', 'bar', 'columnParallel', 'barParallel', 'columnPercent', 'b
{ offset: 1, color: 'rgba(255,0,0,0.5)' },
],
})
expect(spec.stackCornerRadius).toBeUndefined()
expect(spec.stackCornerRadius).toBeTypeOf('function')
},
)

Expand Down
8 changes: 5 additions & 3 deletions packages/vseed/tests/unit/builder/barUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ describe('bar geometry across spec updates', () => {
const current = chart.getChart()!.getAllSeries()[0]
expect(current === series).toBe(true)
const mark = current.getMarkInName('bar')!
expect(
current.getViewData()!.latestData.map((datum: any) => mark.getAttribute('cornerRadius', datum)),
).toEqual(values.map((value) => (value > 0 ? [4, 4, 0, 0] : [0, 0, 4, 4])))
const config = mark.getMarkConfig() as any
expect(config.clip).toBe(true)
expect(config.clipPath().map((rectangle: any) => rectangle.attribute.cornerRadius)).toEqual(
values.map((value) => (value > 0 ? [4, 4, 0, 0] : [0, 0, 4, 4])),
)
}
} finally {
chart.release()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,21 @@ describe('stackCornerRadius pipe', () => {
expect(createStackCornerRadius([7, 7, 0, 0])).not.toBe(callback)
})

it.each([0, 4, [4, 4, 4, 4]])('explicit per-bar corners %j take priority over a stack clip', (barRadius) => {
it.each([0, 4, [4, 4, 4, 4]])('explicit per-bar corners %j retain the stroke clip', (barRadius) => {
const spec = Builder.from({
chartType: 'column',
dataset: [{ category: 'A', value: 10 }],
dimensions: [{ id: 'category' }],
measures: [{ id: 'value' }],
barStyle: { barRadius },
}).build<any>()
expect(spec.stackCornerRadius).toBeUndefined()
expect(spec.stackCornerRadius(null, { __VCHART_STACK_START: 0, __VCHART_STACK_END: 10 })).toEqual(
barRadius === 0 ? [0, 0, 0, 0] : [4, 4, 0, 0],
)
expect(spec.bar.style.cornerRadius).toEqual(barRadius)
})

it('retains the stack clip for colors but removes it for conditional per-bar corners', () => {
it('conditional per-bar corners preserve the default outline on unmatched bars', () => {
const dsl = {
chartType: 'column' as const,
dataset: [
Expand All @@ -57,9 +59,54 @@ describe('stackCornerRadius pipe', () => {
'function',
)
const spec = Builder.from({ ...dsl, barStyle: { selector: { value: -10 }, barRadius: 5 } }).build<any>()
expect(spec.stackCornerRadius).toBeUndefined()
expect(spec.stackCornerRadius(null, { value: -10, __VCHART_STACK_START: -10, __VCHART_STACK_END: 0 })).toEqual([
0, 0, 4, 4,
])
expect(spec.stackCornerRadius(null, { value: 20, __VCHART_STACK_START: 0, __VCHART_STACK_END: 20 })).toEqual([
4, 4, 0, 0,
])
expect(spec.bar.state.custom1.style.cornerRadius).toBe(5)
})

it('dynamic corner rules preserve the default outline when they do not match', () => {
const context = createContext()
context.advancedVSeed.markStyle = {
barStyle: {
barRadius: 0,
dynamicFilter: {
type: 'row-with-field',
code: '',
result: { success: true, data: [{ __row_index: 0, field: 'value' }] },
},
},
}
const spec = stackCornerRadius({}, context) as any
expect(spec.stackCornerRadius(null, { __row_index: 0, __MeaId__: 'value', value: 10 })).toEqual([0, 0, 0, 0])
expect(spec.stackCornerRadius(null, { __row_index: 1, __VCHART_STACK_START: 0, __VCHART_STACK_END: 10 })).toEqual([
4, 4, 0, 0,
])
})

it('still clips strokes when no radius is configured', () => {
const context = createContext()
context.advancedVSeed.config = {}
const spec = stackCornerRadius({}, context) as any
expect(spec.stackCornerRadius(null, { __VCHART_STACK_START: 0, __VCHART_STACK_END: 10 })).toBe(0)
})

it('honors asymmetric corners and the last matching radius rule', () => {
const context = createContext()
context.advancedVSeed.markStyle = {
barStyle: [{ barRadius: [0, 8, 8, 0] }, { selector: { value: 10 }, barRadius: 2 }],
}
const spec = stackCornerRadius({}, context) as any
expect(spec.stackCornerRadius(null, { value: 10, __VCHART_STACK_START: 0, __VCHART_STACK_END: 10 })).toEqual([
2, 2, 0, 0,
])
expect(spec.stackCornerRadius(null, { value: -10, __VCHART_STACK_START: -10, __VCHART_STACK_END: 0 })).toEqual([
0, 0, 4, 0,
])
})
it('should use root stackCornerRadius when no moveIn animation exists', () => {
const result = stackCornerRadius({}, createContext()) as any

Expand Down
4 changes: 2 additions & 2 deletions packages/vseed/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export default defineConfig(({ mode }) => ({
// V8 branch counts vary slightly between runs; use verified floors.
thresholds:
mode === 'unit'
? { statements: 81.9, lines: 81.9, branches: 75.7, functions: 80.3 }
? { statements: 82, lines: 82, branches: 76, functions: 80.6 }
: mode === 'integration'
? undefined
: { statements: 90.4, lines: 90.4, branches: 81.3, functions: 83.2 },
: { statements: 90.5, lines: 90.5, branches: 81.5, functions: 83.4 },
},
globals: true,
environment: 'jsdom',
Expand Down
Loading