Skip to content

Commit

Permalink
Split probability selector into its own component
Browse files Browse the repository at this point in the history
  • Loading branch information
dougmartin committed Jul 17, 2023
1 parent 6202bda commit d779168
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 184 deletions.
185 changes: 1 addition & 184 deletions src/components/drawing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { nanoid } from "nanoid";

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

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

import "./drawing.scss";

const svgWidth = 200;
const svgHeight = 18;
const thumbRadius = 7;
const thumbDiameter = thumbRadius * 2;
const lineWidth = 2;
const lineStart = thumbRadius;
const lineEnd = svgWidth - thumbRadius;
const lineLength = lineEnd - lineStart;
const lineY = (svgHeight - lineWidth) / 2;

const distinctColors = [
"#e6194B", "#3cb44b", "#4363d8", "#f58231", "#42d4f4", "#ffe119",
"#f032e6", "#fabed4", "#469990", "#dcbeff", "#9A6324", "#fffac8",
"#800000", "#aaffc3", "#000075", "#a9a9a9", "#ffffff", "#000000"
];
const getDistinctColor = (i: number) => distinctColors[i % distinctColors.length];

interface ProbabilitySelectorOffset {
min: number
cur: number
max: number
}
interface ProbabilitySelectorThumbProps {
index: number
offset: ProbabilitySelectorOffset
visibleLineLength: number
onChange: (index: number, probability: number) => void;
}

export const ProbabilitySelectorThumb = (props: ProbabilitySelectorThumbProps) => {
const {index, offset, onChange} = props;

const handleMouseDown = useCallback((e: React.MouseEvent<SVGCircleElement>) => {
const startX = e.clientX;

const handleMouseMove = (e2: MouseEvent) => {
const delta = e2.clientX - startX;
const newOffset = Math.min(Math.max(offset.min, offset.cur + delta), offset.max);
const newProbability = (newOffset - offset.min) / (offset.max - offset.min);
onChange(index, newProbability);
};

const handleMouseUp = () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};

window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
}, [index, offset, onChange]);

return (
<circle
cx={offset.cur}
cy={lineY}
r={thumbRadius}
fill="#777"
style={{cursor: "pointer"}}
onMouseDown={handleMouseDown}
/>
);
};


interface ProbabilitySelectorProps {
edgeValues: number[]
edgeLabels: string[]
onChange: React.Dispatch<React.SetStateAction<number[]>>
}

const percentage = (n: number) => n * 100;
const probability = (n: number) => n / 100;
const reduceToSum = (a: number[]) => a.reduce((acc, cur) => acc + cur, 0);

export const ProbabilitySelector = ({edgeValues, edgeLabels, onChange}: ProbabilitySelectorProps) => {

const sum = useMemo(() => reduceToSum(edgeValues), [edgeValues]);

const exactPercentages = useMemo(() => {
const result = edgeValues.map(edgeValue => percentage(edgeValue / sum));

// make sure percentages add up to exactly 100%
const allButLastSum = reduceToSum(result.slice(0, -1));
result[result.length - 1] = 100 - allButLastSum;

return result;
}, [edgeValues, sum]);

const roundPercentages = useMemo(() => {
const result = exactPercentages.slice(0, -1).map(exactPercentage => Math.round(exactPercentage));
result.push(100 - reduceToSum(result));
return result;
}, [exactPercentages]);

const visibleLineLength = lineLength - ((exactPercentages.length - 1) * thumbDiameter);

const offsets = useMemo(() => {
let lastOffset = lineStart;
return exactPercentages.slice(0, -1).map((exactPercentage, i) => {
const offset: ProbabilitySelectorOffset = {
min: lineStart + (i * thumbRadius),
cur: lastOffset + (probability(exactPercentage) * lineLength),
max: lineEnd - (i * thumbRadius),
};
lastOffset = offset.cur;
return offset;
});
}, [exactPercentages]);

const segments = useMemo(() => {
if (offsets.length > 0) {
let start = offsets[0].min;
const result = offsets.map(offset => {
const segment = {
start,
end: offset.cur
};
start = offset.cur;
return segment;
});
result.push({
start: result[result.length - 1].end,
end: lineEnd
});
return result;
}
return [];
}, [offsets]);

const handleProbabilitySelectorThumbChange = useCallback((index: number, newProbability: number) => {
const newExactPercentages = [...exactPercentages];
const prevExactPercentage = exactPercentages[index - 1] || 0;
const exactPercentage = Math.min(100, Math.max(0, percentage(newProbability) - prevExactPercentage));
const delta = newExactPercentages[index] - exactPercentage;

newExactPercentages[index] = exactPercentage;
newExactPercentages[index+1] = Math.max(0, newExactPercentages[index+1] + delta);

const newEdgeValues = newExactPercentages.map(p => sum * probability(p));
onChange(newEdgeValues);
}, [exactPercentages, sum, onChange]);

if (edgeValues.length < 2) {
return null;
}

return (
<div>
<div style={{fontWeight: "bold"}}>Transition Probabilities</div>
<div style={{margin: "10px 0", width: svgWidth, height: svgHeight}}>
<svg height={"100%"} width={"100%"} viewBox={`0 0 ${svgWidth} ${svgHeight}`}>
{segments.map(({start, end}, i) => (
<line
key={i}
x1={start}
y1={lineY}
x2={end}
y2={lineY}
strokeWidth={lineWidth}
stroke={getDistinctColor(i)}
/>
))}
{offsets.map((offset, index) => (
<ProbabilitySelectorThumb
key={index}
index={index}
offset={offset}
visibleLineLength={visibleLineLength}
onChange={handleProbabilitySelectorThumbChange}
/>
))}
</svg>
</div>
<div style={{userSelect: "none"}}>
{roundPercentages.map((p, i) => (
<div key={i} style={{color: getDistinctColor(i)}}>To {edgeLabels[i]}: {p}%</div>
))}
</div>
</div>
);
};

interface NodeModalProps {
node?: Node,
graph: GraphData;
Expand Down Expand Up @@ -245,8 +64,6 @@ export const NodeModal = ({node, graph, onChange, onCancel}: NodeModalProps) =>
return null;
}

// check to see if there are edges from this node

return (
<>
<div className="nodeModalBackground" />
Expand Down
18 changes: 18 additions & 0 deletions src/components/probability-selector.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.probability-selector {
.header {
font-weight: bold;
}

.svg-container {
margin: 10px 0;

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

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

0 comments on commit d779168

Please sign in to comment.