diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/area.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/area.ts index 632da5e4..e9dc89c4 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/area.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/area.ts @@ -1,13 +1,18 @@ -import { hasSubset, intersects } from '../../../../../utils'; -import { splitAreaXYSeries } from '../../visual-encoder/split-fields'; +import { find } from 'lodash'; + import { getLineSize } from '../../visual-encoder/utils'; +import { mapFieldsToVisualEncode } from '../../visual-encoder/encode-mapping'; +import { areaEncodeRequirement } from '../../../../../../ckb/encode'; -import type { Data, Datum } from '../../../../../../common/types'; -import type { Advice, BasicDataPropertyForAdvice } from '../../../../../types'; +import type { Datum } from '../../../../../../common/types'; +import type { Advice } from '../../../../../types'; +import type { GenerateChartSpecParams } from '../types'; -export function areaChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const field4X = dataProps.find((field) => intersects(field.levelOfMeasurements, ['Time', 'Ordinal'])); - const field4Y = dataProps.find((field) => hasSubset(field.levelOfMeasurements, ['Interval'])); +export function areaChart({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: areaEncodeRequirement }); + const field4X = encode.x?.[0]; + const field4Y = encode.y?.[0]; if (!field4X || !field4Y) return null; @@ -15,9 +20,9 @@ export function areaChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): type: 'area', data, encode: { - x: field4X.name, - y: field4Y.name, - size: (datum: Datum) => getLineSize(datum, data, { field4X }), + x: field4X, + y: field4Y, + size: (datum: Datum) => getLineSize(datum, data, { field4X: find(dataProps, ['name', field4X]) }), }, legend: { size: false, @@ -27,42 +32,44 @@ export function areaChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): return spec; } -export function stackedAreaChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, field4Series] = splitAreaXYSeries(dataProps); - if (!field4X || !field4Y || !field4Series) return null; +export function stackedAreaChart({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: areaEncodeRequirement }); + const [field4X, field4Y, field4Series] = [encode.x?.[0], encode.y?.[0], encode.color?.[0]]; + if (!field4X || !field4Y) return null; const spec: Advice['spec'] = { type: 'area', data, encode: { - x: field4X.name, - y: field4Y.name, - color: field4Series.name, - size: (datum: Datum) => getLineSize(datum, data, { field4Split: field4Series, field4X }), + x: field4X, + y: field4Y, + size: (datum: Datum) => + getLineSize(datum, data, { + field4Split: find(dataProps, ['name', field4Series]), + field4X: find(dataProps, ['name', field4X]), + }), }, legend: { size: false, }, - transform: [{ type: 'stackY' }], }; + if (field4Series) { + spec.encode.color = field4Series; + spec.transform = [{ type: 'stackY' }]; + } + return spec; } -export function percentStackedAreaChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, field4Series] = splitAreaXYSeries(dataProps); - if (!field4X || !field4Y || !field4Series) return null; - - const spec: Advice['spec'] = { - type: 'area', - data, - encode: { - x: field4X.name, - y: field4Y.name, - color: field4Series.name, - }, - transform: [{ type: 'stackY' }, { type: 'normalizeY' }], - }; +export function percentStackedAreaChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + const spec = stackedAreaChart({ data, dataProps, encode }); + if (spec?.transform) { + spec.transform.push({ type: 'normalizeY' }); + } else { + spec.transform = [{ type: 'normalizeY' }]; + } return spec; } diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/bar.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/bar.ts index 8f0e54bf..689f0444 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/bar.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/bar.ts @@ -1,10 +1,14 @@ import { splitBarXYSeries } from '../../visual-encoder/split-fields'; +import { mapFieldsToVisualEncode } from '../../visual-encoder/encode-mapping'; +import { barEncodeRequirement } from '../../../../../../ckb/encode'; -import type { Data } from '../../../../../../common/types'; -import type { Advice, BasicDataPropertyForAdvice } from '../../../../../types'; +import type { Advice } from '../../../../../types'; +import type { GenerateChartSpecParams } from '../types'; -export function barChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, field4Color] = splitBarXYSeries(dataProps); +export function barChart({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: barEncodeRequirement }); + const [field4X, field4Y, field4Color] = [encode.x?.[0], encode.y?.[0], encode.color?.[0]]; if (!field4X || !field4Y) return null; @@ -14,8 +18,8 @@ export function barChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): A // G2's implementation converts column chart (vertical bar) and bar chart (horizontal bar) by transpose, so the x and y fields need to be swapped. // 由于g2的实现是通过transpose来转换 column chart(竖着的bar)和bar chart(横着的bar),所以x和y的字段需要做交换 encode: { - x: field4Y.name, - y: field4X.name, + x: field4Y, + y: field4X, }, coordinate: { transform: [{ type: 'transpose' }], @@ -23,72 +27,34 @@ export function barChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): A }; if (field4Color) { - spec.encode.color = field4Color.name; + spec.encode.color = field4Color; spec.transform = [{ type: 'stackY' }]; } return spec; } -export function groupedBarChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, field4Series] = splitBarXYSeries(dataProps); - if (!field4X || !field4Y || !field4Series) return null; - - const spec: Advice['spec'] = { - type: 'interval', - data, - encode: { - x: field4Y.name, - y: field4X.name, - color: field4Series.name, - }, - transform: [{ type: 'dodgeX' }], - coordinate: { - transform: [{ type: 'transpose' }], - }, - }; +export function groupedBarChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + const spec = barChart({ data, dataProps, encode }); + if (spec?.encode?.color) { + spec.transform = [{ type: 'dodgeX' }]; + } return spec; } -export function stackedBarChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, field4Series] = splitBarXYSeries(dataProps); - if (!field4X || !field4Y || !field4Series) return null; - - const spec: Advice['spec'] = { - type: 'interval', - data, - encode: { - x: field4Y.name, - y: field4X.name, - color: field4Series.name, - }, - transform: [{ type: 'stackY' }], - coordinate: { - transform: [{ type: 'transpose' }], - }, - }; - - return spec; +export function stackedBarChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + return barChart({ data, dataProps, encode }); } -export function percentStackedBarChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { +export function percentStackedBarChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { const [field4X, field4Y, field4Series] = splitBarXYSeries(dataProps); if (!field4X || !field4Y || !field4Series) return null; - - const spec: Advice['spec'] = { - type: 'interval', - data, - encode: { - x: field4Y.name, - y: field4X.name, - color: field4Series.name, - }, - transform: [{ type: 'stackY' }, { type: 'normalizeY' }], - coordinate: { - transform: [{ type: 'transpose' }], - }, - }; - + const spec = barChart({ data, dataProps, encode }); + if (spec?.transform) { + spec.transform.push({ type: 'normalizeY' }); + } else { + spec.transform = [{ type: 'normalizeY' }]; + } return spec; } diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/column.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/column.ts index da66f8ad..17ae8831 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/column.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/column.ts @@ -1,84 +1,50 @@ -import { Data } from '../../../../../../common/types'; -import { Advice, BasicDataPropertyForAdvice } from '../../../../../types'; -import { compare, hasSubset } from '../../../../../utils'; -import { splitColumnXYSeries } from '../../visual-encoder/split-fields'; +import { columnEncodeRequirement } from '../../../../../../ckb/encode'; +import { mapFieldsToVisualEncode } from '../../visual-encoder/encode-mapping'; -export function columnChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const nominalFields = dataProps.filter((field) => hasSubset(field.levelOfMeasurements, ['Nominal'])); - const sortedNominalFields = nominalFields.sort(compare); - const field4X = sortedNominalFields[0]; - const field4Color = sortedNominalFields[1]; - const field4Y = dataProps.find((field) => hasSubset(field.levelOfMeasurements, ['Interval'])); +import type { Advice } from '../../../../../types'; +import type { GenerateChartSpecParams } from '../types'; +export function columnChart({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: columnEncodeRequirement }); + const [field4X, field4Y, field4Color] = [encode.x?.[0], encode.y?.[0], encode.color?.[0]]; if (!field4X || !field4Y) return null; const spec: Advice['spec'] = { type: 'interval', data, encode: { - x: field4X.name, - y: field4Y.name, + x: field4X, + y: field4Y, }, }; if (field4Color) { - spec.encode.color = field4Color.name; + spec.encode.color = field4Color; spec.transform = [{ type: 'stackY' }]; } return spec; } -export function groupedColumnChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, field4Series] = splitColumnXYSeries(dataProps); - if (!field4X || !field4Y || !field4Series) return null; - - const spec: Advice['spec'] = { - type: 'interval', - data, - encode: { - x: field4X.name, - y: field4Y.name, - color: field4Series.name, - }, - transform: [{ type: 'dodgeX' }], - }; - +export function groupedColumnChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + const spec = columnChart({ data, dataProps, encode }); + if (spec?.encode?.color) { + spec.transform = [{ type: 'dodgeX' }]; + } return spec; } -export function stackedColumnChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, Field4Series] = splitColumnXYSeries(dataProps); - if (!field4X || !field4Y || !Field4Series) return null; - - const spec: Advice['spec'] = { - type: 'interval', - data, - encode: { - x: field4X.name, - y: field4Y.name, - color: Field4Series.name, - }, - transform: [{ type: 'stackY' }], - }; - - return spec; +export function stackedColumnChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + return columnChart({ data, dataProps, encode }); } -export function percentStackedColumnChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, Field4Series] = splitColumnXYSeries(dataProps); - if (!field4X || !field4Y || !Field4Series) return null; - - const spec: Advice['spec'] = { - type: 'interval', - data, - encode: { - x: field4X.name, - y: field4Y.name, - color: Field4Series.name, - }, - transform: [{ type: 'stackY' }, { type: 'normalizeY' }], - }; - +export function percentStackedColumnChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + const spec = columnChart({ data, dataProps, encode }); + if (spec?.transform) { + spec.transform.push({ type: 'normalizeY' }); + } else { + spec.transform = [{ type: 'normalizeY' }]; + } return spec; } diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/heatmap.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/heatmap.ts index f6fdbbc0..f56e0bab 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/heatmap.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/heatmap.ts @@ -1,14 +1,15 @@ -import { intersects, compare, hasSubset } from '../../../../../utils'; +import { mapFieldsToVisualEncode } from '../../visual-encoder/encode-mapping'; +import { heatmapEncodeRequirement } from '../../../../../../ckb/encode'; -import type { Data } from '../../../../../../common/types'; -import type { BasicDataPropertyForAdvice, Advice } from '../../../../../types'; +import type { Advice } from '../../../../../types'; +import type { GenerateChartSpecParams } from '../types'; -export function heatmap(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const axisFields = dataProps.filter((field) => intersects(field.levelOfMeasurements, ['Nominal', 'Ordinal'])); - const sortedFields = axisFields.sort(compare); - const field4X = sortedFields[0]; - const field4Y = sortedFields[1]; - const field4Color = dataProps.find((field) => hasSubset(field.levelOfMeasurements, ['Interval'])); +export function heatmap({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: heatmapEncodeRequirement }); + const field4X = encode?.x?.[0]; + const field4Y = encode?.y?.[0]; + const field4Color = encode?.color?.[0]; if (!field4X || !field4Y || !field4Color) return null; @@ -16,9 +17,9 @@ export function heatmap(data: Data, dataProps: BasicDataPropertyForAdvice[]): Ad type: 'cell', data, encode: { - x: field4X.name, - y: field4Y.name, - color: field4Color.name, + x: field4X, + y: field4Y, + color: field4Color, }, }; diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/histogram.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/histogram.ts index a5d8cf98..9b676561 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/histogram.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/histogram.ts @@ -1,17 +1,20 @@ -import { hasSubset } from '../../../../../utils'; +import { mapFieldsToVisualEncode } from '../../visual-encoder/encode-mapping'; +import { histogramEncodeRequirement } from '../../../../../../ckb/encode'; -import type { Data } from '../../../../../../common/types'; -import type { BasicDataPropertyForAdvice, Advice } from '../../../../../types'; +import type { Advice } from '../../../../../types'; +import type { GenerateChartSpecParams } from '../types'; -export function histogram(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const field = dataProps.find((field) => hasSubset(field.levelOfMeasurements, ['Interval'])); +export function histogram({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: histogramEncodeRequirement }); + const field = encode.x?.[0]; if (!field) return null; const spec: Advice['spec'] = { type: 'rect', data, encode: { - x: field.name, + x: field, }, transform: [{ type: 'binX', y: 'count' }], }; diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/line.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/line.ts index e3bf3584..75210199 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/line.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/line.ts @@ -1,20 +1,26 @@ -import { splitLineXY } from '../../visual-encoder/split-fields'; +import { find } from 'lodash'; + import { getLineSize } from '../../visual-encoder/utils'; +import { mapFieldsToVisualEncode } from '../../visual-encoder/encode-mapping'; +import { lineEncodeRequirement } from '../../../../../../ckb/encode'; -import type { Data, Datum } from '../../../../../../common/types'; -import type { Advice, BasicDataPropertyForAdvice } from '../../../../../types'; +import type { Datum } from '../../../../../../common/types'; +import type { Advice } from '../../../../../types'; +import type { GenerateChartSpecParams } from '../types'; -export function lineChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, field4Color] = splitLineXY(dataProps); +export function lineChart({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: lineEncodeRequirement }); + const [field4X, field4Y, field4Color] = [encode.x?.[0], encode.y?.[0], encode.color?.[0]]; if (!field4X || !field4Y) return null; const spec: Advice['spec'] = { type: 'line', data, encode: { - x: field4X.name, - y: field4Y.name, - size: (datum: Datum) => getLineSize(datum, data, { field4X }), + x: field4X, + y: field4Y, + size: (datum: Datum) => getLineSize(datum, data, { field4X: find(dataProps, ['name', field4X]) }), }, legend: { size: false, @@ -22,33 +28,16 @@ export function lineChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): }; if (field4Color) { - spec.encode.color = field4Color.name; + spec.encode.color = field4Color; } return spec; } -export function stepLineChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4X, field4Y, field4Color] = splitLineXY(dataProps); - if (!field4X || !field4Y) return null; - - const spec: Advice['spec'] = { - type: 'line', - data, - encode: { - x: field4X.name, - y: field4Y.name, - shape: 'hvh', - size: (datum: Datum) => getLineSize(datum, data, { field4X }), - }, - legend: { - size: false, - }, - }; - - if (field4Color) { - spec.encode.color = field4Color.name; +export function stepLineChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + const spec = lineChart({ data, dataProps, encode }); + if (spec?.encode) { + spec.encode.shape = 'hvh'; } - return spec; } diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/pie.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/pie.ts index 08219c77..f2f6fc20 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/pie.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/pie.ts @@ -1,18 +1,21 @@ -import { splitAngleColor } from '../../visual-encoder/split-fields'; +import { mapFieldsToVisualEncode } from '../../visual-encoder/encode-mapping'; +import { pieEncodeRequirement } from '../../../../../../ckb/encode'; -import type { Data } from '../../../../../../common/types'; -import type { Advice, BasicDataPropertyForAdvice } from '../../../../../types'; +import type { Advice } from '../../../../../types'; +import type { GenerateChartSpecParams } from '../types'; -export function pieChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4Color, field4Angle] = splitAngleColor(dataProps); +export function pieChart({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: pieEncodeRequirement }); + const [field4Angle, field4Color] = [encode.y?.[0], encode.color?.[0]]; if (!field4Angle || !field4Color) return null; const spec: Advice['spec'] = { type: 'interval', data, encode: { - color: field4Color.name, - y: field4Angle.name, + color: field4Color, + y: field4Angle, }, transform: [{ type: 'stackY' }], coordinate: { type: 'theta' }, @@ -20,19 +23,10 @@ export function pieChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): A return spec; } -export function donutChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const [field4Color, field4Angle] = splitAngleColor(dataProps); - if (!field4Angle || !field4Color) return null; - - const spec: Advice['spec'] = { - type: 'interval', - data, - encode: { - color: field4Color.name, - y: field4Angle.name, - }, - transform: [{ type: 'stackY' }], - coordinate: { type: 'theta', innerRadius: 0.6 }, - }; +export function donutChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + const spec = pieChart({ data, dataProps, encode }); + if (spec?.coordinate?.type === 'theta') { + spec.coordinate.innerRadius = 0.6; + } return spec; } diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/scatter.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/scatter.ts index 37931be5..d68506da 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/scatter.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/charts/scatter.ts @@ -1,15 +1,18 @@ -import { pearson } from '../../../../../../data'; -import { hasSubset, compare, intersects } from '../../../../../utils'; - -import type { BasicDataPropertyForAdvice, Advice } from '../../../../../types'; -import type { Data } from '../../../../../../common/types'; - -export function scatterPlot(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const intervalFields = dataProps.filter((field) => hasSubset(field.levelOfMeasurements, ['Interval'])); - const sortedIntervalFields = intervalFields.sort(compare); - const field4X = sortedIntervalFields[0]; - const field4Y = sortedIntervalFields[1]; - const field4Color = dataProps.find((field) => hasSubset(field.levelOfMeasurements, ['Nominal'])); +import { mapFieldsToVisualEncode } from '../../visual-encoder/encode-mapping'; +import { scatterEncodeRequirement } from '../../../../../../ckb/encode'; + +import type { Advice } from '../../../../../types'; +import type { GenerateChartSpecParams } from '../types'; + +export function scatterPlot({ data, dataProps, encode: customEncode }: GenerateChartSpecParams): Advice['spec'] { + const encode = + customEncode ?? mapFieldsToVisualEncode({ fields: dataProps, encodeRequirements: scatterEncodeRequirement }); + const [field4X, field4Y, field4Color, field4Size] = [ + encode?.x?.[0], + encode?.y?.[0], + encode?.color?.[0], + encode?.size?.[0], + ]; if (!field4X || !field4Y) return null; @@ -17,61 +20,22 @@ export function scatterPlot(data: Data, dataProps: BasicDataPropertyForAdvice[]) type: 'point', data, encode: { - x: field4X.name, - y: field4Y.name, + x: field4X, + y: field4Y, }, }; if (field4Color) { - spec.encode.color = field4Color.name; + spec.encode.color = field4Color; } - return spec; -} - -export function bubbleChart(data: Data, dataProps: BasicDataPropertyForAdvice[]): Advice['spec'] { - const intervalFields = dataProps.filter((field) => hasSubset(field.levelOfMeasurements, ['Interval'])); - - const triple = { - x: intervalFields[0], - y: intervalFields[1], - corr: 0, - size: intervalFields[2], - }; - for (let i = 0; i < intervalFields.length; i += 1) { - for (let j = i + 1; j < intervalFields.length; j += 1) { - const p = pearson(intervalFields[i].rawData, intervalFields[j].rawData); - if (Math.abs(p) > triple.corr) { - triple.x = intervalFields[i]; - triple.y = intervalFields[j]; - triple.corr = p; - triple.size = intervalFields[[...Array(intervalFields.length).keys()].find((e) => e !== i && e !== j) || 0]; - } - } - } - - const field4X = triple.x; - const field4Y = triple.y; - const field4Size = triple.size; - - const field4Color = dataProps.find((field) => intersects(field.levelOfMeasurements, ['Nominal'])); - - // require x,y,size,color at the same time - if (!field4X || !field4Y || !field4Size) return null; - - const spec: Advice['spec'] = { - type: 'point', - data, - encode: { - x: field4X.name, - y: field4Y.name, - size: field4Size.name, - }, - }; - - if (field4Color) { - spec.encode.color = field4Color.name; + if (field4Size) { + spec.encode.size = field4Size; } return spec; } + +export function bubbleChart({ data, dataProps, encode }: GenerateChartSpecParams): Advice['spec'] { + return scatterPlot({ data, dataProps, encode }); +} diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/get-chart-spec.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/get-chart-spec.ts index 1d497798..98255c99 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/get-chart-spec.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/get-chart-spec.ts @@ -1,3 +1,5 @@ +import { isFunction } from 'lodash'; + import { CHART_IDS } from '../../../../../ckb'; import { @@ -25,7 +27,7 @@ import { import type { Data } from '../../../../../common/types'; import type { ChartId, ChartKnowledge } from '../../../../../ckb'; import type { BasicDataPropertyForAdvice } from '../../../../ruler'; -import type { Advice } from '../../../../types'; +import type { Advice, ChartEncodeMapping } from '../../../../types'; /** * Convert chartType + data to antv-spec @@ -41,69 +43,71 @@ export function getChartTypeSpec({ chartType, data, dataProps, + encode, chartKnowledge, }: { chartType: string; data: Data; dataProps: BasicDataPropertyForAdvice[]; + encode?: ChartEncodeMapping; chartKnowledge?: ChartKnowledge; }): Advice['spec'] { - // step 0: check whether the chartType is default in `ChartId` - // if not, use customized `toSpec` function - if (!CHART_IDS.includes(chartType as ChartId) && chartKnowledge) { - if (chartKnowledge.toSpec) { - const spec = chartKnowledge.toSpec(data, dataProps); - return spec; - } + if (isFunction(chartKnowledge.toSpec)) { + const spec = chartKnowledge.toSpec(data, dataProps, encode); + return spec; + } + + if (!CHART_IDS.includes(chartType as ChartId)) { return null; } + switch (chartType) { // pie case 'pie_chart': - return pieChart(data, dataProps); + return pieChart({ data, dataProps, encode }); case 'donut_chart': - return donutChart(data, dataProps); + return donutChart({ data, dataProps, encode }); // line case 'line_chart': - return lineChart(data, dataProps); + return lineChart({ data, dataProps, encode }); case 'step_line_chart': - return stepLineChart(data, dataProps); + return stepLineChart({ data, dataProps, encode }); // area case 'area_chart': - return areaChart(data, dataProps); + return areaChart({ data, dataProps, encode }); case 'stacked_area_chart': - return stackedAreaChart(data, dataProps); + return stackedAreaChart({ data, dataProps, encode }); case 'percent_stacked_area_chart': - return percentStackedAreaChart(data, dataProps); + return percentStackedAreaChart({ data, dataProps, encode }); // bar case 'bar_chart': - return barChart(data, dataProps); + return barChart({ data, dataProps, encode }); case 'grouped_bar_chart': - return groupedBarChart(data, dataProps); + return groupedBarChart({ data, dataProps, encode }); case 'stacked_bar_chart': - return stackedBarChart(data, dataProps); + return stackedBarChart({ data, dataProps, encode }); case 'percent_stacked_bar_chart': - return percentStackedBarChart(data, dataProps); + return percentStackedBarChart({ data, dataProps, encode }); // column case 'column_chart': - return columnChart(data, dataProps); + return columnChart({ data, dataProps, encode }); case 'grouped_column_chart': - return groupedColumnChart(data, dataProps); + return groupedColumnChart({ data, dataProps, encode }); case 'stacked_column_chart': - return stackedColumnChart(data, dataProps); + return stackedColumnChart({ data, dataProps, encode }); case 'percent_stacked_column_chart': - return percentStackedColumnChart(data, dataProps); + return percentStackedColumnChart({ data, dataProps, encode }); // scatter case 'scatter_plot': - return scatterPlot(data, dataProps); + return scatterPlot({ data, dataProps, encode }); // bubble case 'bubble_chart': - return bubbleChart(data, dataProps); + return bubbleChart({ data, dataProps, encode }); // histogram case 'histogram': - return histogram(data, dataProps); + return histogram({ data, dataProps, encode }); case 'heatmap': - return heatmap(data, dataProps); + return heatmap({ data, dataProps, encode }); // TODO other case 'kpi_panel' & 'table' // // FIXME kpi_panel and table spec to be null temporarily // const customChartType = ['kpi_panel', 'table']; diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/plugin-config.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/plugin-config.ts index 8fa71094..e62e6894 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/plugin-config.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/plugin-config.ts @@ -9,53 +9,81 @@ import type { SpecGeneratorInput, SpecGeneratorOutput, AdvisorPluginType, + Specification, } from '../../../../types'; -// todo 内置的 visualEncode 和 spec generate 插件需要明确支持哪些图表类型 +const refineSpec = ( + params: SpecGeneratorInput & { + chartType: string; + spec: Specification; + }, + context: AdvisorPipelineContext +) => { + const { chartType, dataProps, spec } = params; + const { options, advisor } = context; + const { refine = false, theme, colorOptions, smartColor } = options || {}; + const { themeColor = DEFAULT_COLOR, colorSchemeType, simulationType } = colorOptions || {}; + // apply spec processors such as design rules, theme, color, to improve spec + if (spec && refine) { + const partEncSpec = applyDesignRules(chartType, dataProps, advisor.ruleBase, spec, context); + deepMix(spec, partEncSpec); + } + + // custom theme + if (spec) { + if (theme && !smartColor) { + const partEncSpec = applyTheme(dataProps, spec, theme); + deepMix(spec, partEncSpec); + } else if (smartColor) { + const partEncSpec = applySmartColor(dataProps, spec, themeColor, colorSchemeType, simulationType); + deepMix(spec, partEncSpec); + } + } +}; + +const generateSpecForChartType = ( + input: SpecGeneratorInput & { + chartType: string; + }, + context: AdvisorPipelineContext +) => { + const { dataProps, data, chartType, encode } = input; + const chartKnowledge = context?.advisor?.ckb?.[chartType]; + const spec = getChartTypeSpec({ + chartType, + data, + dataProps, + encode, + chartKnowledge, + }); + + refineSpec({ ...input, spec, chartType }, context); + return spec; +}; + export const specGeneratorPlugin: AdvisorPluginType = { name: 'defaultSpecGenerator', stage: ['specGenerate'], execute: (input: SpecGeneratorInput, context: AdvisorPipelineContext): SpecGeneratorOutput => { - const { chartTypeRecommendations, dataProps, data } = input; - const { options, advisor } = context || {}; - const { refine = false, theme, colorOptions, smartColor } = options || {}; - const { themeColor = DEFAULT_COLOR, colorSchemeType, simulationType } = colorOptions || {}; - const advices = chartTypeRecommendations - ?.map((chartTypeAdvice) => { - const { chartType } = chartTypeAdvice; - const chartKnowledge = advisor.ckb[chartType]; - const chartTypeSpec = - chartKnowledge?.toSpec?.(data, dataProps) ?? - getChartTypeSpec({ - chartType, - data, - dataProps, - chartKnowledge, - }); - - // step 3: apply spec processors such as design rules, theme, color, to improve spec - if (chartTypeSpec && refine) { - const partEncSpec = applyDesignRules(chartType, dataProps, advisor.ruleBase, chartTypeSpec, context); - deepMix(chartTypeSpec, partEncSpec); - } - - // step 4: custom theme - if (chartTypeSpec) { - if (theme && !smartColor) { - const partEncSpec = applyTheme(dataProps, chartTypeSpec, theme); - deepMix(chartTypeSpec, partEncSpec); - } else if (smartColor) { - const partEncSpec = applySmartColor(dataProps, chartTypeSpec, themeColor, colorSchemeType, simulationType); - deepMix(chartTypeSpec, partEncSpec); - } - } - return { - type: chartTypeAdvice.chartType, - spec: chartTypeSpec, - score: chartTypeAdvice.score, - }; - }) - .filter((advices) => advices.spec); + const chartTypeRecommendations = + input.chartTypeRecommendations ?? + (input.chartType + ? [ + { + chartType: input.chartType, + encode: input.encode, + }, + ] + : []); + const advices = chartTypeRecommendations?.map((chartTypeAdvice) => { + const { chartType, encode } = chartTypeAdvice; + const spec = generateSpecForChartType({ ...input, chartType, encode }, context); + return { + ...chartTypeAdvice, + spec, + }; + }); + return { advices }; }, }; diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/types.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/types.ts new file mode 100644 index 00000000..206318bf --- /dev/null +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/spec-generator/types.ts @@ -0,0 +1,8 @@ +import type { Data } from '../../../../../common/types'; +import type { BasicDataPropertyForAdvice, ChartEncodeMapping } from '../../../../types'; + +export type GenerateChartSpecParams = { + data: Data; + dataProps: BasicDataPropertyForAdvice[]; + encode: ChartEncodeMapping; +}; diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/encode-mapping.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/encode-mapping.ts new file mode 100644 index 00000000..be94b6d6 --- /dev/null +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/encode-mapping.ts @@ -0,0 +1,132 @@ +import { map, mapValues, size } from 'lodash'; + +import { compare, intersects } from '../../../../utils'; +import { isParentChild } from '../../../../../data'; +import { findTopCorrFields } from '../../../../utils/top-corr-fields'; + +import type { DataPrerequisite } from '../../../../../ckb'; +import type { EncodeRequirements } from '../../../../../ckb/encode'; +import type { BasicDataPropertyForAdvice, ChartEncodeMapping } from '../../../../types'; + +// @todo chenluli encoding mapping 也考虑纳入规则系统中,可用规则系统来约束推荐逻辑 +/** find matching fields for encode requirement */ +function findAndMarkFields({ + requirement, + fields, + fieldUsage, +}: { + requirement: DataPrerequisite; + fields: BasicDataPropertyForAdvice[]; + fieldUsage: Set; +}) { + const matchingFields: BasicDataPropertyForAdvice[] = []; + let count = requirement.minQty; + requirement.fieldConditions.forEach((targetLoM) => { + if (count <= 0) return; + const filteredFields = fields + .filter((field) => !fieldUsage.has(field.name) && field.levelOfMeasurements.includes(targetLoM)) + .slice(0, count); + count -= filteredFields.length; + matchingFields.push(...filteredFields); + filteredFields.forEach((field) => fieldUsage.add(field.name)); + }); + + // Use other type of fields if not enough matching fields + if (count > 0) { + const remainingFields = fields.filter((field) => !fieldUsage.has(field.name)).slice(0, count); + matchingFields.push(...remainingFields); + remainingFields.forEach((field) => fieldUsage.add(field.name)); + } + return matchingFields; +} + +export function mapFieldsToVisualEncode({ + fields = [], + encodeRequirements = {}, +}: { + fields: BasicDataPropertyForAdvice[]; + encodeRequirements: EncodeRequirements; +}): ChartEncodeMapping { + const encodeMapping: Record = {}; + const fieldUsage: Set = new Set(); + const sortedFields = fields.sort(compare); + + // Iterate through each encode requirement + // 遍历每个 encode 的满足条件,先满足每个 encode key 对字段的最小数目要求 + Object.entries(encodeRequirements).forEach(([encodeKey, requirement]) => { + encodeMapping[encodeKey] = findAndMarkFields({ requirement, fields: sortedFields, fieldUsage }); + }); + + // assign remaining fields to the available slot + // 如果还有剩余字段没有映射上,则填充至最大数量还能满足的部分 + const isAcceptableQty = (encodeKey: string) => { + const currentCount = size(encodeMapping[encodeKey]); + const maxQty = encodeRequirements[encodeKey]?.maxQty; + return maxQty === '*' || currentCount < maxQty; + }; + sortedFields + .filter((field) => !fieldUsage.has(field.name)) + .forEach((field) => { + const matchingEncodeKey = + Object.keys(encodeRequirements).find( + (encodeKey) => + isAcceptableQty(encodeKey) && + intersects(field.levelOfMeasurements, encodeRequirements[encodeKey]?.fieldConditions) + ) ?? Object.keys(encodeRequirements).find((encodeKey) => isAcceptableQty(encodeKey)); + + if (matchingEncodeKey) { + encodeMapping[matchingEncodeKey].push(field); + fieldUsage.add(field.name); + } + }); + + // todo 填充 slots 完成后,进一步优化调整 + // 1. 如果 x 轴和 series 都是 nominal 类型字段:distinct 大的作为 x 轴;如果有层级关系,则使用 parent 作为 x 轴; + const xField = encodeMapping.x?.[0]; + const colorField = encodeMapping.color?.[0]; + if ( + size(encodeMapping.x) === 1 && + size(encodeMapping.color) === 1 && + xField.levelOfMeasurements?.includes('Nominal') && + colorField.levelOfMeasurements?.includes('Nominal') + ) { + if (isParentChild(colorField?.rawData, xField?.rawData) || colorField.distinct > xField.distinct) { + encodeMapping.x = [colorField]; + encodeMapping.color = [xField]; + } + } + // 2. 如果 x, y, size 都是 interval 类型字段,则选 correlation 较大的作为 x,y 另一个作为 size + const yField = encodeMapping.y?.[0]; + const sizeField = encodeMapping.size?.[0]; + if ( + size(encodeMapping.x) === 1 && + size(encodeMapping.y) === 1 && + size(encodeMapping.size) === 1 && + xField.levelOfMeasurements?.includes('Interval') && + yField.levelOfMeasurements?.includes('Interval') && + sizeField.levelOfMeasurements?.includes('Interval') + ) { + const { x, y, size } = findTopCorrFields([xField, yField, colorField]); + encodeMapping.x = [x]; + encodeMapping.y = [y]; + encodeMapping.size = [size]; + } + + // check for unmet encoding requirements + Object.entries(encodeRequirements).forEach(([key, requirement]) => { + if (encodeMapping[key].length < requirement.minQty) { + // eslint-disable-next-line no-console + console.warn( + `Requirement for ${key} not satisfied. Minimum required fields: ${requirement.minQty}, but got ${encodeMapping[key].length}.` + ); + } + const remainingFields = sortedFields.filter((field) => !fieldUsage.has(field.name)); + if (remainingFields.length) { + // eslint-disable-next-line no-console + console.warn( + `No available visual encoding slots for remaining ${remainingFields.length} fields. Excess fields are ignored.` + ); + } + }); + return mapValues(encodeMapping, (fields) => map(fields, 'name')); +} diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/plugin-config.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/plugin-config.ts index b5cc147f..9cdcff82 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/plugin-config.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/plugin-config.ts @@ -1,10 +1,65 @@ -import type { AdvisorPluginType, VisualEncoderInput, VisualEncoderOutput } from '../../../../types'; +import { isFunction } from 'lodash'; + +import { chartType2EncodeRequirement } from '../../../../../ckb/encode'; + +import { mapFieldsToVisualEncode } from './encode-mapping'; + +import type { + AdvisorPipelineContext, + AdvisorPluginType, + ChartEncodeMapping, + ScoringResultForChartType, + VisualEncoderInput, + VisualEncoderOutput, +} from '../../../../types'; + +const getEncodeMapping = ( + params: VisualEncoderInput & { + chartType: string; + }, + context: AdvisorPipelineContext +): ChartEncodeMapping => { + const { chartType, dataProps, data } = params; + const chartKnowledgeBase = context?.advisor?.ckb; + if (isFunction(chartKnowledgeBase[chartType]?.toEncode)) { + return chartKnowledgeBase[chartType]?.toEncode({ data, dataProps, context }); + } + const encode = mapFieldsToVisualEncode({ + fields: dataProps, + encodeRequirements: chartKnowledgeBase[chartType]?.encodePres ?? chartType2EncodeRequirement[chartType], + }); + return encode; +}; export const visualEncoderPlugin: AdvisorPluginType = { name: 'defaultVisualEncoder', stage: ['encode'], - execute: (input) => { - // todo 从 spec-generator 中拆分出来核心 encode 部分 - return input; + execute: (input, context) => { + const { chartType } = input; + const chartTypeRecommendations: ScoringResultForChartType[] = + input?.chartTypeRecommendations ?? + (chartType + ? [ + { + chartType, + score: 1, + }, + ] + : []); + + const advices = chartTypeRecommendations.map((chartTypeAdvice) => { + const encode = getEncodeMapping( + { + ...input, + chartType: chartTypeAdvice.chartType, + }, + context + ); + return { + ...chartTypeAdvice, + encode, + }; + }); + return { chartTypeRecommendations: advices }; }, }; diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/split-fields.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/split-fields.ts index a3a374a9..18a79e92 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/split-fields.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/visual-encoder/split-fields.ts @@ -1,22 +1,41 @@ import { isParentChild } from '../../../../../data'; import { compare, hasSubset, intersects } from '../../../../utils'; +import { LEVEL_OF_MEASUREMENTS, type LevelOfMeasurement } from '../../../../../ckb'; import type { BasicDataPropertyForAdvice } from '../../../../types'; type ReturnField = BasicDataPropertyForAdvice | undefined; +export const getFieldByLoM = ({ + dataProps = [], + levelOfMeasurements, + count = 1, +}: { + dataProps: BasicDataPropertyForAdvice[]; + levelOfMeasurements: LevelOfMeasurement[]; + count?: number; +}) => { + return dataProps.filter((field) => intersects(field.levelOfMeasurements, levelOfMeasurements)).slice(0, count); +}; + +export const splitFields = (dataProps: BasicDataPropertyForAdvice[] = [], filteredFieldNames: string[] = []) => { + const filteredFields = dataProps.filter((field) => !filteredFieldNames.includes(field.name)); + const levelOfMeasurementFieldMap: Partial> = {}; + LEVEL_OF_MEASUREMENTS.forEach((levelOfMeasurement) => { + const [field] = getFieldByLoM({ dataProps: filteredFields, levelOfMeasurements: ['Nominal'] }); + levelOfMeasurementFieldMap[levelOfMeasurement] = field; + }); + return levelOfMeasurementFieldMap; +}; + export function splitAngleColor(dataProps: BasicDataPropertyForAdvice[]): [ReturnField, ReturnField] { + const splittedFields = splitFields(dataProps); const field4Color = - dataProps.find((field) => intersects(field.levelOfMeasurements, ['Nominal'])) ?? - dataProps.find((field) => intersects(field.levelOfMeasurements, ['Time', 'Ordinal'])) ?? - dataProps.find((field) => intersects(field.levelOfMeasurements, ['Interval'])); + splittedFields.Nominal ?? splittedFields.Ordinal ?? splittedFields.Time ?? splittedFields.Interval; + const usedFieldKey = field4Color?.name ? [field4Color?.name] : []; + const filteredFieldsMap = splitFields(dataProps, usedFieldKey); const field4Angle = - dataProps - .filter((field) => field !== field4Color) - .find((field) => intersects(field.levelOfMeasurements, ['Interval'])) ?? - dataProps - .filter((field) => field !== field4Color) - .find((field) => intersects(field.levelOfMeasurements, ['Nominal', 'Time', 'Ordinal'])); + filteredFieldsMap.Interval ?? filteredFieldsMap.Nominal ?? filteredFieldsMap.Ordinal ?? splittedFields.Time; return [field4Color, field4Angle]; } diff --git a/packages/ava/src/advisor/advisor.ts b/packages/ava/src/advisor/advisor.ts index a2d3e53f..69ce61c8 100644 --- a/packages/ava/src/advisor/advisor.ts +++ b/packages/ava/src/advisor/advisor.ts @@ -112,6 +112,9 @@ export class Advisor { options: params.options, }; const adviseResult = await this.pipeline.execute(params); + if (params.options?.requireSpec !== false) { + return adviseResult.advices?.filter((advice) => advice.spec); + } return adviseResult.advices; } diff --git a/packages/ava/src/advisor/pipeline/component.ts b/packages/ava/src/advisor/pipeline/component.ts index 1f4c7e9d..abc40a9b 100644 --- a/packages/ava/src/advisor/pipeline/component.ts +++ b/packages/ava/src/advisor/pipeline/component.ts @@ -97,7 +97,7 @@ export class BaseComponent { execute(params: Input): Output { if (this.hasAsyncPlugin) { // eslint-disable-next-line no-console - console.warn('存在异步执行的插件,请使用 executeAsync'); + console.warn('async plugins detected, please use executeAsync'); } const pluginsOutput = {}; this.syncPluginManager.call(params, pluginsOutput); diff --git a/packages/ava/src/advisor/types/component.ts b/packages/ava/src/advisor/types/component.ts index 6c046522..321cff9e 100644 --- a/packages/ava/src/advisor/types/component.ts +++ b/packages/ava/src/advisor/types/component.ts @@ -2,7 +2,6 @@ import type { Data } from '@antv/g2'; import type { Advice, ScoringResultForChartType, AdvisorPipelineContext } from './pipeline'; import type { BasicDataPropertyForAdvice } from '../ruler'; import type { ChartId } from '../../ckb'; -import type { MarkEncode } from './mark'; export type PipelineStageType = 'dataAnalyze' | 'chartTypeRecommend' | 'encode' | 'specGenerate'; @@ -39,27 +38,37 @@ export type ChartTypeRecommendInput = { export type ChartTypeRecommendOutput = { chartTypeRecommendations: ScoringResultForChartType[] }; -export type SpecGeneratorInput = { - // todo 实际上不应该需要 score 信息 - chartTypeRecommendations: ScoringResultForChartType[]; +export type VisualEncoderInput = { data: Data; - // 单独调用 SpecGenerator 时,还要额外计算 dataProps 么 dataProps: BasicDataPropertyForAdvice[]; - encode?: MarkEncode; + chartType?: string; + chartTypeRecommendations?: ScoringResultForChartType[]; }; -export type SpecGeneratorOutput = { - advices: (Omit & { - spec: Record | null; - })[]; + +export type ChartEncodeMapping = { + x?: string[]; + y?: string[]; + color?: string[]; + size?: string[]; + [key: string]: string[]; }; -export type VisualEncoderInput = { - chartType: ChartId; +export type VisualEncoderOutput = { + encode?: ChartEncodeMapping; + chartTypeRecommendations?: (ScoringResultForChartType & { encode?: ChartEncodeMapping })[]; +}; + +export type SpecGeneratorInput = { + data: Data; dataProps: BasicDataPropertyForAdvice[]; - chartTypeRecommendations: ScoringResultForChartType[]; + encode?: ChartEncodeMapping; + chartType?: ChartId; + // todo 实际上不应该需要 score 信息 + chartTypeRecommendations: (ScoringResultForChartType & { encode?: ChartEncodeMapping })[]; }; -export type VisualEncoderOutput = { - encode?: MarkEncode; - chartTypeRecommendations?: ScoringResultForChartType[]; +export type SpecGeneratorOutput = { + advices: (Omit & { + spec: Record | null; + })[]; }; diff --git a/packages/ava/src/advisor/types/mark.ts b/packages/ava/src/advisor/types/mark.ts index 0bf79f6f..67c50f15 100644 --- a/packages/ava/src/advisor/types/mark.ts +++ b/packages/ava/src/advisor/types/mark.ts @@ -27,12 +27,4 @@ export type G2ChartSpec = Omit & { encode: MarkEncode }; /** 原 G2 spec 去掉复杂 Encode 类型并添加简易版(带字段类型的) Encode 类型 */ export type ChartSpecWithEncodeType = Omit & { encode: MarkEncodeWithType }; -export type ChartEncoding = { - x?: string; - y?: string; - color?: string; - theta?: string; - size?: string; -}; - export type { Specification }; diff --git a/packages/ava/src/advisor/utils/top-corr-fields.ts b/packages/ava/src/advisor/utils/top-corr-fields.ts new file mode 100644 index 00000000..b0b7c6e2 --- /dev/null +++ b/packages/ava/src/advisor/utils/top-corr-fields.ts @@ -0,0 +1,33 @@ +import { filter } from 'lodash'; + +import { pearson } from '../../data'; +import { BasicDataPropertyForAdvice } from '../ruler'; + +export const findTopCorrFields = (fields: BasicDataPropertyForAdvice[]) => { + const intervalFields = filter(fields, (field) => field.levelOfMeasurements.includes('Interval')); + if (intervalFields.length < 3) { + return { + x: intervalFields[0], + y: intervalFields[1], + }; + } + const triple = { + x: intervalFields[0], + y: intervalFields[1], + corr: 0, + size: intervalFields[2], + }; + for (let i = 0; i < intervalFields.length; i += 1) { + for (let j = i + 1; j < intervalFields.length; j += 1) { + const p = pearson(intervalFields[i].rawData, intervalFields[j].rawData); + if (Math.abs(p) > triple.corr) { + triple.x = intervalFields[i]; + triple.y = intervalFields[j]; + triple.corr = p; + triple.size = intervalFields[[...Array(intervalFields.length).keys()].find((e) => e !== i && e !== j) || 0]; + } + } + } + + return { x: triple.x, y: triple.y, size: triple.size }; +}; diff --git a/packages/ava/src/ckb/encode.ts b/packages/ava/src/ckb/encode.ts new file mode 100644 index 00000000..e03f701a --- /dev/null +++ b/packages/ava/src/ckb/encode.ts @@ -0,0 +1,132 @@ +import { ChartId, EncodePrerequisite } from './types'; + +export type EncodeRequirements = Record; + +// todo @chenluli 全部迁移进入 ckb +export const lineEncodeRequirement: EncodeRequirements = { + x: { + maxQty: 1, + minQty: 1, + fieldConditions: ['Time', 'Ordinal', 'Nominal'], + }, + y: { + maxQty: '*', + minQty: 1, + fieldConditions: ['Interval'], + }, + color: { + maxQty: '*', + minQty: 0, + fieldConditions: ['Nominal', 'Ordinal', 'Time'], + }, +}; + +export const pieEncodeRequirement: EncodeRequirements = { + y: { + maxQty: 1, + minQty: 1, + fieldConditions: ['Interval'], + }, + color: { + minQty: 1, + maxQty: 1, // todo 是否限制为 1 个还是允许多个;多个切片维度时,实际上会变成旭日图 + fieldConditions: ['Nominal', 'Ordinal', 'Time'], + }, +}; + +export const barEncodeRequirement: EncodeRequirements = { + x: { + maxQty: 1, + minQty: 1, + fieldConditions: ['Nominal', 'Ordinal', 'Time'], + }, + y: { + maxQty: '*', + minQty: 1, + fieldConditions: ['Interval'], + }, + color: { + maxQty: '*', + minQty: 0, + fieldConditions: ['Nominal', 'Ordinal', 'Time'], + }, +}; + +export const scatterEncodeRequirement: EncodeRequirements = { + x: { + maxQty: 1, + minQty: 1, + fieldConditions: ['Interval'], + }, + y: { + maxQty: 1, + minQty: 1, + fieldConditions: ['Interval'], + }, + size: { + maxQty: 1, + minQty: 0, + fieldConditions: ['Interval'], + }, + color: { + maxQty: '*', + minQty: 0, + fieldConditions: ['Nominal', 'Ordinal', 'Time'], + }, +}; + +export const histogramEncodeRequirement: EncodeRequirements = { + x: { + maxQty: 1, + minQty: 1, + fieldConditions: ['Interval'], + }, + color: { + maxQty: '*', + minQty: 0, + fieldConditions: ['Nominal', 'Ordinal', 'Time'], + }, +}; + +export const heatmapEncodeRequirement: EncodeRequirements = { + x: { + maxQty: 1, + minQty: 1, + fieldConditions: ['Nominal', 'Ordinal', 'Time', 'Interval'], + }, + y: { + maxQty: 1, + minQty: 1, + fieldConditions: ['Nominal', 'Ordinal', 'Time', 'Interval'], + }, + color: { + maxQty: '*', + minQty: 0, + fieldConditions: ['Interval'], + }, +}; + +export const areaEncodeRequirement = lineEncodeRequirement; +export const columnEncodeRequirement = barEncodeRequirement; + +export const chartType2EncodeRequirement: Partial> = { + line_chart: lineEncodeRequirement, + pie_chart: pieEncodeRequirement, + donut_chart: pieEncodeRequirement, + step_line_chart: lineEncodeRequirement, + area_chart: areaEncodeRequirement, + stacked_area_chart: areaEncodeRequirement, + percent_stacked_area_chart: areaEncodeRequirement, + bar_chart: barEncodeRequirement, + grouped_bar_chart: barEncodeRequirement, + stacked_bar_chart: barEncodeRequirement, + percent_stacked_bar_chart: barEncodeRequirement, + column_chart: barEncodeRequirement, + grouped_column_chart: columnEncodeRequirement, + stacked_column_chart: columnEncodeRequirement, + percent_stacked_column_chart: columnEncodeRequirement, + scatter_plot: scatterEncodeRequirement, + bubble_chart: scatterEncodeRequirement, + histogram: histogramEncodeRequirement, + heatmap: heatmapEncodeRequirement, +}; diff --git a/packages/ava/src/ckb/types.ts b/packages/ava/src/ckb/types.ts index b71f9e2a..382319e4 100644 --- a/packages/ava/src/ckb/types.ts +++ b/packages/ava/src/ckb/types.ts @@ -1,6 +1,7 @@ import * as constants from './constants'; import type { Data, Specification } from '../common/types'; +import type { AdvisorPipelineContext, BasicDataPropertyForAdvice, ChartEncodeMapping } from '../advisor'; /** * TS type of standard IDs for each chart type. @@ -98,6 +99,13 @@ export type DataPrerequisite = { fieldConditions: LevelOfMeasurement[]; }; +/** + * TS type of A prerequisite for being able to mapping to a specific chart visual encode channel + * + * 图表视觉映射所需的字段类型,例如折线图 x 轴需要1个日期型字段,y 轴需要至少1个数值型字段 + */ +export type EncodePrerequisite = DataPrerequisite; + /** * TS type of channels. * @@ -135,6 +143,7 @@ export type PureChartKnowledge = { coord: CoordinateSystem[]; category: GraphicCategory[]; shape: Shape[]; + encodePres?: Record; dataPres: DataPrerequisite[]; channel: Channel[]; recRate: RecommendRating; @@ -151,17 +160,32 @@ export type PureChartKnowledge = { export type ChartKnowledge = { id: string; name: string; - alias: string[]; - family: string[]; - def: string; - purpose: string[]; - coord: string[]; - category: string[]; - shape: string[]; + alias?: string[]; + family?: string[]; + def?: string; + purpose?: string[]; + coord?: string[]; + category?: string[]; + shape?: string[]; + encodePres?: Record; dataPres: (Omit & { fieldConditions: string[] })[]; - channel: string[]; - recRate: string; - toSpec?: (data: Data, dataProps: any) => Specification | null; + channel?: string[]; + recRate?: string; + toEncode?: ({ + data, + dataProps, + context, + }: { + data?: Data; + dataProps?: BasicDataPropertyForAdvice[]; + context?: AdvisorPipelineContext; + }) => Record; + toSpec?: ( + data: Data, + dataProps: any, + encode?: ChartEncodeMapping, + context?: AdvisorPipelineContext + ) => Specification | null; }; /**