Skip to content

Commit

Permalink
Merge pull request #31 from concord-consortium/split-drawing-component
Browse files Browse the repository at this point in the history
Split drawing component
  • Loading branch information
dougmartin authored Apr 2, 2024
2 parents fa17fba + f99e191 commit 8de4c19
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 112 deletions.
116 changes: 4 additions & 112 deletions src/components/drawing.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, { useCallback, useEffect, 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 { DragIcon } from "./drawing/drag-icon";
import { NodeModal } from "./drawing/node-modal";

import SelectIcon from "../assets/select-icon.svg";
import AddNodeIcon from "../assets/add-node-icon.svg";
Expand All @@ -13,116 +15,6 @@ import DeleteIcon from "../assets/delete-icon.svg";

import "./drawing.scss";

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

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, setLabel]);

const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (node) {
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, exactPercentages, graph.edges, sum, onChange]);

const handleChangeLabel = (e: React.ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
};

if (!node) {
return null;
}

return (
<>
<div className="nodeModalBackground" />
<div className="nodeModal">
<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>
</div>
</form>
</div>
</div>
</>
);
};

export const DragIcon = ({drawingMode}: {drawingMode: DrawingMode}) => {
const [style, setStyle] = useState<React.CSSProperties>({});

useEffect(() => {
const mouseHandler = (e: MouseEvent) => setStyle({left: e.clientX - 20, top: e.clientY - 20});
window.addEventListener("mousemove", mouseHandler);
return () => window.removeEventListener("mousemove", mouseHandler);
}, []);

if (drawingMode === "addEdge") {
return <div className="dragIcon" style={style}><AddEdgeIcon /></div>;
}
if (drawingMode === "addNode") {
return <div className="dragIcon" style={style}><AddNodeIcon /></div>;
}
if (drawingMode === "delete") {
return <div className="dragIcon" style={style}><DeleteIcon /></div>;
}
return null;
};

interface Props {
highlightNode?: Node,
Expand Down
26 changes: 26 additions & 0 deletions src/components/drawing/drag-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useEffect, useState } from "react";
import { DrawingMode } from "../graph";
import AddNodeIcon from "../../assets/add-node-icon.svg";
import AddEdgeIcon from "../../assets/add-edge-icon.svg";
import DeleteIcon from "../../assets/delete-icon.svg";

export const DragIcon = ({ drawingMode }: { drawingMode: DrawingMode; }) => {
const [style, setStyle] = useState<React.CSSProperties>({});

useEffect(() => {
const mouseHandler = (e: MouseEvent) => setStyle({ left: e.clientX - 20, top: e.clientY - 20 });
window.addEventListener("mousemove", mouseHandler);
return () => window.removeEventListener("mousemove", mouseHandler);
}, []);

if (drawingMode === "addEdge") {
return <div className="dragIcon" style={style}><AddEdgeIcon /></div>;
}
if (drawingMode === "addNode") {
return <div className="dragIcon" style={style}><AddNodeIcon /></div>;
}
if (drawingMode === "delete") {
return <div className="dragIcon" style={style}><DeleteIcon /></div>;
}
return null;
};
92 changes: 92 additions & 0 deletions src/components/drawing/node-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { ProbabilitySelector, percentage, reduceToSum } from "./probability-selector";
import { Node, Edge, GraphData } from "../../type";

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

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, setLabel]);

const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (node) {
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, exactPercentages, graph.edges, sum, onChange]);

const handleChangeLabel = (e: React.ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
};

if (!node) {
return null;
}

return (
<>
<div className="nodeModalBackground" />
<div className="nodeModal">
<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>
</div>
</form>
</div>
</div>
</>
);
};
File renamed without changes.
File renamed without changes.

0 comments on commit 8de4c19

Please sign in to comment.