Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export class DimensionNodeBuilder {
return this
}

setEncoding(encoding: VBIDimension['encoding']): this {
this.yMap.set('encoding', encoding)
return this
}

build(): VBIDimension {
return this.yMap.toJSON() as VBIDimension
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,23 @@ export class DimensionsBuilder {
if (typeof fieldOrDimension === 'string') {
defaultDimension.alias = fieldOrDimension
defaultDimension.field = fieldOrDimension
// Don't set encoding - let VSeed auto-select based on chart type
} else {
defaultDimension.alias = fieldOrDimension.alias
defaultDimension.field = fieldOrDimension.field || fieldOrDimension.alias
// Only use encoding if explicitly provided
if (fieldOrDimension.encoding) {
defaultDimension.encoding = fieldOrDimension.encoding
}
}

const yMap = new Y.Map<any>()
for (const [key, value] of Object.entries(defaultDimension)) {
yMap.set(key, value)
}

this.dsl.get('dimensions').push([yMap])
const dimensionsArray = this.dsl.get('dimensions') as Y.Array<any>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

为什么要拆成行? 之前的写法有啥问题吗?

dimensionsArray.insert(dimensionsArray.length, [yMap])
const dimensionNode = new DimensionNodeBuilder(yMap)

if (callback) {
Expand All @@ -52,6 +58,17 @@ export class DimensionsBuilder {
}
}

updateEncoding(field: string, encoding: string) {
const dimensions = this.dsl.get('dimensions')
const index = dimensions.toArray().findIndex((item: any) => item.get('field') === field)
if (index !== -1) {
const dimensionYMap = dimensions.get(index)
if (dimensionYMap) {
dimensionYMap.set('encoding', encoding)
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

建议实现一个 modifyDimension, 然后基于它, 实现modifyEncoding.

另外取名的话, update和modify需要选择一个统一的, 我建议modify, 因为我们的修改大多都是局部的.


getDimensions(): VBIDimension[] {
return this.dsl.get('dimensions').toJSON()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ export class MeasureNodeBuilder {
}

setAggregate(aggregate: VBIMeasure['aggregate']): this {
this.yMap.set('aggregate', aggregate)
// Create a Y.Map to store aggregate properly (not plain object)
const aggregateMap = new Y.Map<any>()
for (const [key, value] of Object.entries(aggregate as Record<string, unknown>)) {
aggregateMap.set(key, value)
}
this.yMap.set('aggregate', aggregateMap)
return this
}

Expand Down
98 changes: 83 additions & 15 deletions packages/vbi/src/builder/sub-builders/measures/measures-builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as Y from 'yjs'
import type { ObserveCallback, VBIMeasure, VBIMeasureGroup, VBIMeasureTree } from 'src/types'
import { fieldExpr } from '../../../types'
import { MeasureNodeBuilder } from './measure-node-builder'

export class MeasuresBuilder {
Expand All @@ -10,34 +11,51 @@ export class MeasuresBuilder {
this.dsl = dsl
}

addMeasure(fieldOrMeasure: VBIMeasure['field'] | VBIMeasure): MeasureNodeBuilder
addMeasure(fieldOrMeasure: string | VBIMeasure): MeasureNodeBuilder
addMeasure(
fieldOrMeasure: VBIMeasure['field'] | VBIMeasure,
fieldOrMeasure: string | VBIMeasure,
callback: (measureNode: MeasureNodeBuilder) => void,
): MeasuresBuilder
addMeasure(
fieldOrMeasure: VBIMeasure['field'] | VBIMeasure,
fieldOrMeasure: string | VBIMeasure,
callback?: (measureNode: MeasureNodeBuilder) => void,
): MeasureNodeBuilder | MeasuresBuilder {
const defaultMeasure: VBIMeasure = {} as VBIMeasure
if (typeof fieldOrMeasure === 'string') {
defaultMeasure.alias = fieldOrMeasure
defaultMeasure.field = fieldOrMeasure
defaultMeasure.encoding = 'yAxis'
defaultMeasure.aggregate = { func: 'sum' }
defaultMeasure.expr = fieldExpr(fieldOrMeasure)
// Don't set encoding - let VSeed auto-select based on chart type
defaultMeasure.aggregate = { func: 'count' }
} else {
defaultMeasure.alias = fieldOrMeasure.alias
defaultMeasure.field = fieldOrMeasure.field
defaultMeasure.encoding = fieldOrMeasure.encoding
defaultMeasure.expr = fieldOrMeasure.expr
// Only use encoding if explicitly provided
if (fieldOrMeasure.encoding) {
defaultMeasure.encoding = fieldOrMeasure.encoding
}
defaultMeasure.aggregate = fieldOrMeasure.aggregate
}

const yMap = new Y.Map<any>()

for (const [key, value] of Object.entries(defaultMeasure)) {
yMap.set(key, value)
yMap.set('alias', defaultMeasure.alias)
yMap.set('encoding', defaultMeasure.encoding)

// 为expr创建一个Y.Map来正确存储嵌套对象
const exprMap = new Y.Map<any>()
for (const [key, value] of Object.entries(defaultMeasure.expr as Record<string, unknown>)) {
exprMap.set(key, value)
}
yMap.set('expr', exprMap)

// 为aggregate创建一个Y.Map
const aggregateMap = new Y.Map<any>()
for (const [key, value] of Object.entries(defaultMeasure.aggregate as Record<string, unknown>)) {
aggregateMap.set(key, value)
}
this.dsl.get('measures').push([yMap])
yMap.set('aggregate', aggregateMap)

const measuresArray = this.dsl.get('measures') as Y.Array<any>
measuresArray.insert(measuresArray.length, [yMap])

const measureNode = new MeasureNodeBuilder(yMap)

Expand All @@ -49,14 +67,64 @@ export class MeasuresBuilder {
}
}

removeMeasure(field: VBIMeasure['field']) {
removeMeasure(measureAlias: string) {
const measures = this.dsl.get('measures')
const index = measures.toArray().findIndex((item: any) => item.get('field') === field)
// Use alias as unique identifier, not field
const index = measures.toArray().findIndex((item: any) => {
return item.get('alias') === measureAlias
})
if (index !== -1) {
this.dsl.get('measures').delete(index, 1)
}
}

renameMeasure(measureAlias: string, newAlias: string) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

同上, 先实现modifyMeasure, 然后再rename功能

const measures = this.dsl.get('measures')
// Use alias as unique identifier, not field
const index = measures.toArray().findIndex((item: any) => {
return item.get('alias') === measureAlias
})
if (index !== -1) {
const measureYMap = measures.get(index)
if (measureYMap) {
measureYMap.set('alias', newAlias)
}
}
}

updateAggregate(measureAlias: string, func: string) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

同上, 基于modifyMeasure, 来实现modifyAggregate

const measures = this.dsl.get('measures')
// Use alias as unique identifier, not field
const index = measures.toArray().findIndex((item: any) => {
return item.get('alias') === measureAlias
})
if (index !== -1) {
const measureYMap = measures.get(index)
const aggregateYMap = measureYMap.get('aggregate')
if (aggregateYMap) {
aggregateYMap.set('func', func)
}
}
}

updateEncoding(measureAlias: string, encoding: string) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

同上

const measures = this.dsl.get('measures')
if (measures.length === 0) {
return
}

const index = measures.toArray().findIndex((item: any) => {
// Measure's identity is alias, not expr.field
return item.get('alias') === measureAlias
})
if (index !== -1) {
const measureYMap = measures.get(index)
if (measureYMap) {
measureYMap.set('encoding', encoding)
}
}
}

getMeasures(): VBIMeasure[] {
return this.dsl.get('measures').toJSON()
}
Expand All @@ -70,7 +138,7 @@ export class MeasuresBuilder {
}

static isMeasureNode(node: VBIMeasureTree[0]): node is VBIMeasure {
return 'field' in node
return 'expr' in node
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这个似乎改错了? 我理解field没问题

}

static isMeasureGroup(node: VBIMeasureTree[0]): node is VBIMeasureGroup {
Expand Down
61 changes: 60 additions & 1 deletion packages/vbi/src/builder/vbi-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as Y from 'yjs'
import { VSeedDSL } from '@visactor/vseed'
import { DimensionsBuilder } from './sub-builders/dimensions'
import { MeasuresBuilder } from './sub-builders/measures'
import { VBIDSL, VBIBuilderInterface } from 'src/types'
import { VBIDSL, VBIBuilderInterface, VBIMeasureTree, VBIMeasure, VBIDimensionTree, VBIDimension } from 'src/types'
import { buildVQuery } from 'src/pipeline'
import { ChartTypeBuilder } from './sub-builders/chart-type'
import { getConnector } from './connector'
Expand Down Expand Up @@ -36,6 +36,45 @@ export class VBIBuilder implements VBIBuilderInterface {
return Y.encodeStateAsUpdate(this.doc, targetStateVector)
}

// 辅助函数:将 VBIMeasureTree 展开为平面的 VBIMeasure 数组
private flattenMeasureTree(tree?: VBIMeasureTree): VBIMeasure[] {
Copy link
Copy Markdown
Collaborator

@youngwinds youngwinds Feb 6, 2026

Choose a reason for hiding this comment

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

共建函数可以放在这里, packages/vbi/src/utils/tree/traverse.ts

这类函数和业务完全无关, 不应放在builder内, 可以单独抽出来作为工具函数, 用更通用的形式来实现.

if (!tree) return []
const result: VBIMeasure[] = []
const traverse = (node: VBIMeasureTree[number]) => {
if ('expr' in node) {
// 这是 VBIMeasure
result.push(node as VBIMeasure)
} else {
// 这是 VBIMeasureGroup,遍历 children
if ('children' in node && node.children) {
node.children.forEach((child) => traverse(child))
}
}
}
tree.forEach((node) => traverse(node))
return result
}

// 辅助函数:将 VBIDimensionTree 展开为平面的 VBIDimension 数组
private flattenDimensionTree(tree?: VBIDimensionTree): VBIDimension[] {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

同上, 应该从builder中剥离

if (!tree) return []
const result: VBIDimension[] = []
const traverse = (node: VBIDimensionTree[number]) => {
if ('field' in node && !('children' in node)) {
// 这是 VBIDimension
result.push(node as VBIDimension)
} else if ('children' in node) {
// 这是 VBIDimensionGroup,遍历 children
const group = node as any
if (group.children) {
group.children.forEach((child: any) => traverse(child))
}
}
}
tree.forEach((node) => traverse(node))
return result
}

public buildVSeed = async (): Promise<VSeedDSL> => {
const vbiDSL = this.build()
const connectorId = vbiDSL.connectorId
Expand All @@ -45,9 +84,29 @@ export class VBIBuilder implements VBIBuilderInterface {
const schema = await connector.discoverSchema()
const queryResult = await connector.query({ queryDSL, schema, connectorId })

// 转换 VBI measures 为 VSeed measures 格式
const flatMeasures = this.flattenMeasureTree(vbiDSL.measures)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

buildVSeed越来越大了, 我强烈建议抽出一个公共方法, builder内仅描述逻辑即可,

const vseedMeasures = flatMeasures.map((m) => ({
id: m.alias || (m.expr?.type === 'field' ? m.expr.field : 'measure'),
alias: m.alias,
encoding: m.encoding,
}))

// 转换 VBI dimensions 为 VSeed dimensions 格式
const flatDimensions = this.flattenDimensionTree(vbiDSL.dimensions)
const vseedDimensions = flatDimensions.map((d) => ({
id: d.field,
alias: d.alias,
encoding: d.encoding,
}))

return {
chartType: vbiDSL.chartType,
dataset: queryResult.dataset,
measures: vseedMeasures,
dimensions: vseedDimensions,
theme: vbiDSL.theme,
locale: vbiDSL.locale,
} as VSeedDSL
}

Expand Down
46 changes: 38 additions & 8 deletions packages/vbi/src/builder/vbi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,47 @@ const createVBI = () => {
if (vbi.locale) dsl.set('locale', vbi.locale)
if (vbi.version) dsl.set('version', vbi.version)

if (!dsl.get('measures')) {
dsl.set('measures', new Y.Array<any>())
} else {
dsl.set('measures', vbi.measures)
// Always create Y.Array for measures, converting plain arrays if needed
const measuresArray = new Y.Array<any>()
if (vbi.measures && Array.isArray(vbi.measures) && vbi.measures.length > 0) {
for (const m of vbi.measures) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

我当时写的逻辑, 是业务yjs内部就会自动设置, 可以省去很多递归, 难道不是这样的?

const yMap = new Y.Map<any>()
for (const [key, value] of Object.entries(m as Record<string, any>)) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const nestedMap = new Y.Map<any>()
for (const [k, v] of Object.entries(value)) {
nestedMap.set(k, v)
}
yMap.set(key, nestedMap)
} else {
yMap.set(key, value)
}
}
measuresArray.insert(measuresArray.length, [yMap])
}
}
dsl.set('measures', measuresArray)

if (!dsl.get('dimensions')) {
dsl.set('dimensions', new Y.Array<any>())
} else {
dsl.set('dimensions', vbi.dimensions)
// Always create Y.Array for dimensions, converting plain arrays if needed
const dimensionsArray = new Y.Array<any>()
if (vbi.dimensions && Array.isArray(vbi.dimensions) && vbi.dimensions.length > 0) {
for (const d of vbi.dimensions) {
const yMap = new Y.Map<any>()
for (const [key, value] of Object.entries(d as Record<string, any>)) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const nestedMap = new Y.Map<any>()
for (const [k, v] of Object.entries(value)) {
nestedMap.set(k, v)
}
yMap.set(key, nestedMap)
} else {
yMap.set(key, value)
}
}
dimensionsArray.insert(dimensionsArray.length, [yMap])
}
}
dsl.set('dimensions', dimensionsArray)
})

return new VBIBuilder(doc)
Expand Down
11 changes: 8 additions & 3 deletions packages/vbi/src/pipeline/vqueryDSL/buildVQuery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Select, VQueryDSL } from '@visactor/vquery'
import { VBIDSL } from '../../types'
import { VBIDSL, exprField } from '../../types'
import { DimensionsBuilder, MeasuresBuilder, VBIBuilder } from 'src'
import { pipe } from 'remeda'

Expand All @@ -21,11 +21,16 @@ const buildSelect: buildPipe = (queryDSL, context) => {
const result = { ...queryDSL }
const measureNodes = measures.filter((measure) => MeasuresBuilder.isMeasureNode(measure))
const measureSelects: Select<Record<string, unknown>> = measureNodes.map((measure) => {
return {
field: measure.field,
const field = exprField(measure.expr)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

不太理解这个expr的具体作用, 具体有啥用? 是要支持用户自定义表达式?

if (!field) {
throw new Error(`Cannot extract field from measure expr: ${JSON.stringify(measure.expr)}`)
}
const selectItem: any = {
field,
alias: measure.alias,
func: measure.aggregate.func,
}
return selectItem
})

const dimensionNodes = dimensions.filter((dimension) => DimensionsBuilder.isDimensionNode(dimension))
Expand Down
1 change: 1 addition & 0 deletions packages/vbi/src/types/dsl/dimensions/dimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod'
export const zVBIDimensionSchema = z.object({
field: z.string(),
alias: z.string(),
encoding: z.string().optional(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

vseed有具体的encoding值, 不应只写一个string

})

export const zVBIDimensionGroupSchema: z.ZodType<VBIDimensionGroup> = z.object({
Expand Down
2 changes: 2 additions & 0 deletions packages/vbi/src/types/dsl/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export type { VBIDSL } from './vbi/vbi'
export type { VBIDimensionTree, VBIDimensionGroup, VBIDimension } from './dimensions/dimensions'
export type { VBIMeasureTree, VBIMeasureGroup, VBIMeasure } from './measures/measures'
export type { MeasureExpr } from './measures/expr'
export { fieldExpr, exprField } from './measures/expr'
export type { VBIDSLTheme } from './theme/theme'
Loading