Skip to content

Commit

Permalink
feat: Added probability selector component [PT-185388713]
Browse files Browse the repository at this point in the history
  • Loading branch information
dougmartin committed Jul 18, 2023
1 parent 7f0ac19 commit b40c862
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 30 deletions.
34 changes: 20 additions & 14 deletions src/components/drawing.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
left: 0;
background: #000;
opacity: 0.25;
z-index: 100;
}
.nodeModal {
position: absolute;
Expand All @@ -52,24 +53,29 @@
display: flex;
justify-content: center;
align-items: center;
z-index: 101;

.nodeModalTitle {
background-color: #177991;
color: white;
padding: 5px 10px;
}
.nodeModalContent {
min-width: 200px;

form {
padding: 10px;
background-color: white;
display: flex;
flex-direction: column;
gap: 10px;
.nodeModalTitle {
background-color: #177991;
color: white;
padding: 5px 10px;
}

.nodeModalButtons {
form {
padding: 10px;
background-color: white;
display: flex;
gap: 5px;
justify-content: flex-end;
flex-direction: column;
gap: 10px;

.nodeModalButtons {
display: flex;
gap: 5px;
justify-content: flex-end;
}
}
}
}
Expand Down
66 changes: 54 additions & 12 deletions src/components/drawing.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { clsx } from "clsx";
import { nanoid } from "nanoid";

import { DrawingMode, Graph, Point, RubberBand } from "./graph";
import { Edge, GraphData, Node } from "../type";
import { ProbabilitySelector, percentage, reduceToSum } from "./probability-selector";

import SelectIcon from "../assets/select-icon.svg";
import AddNodeIcon from "../assets/add-node-icon.svg";
Expand All @@ -14,23 +15,61 @@ import "./drawing.scss";

interface NodeModalProps {
node?: Node,
onChange: (id: string, newNode: Node) => void,
graph: GraphData;
onChange: (id: string, newNode: Node, newEdge: Edge[]) => void,
onCancel: () => void
}

export const NodeModal = ({node, onChange, onCancel}: NodeModalProps) => {
export const NodeModal = ({node, graph, onChange, onCancel}: NodeModalProps) => {
const [label, setLabel] = useState(node?.label || "");
const [exactPercentages, _setExactPercentages] = useState<number[]>([]);

const setExactPercentages = useCallback((percentages: number[]) => {
// make sure percentages add up to exactly 100%
const allButLastSum = reduceToSum(percentages.slice(0, -1));
percentages[percentages.length - 1] = 100 - allButLastSum;
_setExactPercentages(percentages);
}, [_setExactPercentages]);

const fromEdges = useMemo(() => {
return node ? graph.edges.filter(e => e.from === node.id) : [];
}, [node, graph.edges]);

const edgeLabels = useMemo(() => {
const labels = graph.nodes.reduce<Record<string,string>>((acc, cur) => {
acc[cur.id] = cur.label;
return acc;
}, {});
return fromEdges.map(e => labels[e.to]);
}, [graph.nodes, fromEdges]);

const edgeValues = useMemo(() => fromEdges.map(e => e.value), [fromEdges]);
const sum = useMemo(() => reduceToSum(edgeValues), [edgeValues]);

useEffect(() => {
const percentages = edgeValues.map(edgeValue => percentage(edgeValue / sum));
setExactPercentages(percentages);
}, [edgeValues, sum, setExactPercentages]);

useEffect(() => {
setLabel(node?.label || "");
}, [node]);
}, [node, setLabel]);

const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (node) {
onChange(node.id, {...node, label: label.trim()});
let valueIndex = 0;
const newEdges = graph.edges.map(edge => {
if (edge.from === node.id) {
const value = sum * exactPercentages[valueIndex++];
return {...edge, value};
} else {
return edge;
}
});
onChange(node.id, {...node, label: label.trim()}, newEdges);
}
}, [label, node, onChange]);
}, [label, node, exactPercentages, graph.edges, sum, onChange]);

const handleChangeLabel = (e: React.ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
Expand All @@ -44,10 +83,15 @@ export const NodeModal = ({node, onChange, onCancel}: NodeModalProps) => {
<>
<div className="nodeModalBackground" />
<div className="nodeModal">
<div>
<div className="nodeModalContent">
<div className="nodeModalTitle">Update State</div>
<form onSubmit={handleSubmit}>
<input type="text" value={label} onChange={handleChangeLabel} autoFocus={true} />
<ProbabilitySelector
exactPercentages={exactPercentages}
edgeLabels={edgeLabels}
onChange={setExactPercentages}
/>
<div className="nodeModalButtons">
<button type="submit">Save</button>
<button onClick={onCancel}>Cancel</button>
Expand Down Expand Up @@ -291,16 +335,13 @@ export const Drawing = (props: Props) => {

const handleClearSelectedNode = useCallback(() => setSelectedNode(undefined), [setSelectedNode]);

const handleChangeNode = useCallback((id: string, newNode: Node) => {
const handleChangeNode = useCallback((id: string, newNode: Node, newEdges: Edge[]) => {
setGraph(prev => {
const nodeIndex = prev.nodes.findIndex(n => n.id === id);
if (nodeIndex !== -1) {
const nodes = [...prev.nodes];
nodes.splice(nodeIndex, 1, newNode);
return {
nodes,
edges: prev.edges
};
return { nodes, edges: newEdges };
} else {
return prev;
}
Expand Down Expand Up @@ -362,6 +403,7 @@ export const Drawing = (props: Props) => {
<DragIcon drawingMode={drawingMode} />
<NodeModal
node={selectedNode}
graph={graph}
onChange={handleChangeNode}
onCancel={handleClearSelectedNode}
/>
Expand Down
12 changes: 8 additions & 4 deletions src/components/graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import "./graph.scss";

export type DrawingMode = "select"|"addNode"|"addEdge"|"delete";


export type GraphSettings = {
minRadius: number;
maxRadius: number;
Expand Down Expand Up @@ -63,6 +62,7 @@ type D3Edge = {
targetX: number,
targetY: number,
weight: number;
value: number;
};

type D3Graph = {
Expand Down Expand Up @@ -213,6 +213,8 @@ const calculateNodeFontSize = (d: D3Node) => {
return {label, fontSize};
};

const lineDashArray = (edge: D3Edge) => edge.value ? "" : "4";

export const Graph = (props: Props) => {
const {graph, highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes,
highlightColor, allowDragging, autoArrange, mode, rubberBand, drawingMode,
Expand Down Expand Up @@ -268,7 +270,7 @@ export const Graph = (props: Props) => {
d3NodeMap[edge.from].loopWeight = edgeWeight(edge.value);
} else {
newD3Graph.edges.push({
//weight: edge.value,
value: edge.value,
weight: edgeWeight(edge.value),
source: d3NodeMap[edge.from],
target: d3NodeMap[edge.to],
Expand Down Expand Up @@ -492,6 +494,7 @@ export const Graph = (props: Props) => {
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", d => d.weight)
.attr("stroke-dasharray", d => lineDashArray(d))
.attr("x1", d => d.sourceX)
.attr("x2", d => d.targetX)
.attr("y1", d => d.sourceY)
Expand Down Expand Up @@ -628,11 +631,12 @@ export const Graph = (props: Props) => {
svg
.selectAll("line")
.attr("stroke", "#999")
.attr("stroke-dasharray", "")
.attr("stroke-dasharray", (d: any) => lineDashArray(d))
.attr("marker-end", "url(#arrow)")
.filter((d: any) => ((
(d.value > 0) && (
(highlightNode?.id === d.source?.id && highlightAllNextNodes) ||
(highlightEdge?.from === d.source?.id && highlightEdge?.to === d.target?.id))))
(highlightEdge?.from === d.source?.id && highlightEdge?.to === d.target?.id)))))
.attr("stroke", highlightColor)
.attr("stroke-dasharray", highlightAllNextNodes ? "4" : "")
.attr("marker-end", arrowUrl);
Expand Down
23 changes: 23 additions & 0 deletions src/components/probability-selector.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.probability-selector {
.header {
font-weight: bold;
}

.svg-container {
margin: 10px 0;

svg {
width: 100%;
height: 100%;

circle {
fill: #777;
cursor: pointer;
}
}
}

.percentages {
user-select: none;
}
}
Loading

0 comments on commit b40c862

Please sign in to comment.