创建一个遵循决策树的 angular 表单

Create an angular form that would follow a decision tree

我想构建一个 Angular 应用程序,它可以向用户提问并根据他们的回答(遵循决策树)生成下一个问题。

例如(我将展示多个场景,每个场景都在一行中,问题用斜体,答案用粗体):

                  CHEST PAIN ? 
                yes/          \no
                              RATE?
                         <60 /    \ >100
                                 HYPO ?
                              no/    \ yes
                                     **SEDATE**(result of the decision tree)

能否请您提供一些有关如何实现该目标的线索?这看起来像一棵树(或一张图)。我应该将我的问题存储在这样的数据结构中吗?

this 之类的东西对你有用吗?

public readonly countries = ["France", "Germany"];
public readonly cities = {
    "France": ["Paris", "Lyon", "Marseille"],
    "Germany": ["Berlin", "Frankfurt", "Hamburg"]
};

public selection = {
    country: '',
    city: ''
};
Pick country
<select [(ngModel)]="selection.country">
  <option *ngFor="let c of countries" [ngValue]="c">{{c}}</option>
</select>

<br/>
<br/>

<ng-container *ngIf="selection.country.length > 0">
    Pick city
    <select [(ngModel)]="selection.city">
      <option *ngFor="let c of cities[selection.country]" [ngValue]="c">{{c}}</option>
    </select>
</ng-container>

从最初的顶级选择开始,然后根据上一个答案显示下一个问题。

更新 1: 我已根据您发布的图表修改了答案。新的解决方案不使用硬编码路径,而是定义一个 "decision tree" 结构并使用它。稍后我会详细解释,其实质是你只需要修改一个变量的内容,应用程序仍然可以工作。 New stackblitz.

想法是将树中的每个节点存储为一个对象,如下所示:

export class TreeNode {
  public readonly id: nodeId;
  public readonly description: string;
  public decision: boolean | null;
  public readonly yesId: nodeId | null;
  public readonly noId: nodeId | null;
  constructor(
    id: nodeId,
    description: string,
    yesId: nodeId | null,
    noId: nodeId | null
  ) {
    this.id = id;
    this.description = description;
    this.decision = null; // <-- must be null on creation. wait for decision from user.
    this.yesId = yesId;
    this.noId = noId;
  }
}
  • id是一个字符串,在所有节点中应该是唯一的。稍后会详细介绍。
  • description 是 question/answer 正文。
  • decision 是 Yes/No 问题的结果。
  • yesId 是我们应该导航到的节点的 id,当决定是时。
  • noId是我们应该导航到的节点的id,当decision是no的时候。 如果一个节点是叶子,那么 yesIdnoId 都应该是 null。

我将您绘图中所有可能的节点保存为一个名为 nodeList 的 "dictionary",因此我们可以使用 nodeList[id] 语法访问节点。现在看起来像这样:

/**
 * Possible values for a node id. This is optional, but highly encouraged.
 */
export type nodeId =
  | "chestPain"
  | "twaveInversion"
  | "rateOver100"
  | "hypotensive"
  | "nstemi"
  | "unstableAngina"
  | "sedate"
  | "qrsOver012"
  | "vtGetExpertHelp"
  | "qrsRegular"
  | "svtVagal"
  | "pwavesPresent"
  | "atrialFibrilation"
  | "aflutter"
  | "rate100temp";

/**
 * Dictionary of nodes using their id as a key.
 */
export const nodeList = {
  chestPain: new TreeNode(
    "chestPain",
    "Chest Pain?",
    "twaveInversion",
    "rateOver100"
  ),
  twaveInversion: new TreeNode(
    "twaveInversion",
    "Twave Inversion?",
    "nstemi",
    "unstableAngina"
  ),
  unstableAngina: new TreeNode("unstableAngina", "Unstable Angina", null, null),
  nstemi: new TreeNode("nstemi", "NSTEMI", null, null),
  rateOver100: new TreeNode(
    "rateOver100",
    "Rate > 100?",
    "hypotensive",
    "rate100temp"
  ),
  hypotensive: new TreeNode(
    "hypotensive",
    "Hypotensive?",
    "sedate",
    "qrsOver012"
  ),
  sedate: new TreeNode("sedate", "Sedate.", null, null),
  qrsOver012: new TreeNode(
    "qrsOver012",
    "QRS > 0.12s ?",
    "vtGetExpertHelp",
    "qrsRegular"
  ),
  vtGetExpertHelp: new TreeNode(
    "vtGetExpertHelp",
    "VT Get expert help.",
    null,
    null
  ),
  qrsRegular: new TreeNode(
    "qrsRegular",
    "QRS regular?",
    "svtVagal",
    "pwavesPresent"
  ),
  svtVagal: new TreeNode("svtVagal", "SVT Vagal", null, null),
  pwavesPresent: new TreeNode(
    "pwavesPresent",
    "Pwaves present?",
    "aflutter",
    "atrialFibrilation"
  ),
  aflutter: new TreeNode("aflutter", "Aflutter", null, null),
  atrialFibrilation: new TreeNode(
    "atrialFibrilation",
    "Afrial fibrilation",
    null,
    null
  ),
  rate100temp: new TreeNode(
    "rate100temp",
    "Rate < 100 answer (no node given)",
    null,
    null
  )
};

最后,DecisionTreeFormComponent:

export class DecisionTreeFormComponent implements OnInit {
  public decisionTree: IDecisionTree;
  public currentNode: TreeNode;

  public treeJSONhidden: boolean = true;
  public nodeJSONhidden: boolean = true;
  constructor() {}

  ngOnInit() {
    this.reset();    
  }

  public reset() {
    // Init base node and tree here.
    this.decisionTree = [];
    this.currentNode = Object.assign({}, nodeList.chestPain);
  }

  public yes() {
    this.currentNode.decision = true;
    this.pushNode();
    this.currentNode = Object.assign({}, nodeList[this.currentNode.yesId]);
    if( this.isFinal(this.currentNode)) {
      this.pushNode();
    }
  }

  public no() {
    this.currentNode.decision = false;
    this.pushNode();
    this.currentNode = Object.assign({}, nodeList[this.currentNode.noId]);
    if( this.isFinal(this.currentNode)) {
      this.pushNode();
    }
  }

  public isFinal = (node: TreeNode) => node.yesId == null && node.noId == null;

  private pushNode():void {
    this.decisionTree.push({
      node: this.currentNode,
      index: this.decisionTree.length
    });
  }
}

reset() 方法中,我只是将 currentNode 初始化为胸痛问题,其余的由用户的选择处理。我将路径保存为一个数组,以便在到达叶节点时显示最终结果。查看我在 stackblitz 中准备的示例 HTML,但在这种情况下演示文稿无关紧要。您可以调整它以满足您的需要。

我认为这是一个不错的解决方案,您只需更改 nodeList,表单就会根据它在那里找到的值工作。

局限性: 目前仅适用于 YES/NO 类型的问题。如果希望多选,可以尝试"refactor"题为多true/false题。否则,我们可以进一步讨论。