Phase 6H - Add plot line story health indicators
This commit is contained in:
parent
cb67c4bcd3
commit
3c28f95ab8
@ -891,20 +891,42 @@ body.timeline-inspector-resizing {
|
|||||||
stroke: #ffffff;
|
stroke: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plotline-state-questionable-marker path {
|
.plotline-health-marker path,
|
||||||
|
.plotline-health-marker circle {
|
||||||
fill: #f4c430;
|
fill: #f4c430;
|
||||||
stroke: #8a6712;
|
stroke: #8a6712;
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plotline-state-questionable-marker line,
|
.plotline-health-marker line,
|
||||||
.plotline-state-questionable-marker circle {
|
.plotline-health-marker text {
|
||||||
stroke: #5e4710;
|
stroke: #5e4710;
|
||||||
fill: #5e4710;
|
fill: #5e4710;
|
||||||
stroke-width: 1.6;
|
stroke-width: 1.6;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plotline-health-marker text {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plotline-health-marker-dangling path,
|
||||||
|
.plotline-health-marker-dangling circle {
|
||||||
|
fill: #f8df8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plotline-health-marker-abandoned path,
|
||||||
|
.plotline-health-marker-abandoned circle {
|
||||||
|
fill: #d85c63;
|
||||||
|
stroke: #8b1e2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plotline-health-marker-abandoned line {
|
||||||
|
stroke: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.plot-event-svg-node circle,
|
.plot-event-svg-node circle,
|
||||||
.plot-event-svg-node path {
|
.plot-event-svg-node path {
|
||||||
stroke: #ffffff;
|
stroke: #ffffff;
|
||||||
|
|||||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1430,25 +1430,38 @@
|
|||||||
}));
|
}));
|
||||||
const rowDataById = new Map(rowData.map((item) => [item.plotLineId, item]));
|
const rowDataById = new Map(rowData.map((item) => [item.plotLineId, item]));
|
||||||
|
|
||||||
const stateFor = (points) => {
|
const healthFor = (points) => {
|
||||||
const hasResolve = points.some((point) => point.type === "resolve");
|
const hasResolve = points.some((point) => point.type === "resolve");
|
||||||
const hasAbandon = points.some((point) => point.type === "abandon");
|
const hasAbandon = points.some((point) => point.type === "abandon");
|
||||||
const hasStructuralTarget = points.some((point) =>
|
const resolvePoint = points.find((point) => point.type === "resolve");
|
||||||
(point.type === "branch" && point.targetPlotLineId) ||
|
const abandonPoint = points.find((point) => point.type === "abandon");
|
||||||
(point.type === "merge" && point.targetPlotLineId) ||
|
const hasMerge = points.some((point) => point.type === "merge" && point.targetPlotLineId);
|
||||||
(point.type === "split" && point.targetPlotLineIds.length >= 2));
|
const hasSplit = points.some((point) => point.type === "split" && point.targetPlotLineIds.length >= 2);
|
||||||
|
const hasBranchFromResolution = Boolean(resolvePoint) && points.some((point) =>
|
||||||
|
point.type === "branch" && point.targetPlotLineId && point.sceneIndex >= resolvePoint.sceneIndex);
|
||||||
const lastSceneIndex = points.reduce((max, point) => Math.max(max, point.sceneIndex), 0);
|
const lastSceneIndex = points.reduce((max, point) => Math.max(max, point.sceneIndex), 0);
|
||||||
const isNeglected = !hasResolve && !hasAbandon && latestSceneIndex - lastSceneIndex >= 10;
|
const isNeglected = !hasResolve && !hasAbandon && latestSceneIndex - lastSceneIndex >= 10;
|
||||||
|
const isDangling = !hasResolve && !hasAbandon && !hasMerge && !hasSplit && latestSceneIndex >= lastSceneIndex;
|
||||||
|
const isQuestionable = hasResolve && !hasMerge && !hasSplit && !hasBranchFromResolution;
|
||||||
|
let state = "active";
|
||||||
|
|
||||||
if (hasAbandon) {
|
if (hasAbandon) {
|
||||||
return "abandoned";
|
state = "abandoned";
|
||||||
|
} else if (hasResolve) {
|
||||||
|
state = isQuestionable ? "questionable" : "resolved";
|
||||||
|
} else if (isNeglected) {
|
||||||
|
state = "neglected";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasResolve) {
|
return {
|
||||||
return hasStructuralTarget ? "resolved" : "questionable";
|
state,
|
||||||
}
|
isNeglected,
|
||||||
|
isDangling,
|
||||||
return isNeglected ? "neglected" : "active";
|
isQuestionable,
|
||||||
|
isAbandoned: hasAbandon,
|
||||||
|
resolvePoint,
|
||||||
|
abandonPoint
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawNode = (point, colour, state) => {
|
const drawNode = (point, colour, state) => {
|
||||||
@ -1481,16 +1494,26 @@
|
|||||||
svg.append(group);
|
svg.append(group);
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawQuestionableMarker = (point) => {
|
const drawHealthMarker = (point, kind, titleText, offsetIndex = 0) => {
|
||||||
const group = makeSvg("g", { class: "plotline-state-questionable-marker" });
|
const offsetX = 13 + offsetIndex * 15;
|
||||||
|
const group = makeSvg("g", { class: `plotline-health-marker plotline-health-marker-${kind}` });
|
||||||
const title = makeSvg("title");
|
const title = makeSvg("title");
|
||||||
title.textContent = "Resolved without merge, split, or branch. Verify this thread has sufficient payoff.";
|
title.textContent = titleText;
|
||||||
group.append(title);
|
group.append(title);
|
||||||
|
|
||||||
|
if (kind === "questionable") {
|
||||||
|
group.append(makeSvg("circle", { cx: point.x + offsetX, cy: point.y - 10, r: 7 }));
|
||||||
|
const text = makeSvg("text", { x: point.x + offsetX, y: point.y - 6, "text-anchor": "middle" });
|
||||||
|
text.textContent = "?";
|
||||||
|
group.append(text);
|
||||||
|
} else {
|
||||||
group.append(makeSvg("path", {
|
group.append(makeSvg("path", {
|
||||||
d: `M ${point.x + 12} ${point.y - 13} L ${point.x + 22} ${point.y + 5} L ${point.x + 2} ${point.y + 5} Z`
|
d: `M ${point.x + offsetX} ${point.y - 18} L ${point.x + offsetX + 8} ${point.y - 3} L ${point.x + offsetX - 8} ${point.y - 3} Z`
|
||||||
}));
|
}));
|
||||||
group.append(makeSvg("line", { x1: point.x + 12, y1: point.y - 7, x2: point.x + 12, y2: point.y - 1 }));
|
group.append(makeSvg("line", { x1: point.x + offsetX, y1: point.y - 13, x2: point.x + offsetX, y2: point.y - 8 }));
|
||||||
group.append(makeSvg("circle", { cx: point.x + 12, cy: point.y + 2, r: 1.4 }));
|
group.append(makeSvg("circle", { cx: point.x + offsetX, cy: point.y - 5.6, r: 1.3 }));
|
||||||
|
}
|
||||||
|
|
||||||
svg.append(group);
|
svg.append(group);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1528,10 +1551,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
rowData.forEach((item) => {
|
rowData.forEach((item) => {
|
||||||
item.state = stateFor(item.points);
|
item.health = healthFor(item.points);
|
||||||
|
item.state = item.health.state;
|
||||||
});
|
});
|
||||||
|
|
||||||
rowData.forEach(({ colour, points, incomingStartXs, state }) => {
|
rowData.forEach((item) => {
|
||||||
|
const { colour, points, incomingStartXs, state } = item;
|
||||||
if (points.length > 0) {
|
if (points.length > 0) {
|
||||||
const explicitStart = points.find((point) => point.type === "start");
|
const explicitStart = points.find((point) => point.type === "start");
|
||||||
const incomingStartX = incomingStartXs.length ? Math.min(...incomingStartXs) : null;
|
const incomingStartX = incomingStartXs.length ? Math.min(...incomingStartXs) : null;
|
||||||
@ -1542,6 +1567,7 @@
|
|||||||
? incomingStartX
|
? incomingStartX
|
||||||
: first.x;
|
: first.x;
|
||||||
const endX = terminatingPoint ? terminatingPoint.x : last.x;
|
const endX = terminatingPoint ? terminatingPoint.x : last.x;
|
||||||
|
item.lineEndPoint = { x: endX, y: first.y };
|
||||||
|
|
||||||
if (endX > startX) {
|
if (endX > startX) {
|
||||||
svg.append(makeSvg("path", {
|
svg.append(makeSvg("path", {
|
||||||
@ -1572,11 +1598,28 @@
|
|||||||
rowData.forEach(({ points, colour, state }) => {
|
rowData.forEach(({ points, colour, state }) => {
|
||||||
points.forEach((point) => {
|
points.forEach((point) => {
|
||||||
drawNode(point, colour, state);
|
drawNode(point, colour, state);
|
||||||
if (state === "questionable" && point.type === "resolve") {
|
|
||||||
drawQuestionableMarker(point);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
rowData.forEach((item) => {
|
||||||
|
const markerPoint = item.lineEndPoint || item.points[item.points.length - 1];
|
||||||
|
let offsetIndex = 0;
|
||||||
|
if (item.health?.isNeglected && markerPoint) {
|
||||||
|
drawHealthMarker(markerPoint, "neglected", "No plot event for 10+ scenes. This thread may have been forgotten.", offsetIndex++);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.health?.isDangling && markerPoint) {
|
||||||
|
drawHealthMarker(markerPoint, "dangling", "This plot line is still unresolved at the end of the current timeline.", offsetIndex++);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.health?.isQuestionable && item.health.resolvePoint) {
|
||||||
|
drawHealthMarker(item.health.resolvePoint, "questionable", "Resolved without merge, split, or branch. Verify this thread has sufficient payoff.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.health?.isAbandoned && item.health.abandonPoint) {
|
||||||
|
drawHealthMarker(item.health.abandonPoint, "abandoned", "This plot line was abandoned. Check this is intentional.");
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2
PlotLine/wwwroot/js/site.min.js
vendored
2
PlotLine/wwwroot/js/site.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user