From 85cc0ce50363cc6474e1f29fbe9354cf68db4b4f Mon Sep 17 00:00:00 2001 From: chenluli Date: Mon, 17 Jun 2024 16:59:39 +0800 Subject: [PATCH] feat(ava/advisor): add async and condition type to advisor plugin --- .../chart-type-recommend/get-chart-Type.ts | 5 +- .../chart-type-recommend/plugin-config.ts | 3 +- .../presets/spec-generator/plugin-config.ts | 4 +- .../spec-processors/apply-design-rules.ts | 15 +++- .../presets/visual-encoder/plugin-config.ts | 4 +- .../score-calculator/compute-score.ts | 20 ++++- .../score-calculator/score-rules.ts | 8 +- packages/ava/src/advisor/advisor.ts | 39 ++++++--- .../src/advisor/lint-pipeline/lintRules.ts | 19 +++-- .../ava/src/advisor/pipeline/component.ts | 79 +++++++++---------- packages/ava/src/advisor/pipeline/pipeline.ts | 6 +- packages/ava/src/advisor/ruler/types.ts | 5 +- packages/ava/src/advisor/types/component.ts | 10 ++- 13 files changed, 139 insertions(+), 78 deletions(-) diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/chart-type-recommend/get-chart-Type.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/chart-type-recommend/get-chart-Type.ts index 266b78b8..826bd4aa 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/chart-type-recommend/get-chart-Type.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/chart-type-recommend/get-chart-Type.ts @@ -6,6 +6,7 @@ import type { BasicDataPropertyForAdvice, RuleModule, AdvisorOptions, + AdvisorPipelineContext, } from '../../../../types'; export const getChartTypeRecommendations = ({ @@ -13,15 +14,17 @@ export const getChartTypeRecommendations = ({ dataProps, ruleBase, options, + advisorContext, }: { dataProps: BasicDataPropertyForAdvice[]; chartWIKI: Record; ruleBase: Record; options?: AdvisorOptions; + advisorContext?: Pick; }) => { const chatTypes = Object.keys(chartWIKI); const list: ScoringResultForChartType[] = chatTypes.map((chartType) => { - return scoreRules(chartType, chartWIKI, dataProps, ruleBase, options); + return scoreRules(chartType, chartWIKI, dataProps, ruleBase, options, advisorContext); }); // filter and sorter diff --git a/packages/ava/src/advisor/advise-pipeline/plugin/presets/chart-type-recommend/plugin-config.ts b/packages/ava/src/advisor/advise-pipeline/plugin/presets/chart-type-recommend/plugin-config.ts index 2f0fe737..ad966ce7 100644 --- a/packages/ava/src/advisor/advise-pipeline/plugin/presets/chart-type-recommend/plugin-config.ts +++ b/packages/ava/src/advisor/advise-pipeline/plugin/presets/chart-type-recommend/plugin-config.ts @@ -12,12 +12,13 @@ export const chartTypeRecommendPlugin: AdvisorPluginType, - chartSpec: Specification + chartSpec: Specification, + advisorContext?: Pick ) { const toCheckRules = Object.values(ruleBase).filter( (rule: RuleModule) => - rule.type === 'DESIGN' && rule.trigger({ dataProps, chartType }) && !ruleBase[rule.id].option?.off + rule.type === 'DESIGN' && rule.trigger({ dataProps, chartType, advisorContext }) && !ruleBase[rule.id].option?.off ); const encodingSpec = toCheckRules.reduce((lastSpec, rule: RuleModule) => { - const relatedSpec = (rule as DesignRuleModule).optimizer(dataProps, chartSpec); + const relatedSpec = (rule as DesignRuleModule).optimizer(dataProps, chartSpec, advisorContext); return deepMix(lastSpec, relatedSpec); }, {}); return encodingSpec; 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 c5654262..b5cc147f 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,6 +1,6 @@ -import type { AdvisorPluginType, VisualEncoderInput } from '../../../../types'; +import type { AdvisorPluginType, VisualEncoderInput, VisualEncoderOutput } from '../../../../types'; -export const visualEncoderPlugin: AdvisorPluginType = { +export const visualEncoderPlugin: AdvisorPluginType = { name: 'defaultVisualEncoder', stage: ['encode'], execute: (input) => { diff --git a/packages/ava/src/advisor/advise-pipeline/score-calculator/compute-score.ts b/packages/ava/src/advisor/advise-pipeline/score-calculator/compute-score.ts index 57e91ac9..27384095 100644 --- a/packages/ava/src/advisor/advise-pipeline/score-calculator/compute-score.ts +++ b/packages/ava/src/advisor/advise-pipeline/score-calculator/compute-score.ts @@ -1,7 +1,7 @@ import { Info, RuleModule } from '../../ruler'; import { DEFAULT_RULE_WEIGHTS } from '../../constants'; -import type { ScoringResultForRule } from '../../types'; +import type { AdvisorPipelineContext, ScoringResultForRule } from '../../types'; import type { ChartRuleModule } from '../../ruler/types'; import type { ChartId, ChartKnowledgeBase } from '../../../ckb'; @@ -13,7 +13,8 @@ export const computeScore = ( ruleBase: Record, ruleType: 'HARD' | 'SOFT', info: Info, - log: ScoringResultForRule[] + log: ScoringResultForRule[], + advisorContext?: Pick ) => { // initial score is 1 for HARD rules and 0 for SOFT rules let computedScore = 1; @@ -21,12 +22,23 @@ export const computeScore = ( .filter((r: RuleModule) => { const weight = r.option?.weight || defaultWeights[r.id] || 1; const extra = r.option?.extra; - return r.type === ruleType && r.trigger({ ...info, weight, ...extra, chartType, chartWIKI }) && !r.option?.off; + return ( + r.type === ruleType && + r.trigger({ ...info, weight, ...extra, chartType, chartWIKI, advisorContext }) && + !r.option?.off + ); }) .forEach((r: RuleModule) => { const weight = r.option?.weight || defaultWeights[r.id] || 1; const extra = r.option?.extra; - const base = (r as ChartRuleModule).validator({ ...info, weight, ...extra, chartType, chartWIKI }) as number; + const base = (r as ChartRuleModule).validator({ + ...info, + weight, + ...extra, + chartType, + chartWIKI, + advisorContext, + }) as number; const score = weight * base; computedScore *= score; log.push({ phase: 'ADVISE', ruleId: r.id, score, base, weight, ruleType }); diff --git a/packages/ava/src/advisor/advise-pipeline/score-calculator/score-rules.ts b/packages/ava/src/advisor/advise-pipeline/score-calculator/score-rules.ts index 872d6ef9..93086108 100644 --- a/packages/ava/src/advisor/advise-pipeline/score-calculator/score-rules.ts +++ b/packages/ava/src/advisor/advise-pipeline/score-calculator/score-rules.ts @@ -4,6 +4,7 @@ import { AdvisorOptions, ScoringResultForChartType, ScoringResultForRule, + AdvisorPipelineContext, } from '../../types'; import { computeScore } from './compute-score'; @@ -24,7 +25,8 @@ export function scoreRules( chartWIKI: ChartKnowledgeBase, dataProps: BasicDataPropertyForAdvice[], ruleBase: Record, - options?: AdvisorOptions + options?: AdvisorOptions, + advisorContext?: Pick ): ScoringResultForChartType { const purpose = options ? options.purpose : ''; const preferences = options ? options.preferences : undefined; @@ -34,7 +36,7 @@ export function scoreRules( const info = { dataProps, chartType, purpose, preferences }; - const hardScore = computeScore(chartType, chartWIKI, ruleBase, 'HARD', info, log); + const hardScore = computeScore(chartType, chartWIKI, ruleBase, 'HARD', info, log, advisorContext); // Hard-Rule pruning if (hardScore === 0) { @@ -42,7 +44,7 @@ export function scoreRules( return result; } - const softScore = computeScore(chartType, chartWIKI, ruleBase, 'SOFT', info, log); + const softScore = computeScore(chartType, chartWIKI, ruleBase, 'SOFT', info, log, advisorContext); const score = hardScore * softScore; diff --git a/packages/ava/src/advisor/advisor.ts b/packages/ava/src/advisor/advisor.ts index c41f9d73..d2cca061 100644 --- a/packages/ava/src/advisor/advisor.ts +++ b/packages/ava/src/advisor/advisor.ts @@ -1,3 +1,5 @@ +import { forEach, size } from 'lodash'; + import { ckb } from '../ckb'; import { processRuleCfg } from './ruler'; @@ -5,7 +7,12 @@ import { dataToAdvices } from './advise-pipeline'; import { checkRules } from './lint-pipeline/check-rules'; import { BaseComponent } from './pipeline/component'; import { Pipeline } from './pipeline/pipeline'; -import { dataProcessorPlugin, specGeneratorPlugin, chartTypeRecommendPlugin } from './advise-pipeline/plugin'; +import { + dataProcessorPlugin, + specGeneratorPlugin, + chartTypeRecommendPlugin, + visualEncoderPlugin, +} from './advise-pipeline/plugin'; import type { ChartKnowledgeBase } from '../ckb'; import type { RuleModule } from './ruler'; @@ -51,7 +58,7 @@ export class Advisor { context: AdvisorPipelineContext; - plugins: AdvisorPluginType[]; + plugins: AdvisorPluginType[] = []; pipeline: Pipeline; @@ -73,8 +80,10 @@ export class Advisor { this.context = { advisor: this, extra }; this.initDefaultComponents(); const defaultComponents = [this.dataAnalyzer, this.chartTypeRecommender, this.chartEncoder, this.specGenerator]; - this.plugins = plugins; - this.pipeline = new Pipeline({ components: components ?? defaultComponents }); + this.registerPlugins(plugins); + this.pipeline = new Pipeline({ + components: components ?? defaultComponents, + }); } private initDefaultComponents() { @@ -83,7 +92,7 @@ export class Advisor { plugins: [chartTypeRecommendPlugin], context: this.context, }); - // this.chartEncoder = new BaseComponent('chartEncode', { plugins: [visualEncoderPlugin], context: this.context }); + this.chartEncoder = new BaseComponent('chartEncode', { plugins: [visualEncoderPlugin], context: this.context }); this.specGenerator = new BaseComponent('specGenerate', { plugins: [specGeneratorPlugin], context: this.context }); } @@ -93,6 +102,11 @@ export class Advisor { return adviseResult.advices; } + adviseWithLog(params: AdviseParams): AdviseResult { + const adviseResult = dataToAdvices({ adviseParams: params, ckb: this.ckb, ruleBase: this.ruleBase }); + return adviseResult; + } + async adviseAsync(params: AdviseParams): Promise { this.context = { ...this.context, @@ -103,10 +117,7 @@ export class Advisor { return adviseResult.advices; } - adviseWithLog(params: AdviseParams): AdviseResult { - const adviseResult = dataToAdvices({ adviseParams: params, ckb: this.ckb, ruleBase: this.ruleBase }); - return adviseResult; - } + // todo 补充 adviseAsyncWithLog lint(params: LintParams): Lint[] { const lintResult = checkRules(params, this.ruleBase, this.ckb); @@ -118,7 +129,7 @@ export class Advisor { return lintResult; } - registerPlugins(plugins: AdvisorPluginType[]) { + registerPlugins(plugins: AdvisorPluginType[] = []) { const stage2Components: Record = { dataAnalyze: this.dataAnalyzer, chartTypeRecommend: this.chartTypeRecommender, @@ -126,10 +137,16 @@ export class Advisor { specGenerate: this.specGenerator, }; - plugins.forEach((plugin) => { + forEach(plugins, (plugin) => { + this.plugins.push(plugin); if (typeof plugin.stage === 'string') { const pipelineComponent = stage2Components[plugin.stage]; pipelineComponent.registerPlugin(plugin); + return; + } + if (size(plugin.stage) === 1) { + const pipelineComponent = stage2Components[plugin.stage[0]]; + pipelineComponent.registerPlugin(plugin); } }); } diff --git a/packages/ava/src/advisor/lint-pipeline/lintRules.ts b/packages/ava/src/advisor/lint-pipeline/lintRules.ts index c3e56b30..2df95567 100644 --- a/packages/ava/src/advisor/lint-pipeline/lintRules.ts +++ b/packages/ava/src/advisor/lint-pipeline/lintRules.ts @@ -1,7 +1,7 @@ import { Info, RuleModule } from '../ruler'; import type { Specification } from '../../common/types'; -import type { ScoringResultForRule, Lint } from '../types'; +import type { ScoringResultForRule, Lint, AdvisorPipelineContext } from '../types'; import type { ChartRuleModule, DesignRuleModule } from '../ruler'; import type { ChartKnowledgeBase } from '../../ckb'; @@ -12,7 +12,8 @@ export function lintRules( log: ScoringResultForRule[], lints: Lint[], ckb: ChartKnowledgeBase, - spec?: Specification + spec?: Specification, + advisorContext?: Pick ) { const judge = (type: 'HARD' | 'SOFT' | 'DESIGN') => { if (ruleTypeToLint === 'DESIGN') { @@ -24,20 +25,28 @@ export function lintRules( Object.values(ruleBase) .filter((r: RuleModule) => { const { weight, extra } = r.option || {}; - return judge(r.type) && !r.option?.off && r.trigger({ ...info, weight, ...extra, chartWIKI: ckb }); + return ( + judge(r.type) && !r.option?.off && r.trigger({ ...info, weight, ...extra, chartWIKI: ckb, advisorContext }) + ); }) .forEach((r: RuleModule) => { const { type, id, docs } = r; let score: number; if (ruleTypeToLint === 'DESIGN') { - const fix = (r as DesignRuleModule).optimizer(info.dataProps, spec); + const fix = (r as DesignRuleModule).optimizer(info.dataProps, spec, advisorContext); // no fix -> means no violation score = Object.keys(fix).length === 0 ? 1 : 0; lints.push({ type, id, score, fix, docs }); } else { const { weight, extra } = r.option || {}; // no weight for linter's result - score = (r as ChartRuleModule).validator({ ...info, weight, ...extra, chartWIKI: ckb }) as number; + score = (r as ChartRuleModule).validator({ + ...info, + weight, + ...extra, + chartWIKI: ckb, + advisorContext, + }) as number; lints.push({ type, id, score, docs }); } log.push({ phase: 'LINT', ruleId: id, score, base: score, weight: 1, ruleType: type }); diff --git a/packages/ava/src/advisor/pipeline/component.ts b/packages/ava/src/advisor/pipeline/component.ts index 5e45b44f..ff156d29 100644 --- a/packages/ava/src/advisor/pipeline/component.ts +++ b/packages/ava/src/advisor/pipeline/component.ts @@ -1,5 +1,4 @@ -import { AsyncParallelHook, SyncHook } from 'tapable'; -import { last } from 'lodash'; +import { isFunction, last } from 'lodash'; import type { AdvisorPluginType, AdvisorPipelineContext } from '../types'; @@ -12,11 +11,7 @@ export class BaseComponent { plugins: AdvisorPluginType[] = []; /** 是否存在异步插件 */ - private hasAsyncPlugin: boolean; - - pluginManager: AsyncParallelHook<[Input, PluginResultMap], Output>; - - syncPluginManager: SyncHook<[Input, PluginResultMap], Output>; + private hasAsyncPlugin: boolean = false; afterPluginsExecute?: (params: PluginResultMap, context?: AdvisorPipelineContext) => Output; @@ -32,8 +27,6 @@ export class BaseComponent { ) { this.name = name; this.afterPluginsExecute = options?.afterPluginsExecute ?? this.defaultAfterPluginsExecute; - this.pluginManager = new AsyncParallelHook(['data', 'results']); - this.syncPluginManager = new SyncHook(['data', 'results']); this.context = options?.context; this.hasAsyncPlugin = !!options?.plugins?.find((plugin) => this.isPluginAsync(plugin)); options?.plugins?.forEach((plugin) => { @@ -41,44 +34,29 @@ export class BaseComponent { }); } - private defaultAfterPluginsExecute(params: Record) { - return last(Object.values(params)); + private defaultAfterPluginsExecute(params: Record) { + if (this.plugins.length) { + const lastPlugin = last(this.plugins); + return params[lastPlugin.name]; + } + return null; } private isPluginAsync(plugin: AdvisorPluginType) { - // 检测插件是否为异步的,并设置hasAsyncPlugin标志位 - if (plugin.execute.constructor.name === 'AsyncFunction') { + // 检测插件是否为异步的 + if (plugin.type === 'async' || plugin.execute.constructor.name === 'AsyncFunction') { return true; } return false; } - // 处理 之前都是同步的插件,新追加注册一个异步的插件 的情况 -- 需要执行的地方就不能用 registerPlugin(plugin: AdvisorPluginType) { plugin.onLoad?.(this.context); - this.plugins.push(plugin); if (this.isPluginAsync(plugin)) { this.hasAsyncPlugin = true; } - if (this.hasAsyncPlugin) { - this.pluginManager.tapPromise(plugin.name, async (input, outputs = {}) => { - plugin.onBeforeExecute?.(input, this.context); - const output = await plugin.execute(input, this.context); - plugin.onAfterExecute?.(output, this.context); - /* eslint-disable no-param-reassign */ - outputs[plugin.name] = output; - }); - } else { - this.syncPluginManager.tap(plugin.name, (input, outputs = {}) => { - plugin.onBeforeExecute?.(input, this.context); - const output = plugin.execute(input, this.context); - plugin.onAfterExecute?.(output, this.context); - /* eslint-disable no-param-reassign */ - outputs[plugin.name] = output; - return output; - }); - } + this.plugins.push(plugin); } unloadPlugin(pluginName: string) { @@ -89,16 +67,37 @@ export class BaseComponent { } } - execute(params: Input): Output | Promise { + execute(params: Input): Output { if (this.hasAsyncPlugin) { - // console.warn('存在异步执行的插件,建议使用 executeAsync') - const pluginsOutput = {}; - return this.pluginManager.promise(params, pluginsOutput).then(async () => { - return this.afterPluginsExecute?.(pluginsOutput, this.context); - }); + // eslint-disable-next-line no-console + console.warn('存在异步执行的插件,请使用 executeAsync'); } const pluginsOutput = {}; - this.syncPluginManager.call(params, pluginsOutput); + this.plugins.forEach((plugin) => { + plugin.onBeforeExecute?.(params, this.context); + if (isFunction(plugin.condition) && !plugin.condition(params, this.context)) return; + const output = plugin.execute(params, this.context) as Output; + plugin.onAfterExecute?.(output, this.context); + pluginsOutput[plugin.name] = output; + }); return this.afterPluginsExecute?.(pluginsOutput, this.context); } + + async executeAsync(params: Input): Promise { + if (!this.hasAsyncPlugin) { + return Promise.resolve(this.execute(params)); + } + const pluginsOutput = {}; + return Promise.all( + this.plugins.map(async (plugin) => { + plugin.onBeforeExecute?.(params, this.context); + if (isFunction(plugin.condition) && !plugin.condition(params, this.context)) return; + const output = (await plugin.execute(params, this.context)) as Output; + plugin.onAfterExecute?.(output, this.context); + pluginsOutput[plugin.name] = output; + }) + ).then(async () => { + return this.afterPluginsExecute?.(pluginsOutput, this.context); + }); + } } diff --git a/packages/ava/src/advisor/pipeline/pipeline.ts b/packages/ava/src/advisor/pipeline/pipeline.ts index 7be30117..8a3f32fe 100644 --- a/packages/ava/src/advisor/pipeline/pipeline.ts +++ b/packages/ava/src/advisor/pipeline/pipeline.ts @@ -2,7 +2,7 @@ import { AsyncSeriesWaterfallHook } from 'tapable'; import { BaseComponent } from './component'; -export class Pipeline { +export class Pipeline { components: BaseComponent[]; componentsManager: AsyncSeriesWaterfallHook; @@ -15,7 +15,7 @@ export class Pipeline { if (!component) return; this.componentsManager.tapPromise(component.name, async (previousResult) => { const input = previousResult; - const componentOutput = await component.execute(input || {}); + const componentOutput = await component.executeAsync(input || {}); return { ...input, @@ -25,7 +25,7 @@ export class Pipeline { }); } - async execute(initialParams: any) { + async execute(initialParams: Input): Promise { const result = await this.componentsManager.promise(initialParams); return result; } diff --git a/packages/ava/src/advisor/ruler/types.ts b/packages/ava/src/advisor/ruler/types.ts index 85a746c3..2e3c92e2 100644 --- a/packages/ava/src/advisor/ruler/types.ts +++ b/packages/ava/src/advisor/ruler/types.ts @@ -1,6 +1,7 @@ import type { FieldInfo } from '../../data'; import type { LevelOfMeasurement, ChartKnowledgeBase } from '../../ckb'; import type { Specification } from '../../common/types'; +import type { AdvisorPipelineContext } from '../types'; /** * Type of different rules. @@ -51,6 +52,7 @@ export interface Info { purpose?: string; preferences?: Preferences; customWeight?: number; + advisorContext?: Pick; [key: string]: any; } @@ -62,7 +64,8 @@ export type Trigger = (args: Info) => boolean; export type Optimizer = ( dataProps: BasicDataPropertyForAdvice[] | BasicDataPropertyForAdvice, - chartSpec: Specification + chartSpec: Specification, + advisorContext?: Pick ) => object; /** diff --git a/packages/ava/src/advisor/types/component.ts b/packages/ava/src/advisor/types/component.ts index 7d7ef79a..6c046522 100644 --- a/packages/ava/src/advisor/types/component.ts +++ b/packages/ava/src/advisor/types/component.ts @@ -12,7 +12,10 @@ export interface AdvisorPluginType { name: string; /** 插件运行的阶段,用于指定插件在 pipeline 的哪个环节运行 * */ stage?: PipelineStageType | PipelineStageType[]; + type?: 'async' | 'sync'; execute: (data: Input, context: AdvisorPipelineContext) => Output | Promise; + /** 判断插件运行的条件 */ + condition?: (data?: Input, context?: AdvisorPipelineContext) => boolean | Promise; // hooks onBeforeExecute?: (input: Input, context: AdvisorPipelineContext) => void | Promise; onAfterExecute?: (output: Output, context: AdvisorPipelineContext) => void | Promise; @@ -42,6 +45,7 @@ export type SpecGeneratorInput = { data: Data; // 单独调用 SpecGenerator 时,还要额外计算 dataProps 么 dataProps: BasicDataPropertyForAdvice[]; + encode?: MarkEncode; }; export type SpecGeneratorOutput = { advices: (Omit & { @@ -52,6 +56,10 @@ export type SpecGeneratorOutput = { export type VisualEncoderInput = { chartType: ChartId; dataProps: BasicDataPropertyForAdvice[]; + chartTypeRecommendations: ScoringResultForChartType[]; }; -export type VisualEncoderOutput = MarkEncode; +export type VisualEncoderOutput = { + encode?: MarkEncode; + chartTypeRecommendations?: ScoringResultForChartType[]; +};