创建一个遵循决策树的 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的时候。
如果一个节点是叶子,那么 yesId
和 noId
都应该是 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题。否则,我们可以进一步讨论。
我想构建一个 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的时候。 如果一个节点是叶子,那么yesId
和noId
都应该是 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题。否则,我们可以进一步讨论。