Changed metric graph to align with scenes.
This commit is contained in:
parent
b5a6de4fb2
commit
2ad5865c58
@ -3,27 +3,6 @@
|
|||||||
ViewData["Title"] = "Timeline";
|
ViewData["Title"] = "Timeline";
|
||||||
ViewData["ShellClass"] = "plotline-shell-wide";
|
ViewData["ShellClass"] = "plotline-shell-wide";
|
||||||
var selectedBook = Model.Books.FirstOrDefault(x => x.Book.BookID == Model.SelectedBookID)?.Book;
|
var selectedBook = Model.Books.FirstOrDefault(x => x.Book.BookID == Model.SelectedBookID)?.Book;
|
||||||
var timelineMetricScenes = Model.Books
|
|
||||||
.SelectMany(book => book.Chapters.SelectMany(chapter => chapter.Scenes.Select(scene => new
|
|
||||||
{
|
|
||||||
sceneId = scene.SceneID,
|
|
||||||
axisLabel = $"{chapter.Chapter.ChapterNumber:g}.{scene.SceneNumber:g}",
|
|
||||||
chapterLabel = $"Chapter {chapter.Chapter.ChapterNumber:g}",
|
|
||||||
sceneNumber = scene.SceneNumber.ToString("g"),
|
|
||||||
sceneTitle = scene.SceneTitle,
|
|
||||||
bookTitle = book.Book.BookTitle,
|
|
||||||
timelineUrl = Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = book.Book.BookID, selectedSceneId = scene.SceneID })
|
|
||||||
})))
|
|
||||||
.ToList();
|
|
||||||
var timelineMetricShapePayload = new
|
|
||||||
{
|
|
||||||
scenes = timelineMetricScenes,
|
|
||||||
metrics = Model.MetricGraphs.Select(graph => new
|
|
||||||
{
|
|
||||||
metricName = graph.MetricName,
|
|
||||||
points = graph.Points.Select(point => new { sceneId = point.SceneID, value = point.Value })
|
|
||||||
})
|
|
||||||
};
|
|
||||||
var hasActiveFilter = Model.MatchingSceneIDs.Count != Model.OrderedScenes.Count || !string.IsNullOrWhiteSpace(Model.FocusLabel);
|
var hasActiveFilter = Model.MatchingSceneIDs.Count != Model.OrderedScenes.Count || !string.IsNullOrWhiteSpace(Model.FocusLabel);
|
||||||
var warningScenes = Model.OrderedScenes.Where(x => x.WarningCount > 0).ToList();
|
var warningScenes = Model.OrderedScenes.Where(x => x.WarningCount > 0).ToList();
|
||||||
var timelineColumns = new List<(BookTimelineViewModel Book, ChapterTimelineViewModel? Chapter, Scene? Scene, bool IsPlaceholder, string PlaceholderLabel)>();
|
var timelineColumns = new List<(BookTimelineViewModel Book, ChapterTimelineViewModel? Chapter, Scene? Scene, bool IsPlaceholder, string PlaceholderLabel)>();
|
||||||
@ -49,6 +28,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var timelineMetricColumns = timelineColumns.Select(column => new
|
||||||
|
{
|
||||||
|
sceneId = column.Scene?.SceneID,
|
||||||
|
isPlaceholder = column.Scene is null,
|
||||||
|
axisLabel = column.Scene is null ? string.Empty : $"Ch {column.Chapter?.Chapter.ChapterNumber:g}.{column.Scene.SceneNumber:g}",
|
||||||
|
chapterLabel = column.Chapter is null ? column.Book.Book.BookTitle : $"Chapter {column.Chapter.Chapter.ChapterNumber:g}",
|
||||||
|
sceneNumber = column.Scene?.SceneNumber.ToString("g"),
|
||||||
|
sceneTitle = column.Scene?.SceneTitle ?? column.PlaceholderLabel,
|
||||||
|
bookTitle = column.Book.Book.BookTitle,
|
||||||
|
timelineUrl = column.Scene is null ? null : Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = column.Book.Book.BookID, selectedSceneId = column.Scene.SceneID })
|
||||||
|
}).ToList();
|
||||||
|
var timelineMetricShapePayload = new
|
||||||
|
{
|
||||||
|
scenes = timelineMetricColumns,
|
||||||
|
metrics = Model.MetricGraphs.Select(graph => new
|
||||||
|
{
|
||||||
|
metricName = graph.MetricName,
|
||||||
|
points = graph.Points.Select(point => new { sceneId = point.SceneID, value = point.Value })
|
||||||
|
})
|
||||||
|
};
|
||||||
int ChapterSceneColumns(ChapterTimelineViewModel chapter) => Math.Max(chapter.Scenes.Count, 1);
|
int ChapterSceneColumns(ChapterTimelineViewModel chapter) => Math.Max(chapter.Scenes.Count, 1);
|
||||||
int BookSceneColumns(BookTimelineViewModel book) => Math.Max(book.Chapters.Any() ? book.Chapters.Sum(ChapterSceneColumns) : 1, 1);
|
int BookSceneColumns(BookTimelineViewModel book) => Math.Max(book.Chapters.Any() ? book.Chapters.Sum(ChapterSceneColumns) : 1, 1);
|
||||||
var hasSelectedScene = Model.SelectedScene is not null;
|
var hasSelectedScene = Model.SelectedScene is not null;
|
||||||
@ -705,6 +704,79 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric-timeline-board" data-timeline-section="metrics" style="--scene-count:@timelineColumns.Count">
|
||||||
|
<div class="lane-board-heading metric-lane-board-heading">
|
||||||
|
<button type="button" data-collapse-toggle="metrics">Metric Shape</button>
|
||||||
|
<small>@Model.OrderedScenes.Count scene@(Model.OrderedScenes.Count == 1 ? "" : "s")</small>
|
||||||
|
</div>
|
||||||
|
<div data-collapse-body="metrics">
|
||||||
|
@if (!Model.MetricGraphs.Any() || !Model.OrderedScenes.Any())
|
||||||
|
{
|
||||||
|
<div class="future-lane-placeholder">
|
||||||
|
Add scenes to begin seeing emotional and pacing shape.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="metric-shape-chart metric-shape-timeline-chart" data-metric-shape-chart>
|
||||||
|
<div class="metric-shape-controls">
|
||||||
|
<p class="metric-shape-intro">Compare selected metrics across the same scene timeline. Click a point to focus that scene.</p>
|
||||||
|
<div class="metric-toggle-row" data-metric-toggles></div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-timeline-row">
|
||||||
|
<div class="metric-lane-label">
|
||||||
|
<strong>Metric Shape</strong>
|
||||||
|
<span>Scene pacing</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-chart-frame metric-timeline-frame">
|
||||||
|
<canvas aria-label="Comparative scene metric line graph"></canvas>
|
||||||
|
<p class="muted d-none" data-chart-fallback>Metric chart could not load. The raw metric rows are still available below.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="application/json">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(timelineMetricShapePayload))</script>
|
||||||
|
</div>
|
||||||
|
<details class="metric-raw-details">
|
||||||
|
<summary>Show raw metric rows</summary>
|
||||||
|
<div class="metric-graph-stack mt-2">
|
||||||
|
@foreach (var graph in Model.MetricGraphs)
|
||||||
|
{
|
||||||
|
<div class="metric-graph-row" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
|
||||||
|
<div class="metric-graph-label">@graph.MetricName</div>
|
||||||
|
<div class="metric-graph-points" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
|
||||||
|
@foreach (var column in timelineColumns)
|
||||||
|
{
|
||||||
|
if (column.Scene is null)
|
||||||
|
{
|
||||||
|
<span class="metric-point metric-point-placeholder timeline-placeholder-cell is-placeholder" title="@column.PlaceholderLabel"><span>No value</span></span>
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var point = graph.Points.FirstOrDefault(x => x.SceneID == column.Scene.SceneID);
|
||||||
|
if (point is null)
|
||||||
|
{
|
||||||
|
<span class="metric-point metric-point-placeholder timeline-placeholder-cell is-placeholder" title="No metric value"><span>No value</span></span>
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
<a asp-controller="Timeline"
|
||||||
|
asp-action="Index"
|
||||||
|
asp-route-projectId="@Model.Project.ProjectID"
|
||||||
|
asp-route-bookId="@Model.SelectedBookID"
|
||||||
|
asp-route-selectedSceneId="@point.SceneID"
|
||||||
|
class="metric-point"
|
||||||
|
style="--metric-height:@point.Percent%"
|
||||||
|
title="Scene @point.SceneNumber: @point.SceneTitle - @point.Value">
|
||||||
|
<span>@point.Value</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
|
@if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
|
||||||
{
|
{
|
||||||
<div class="plot-lane-board" data-timeline-section="plot-lines" style="--scene-count:@timelineColumns.Count">
|
<div class="plot-lane-board" data-timeline-section="plot-lines" style="--scene-count:@timelineColumns.Count">
|
||||||
@ -908,73 +980,6 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pacing-panel" data-timeline-section="metrics">
|
|
||||||
<div class="pacing-heading">
|
|
||||||
<div>
|
|
||||||
<p class="eyebrow">Pacing</p>
|
|
||||||
<h2><button type="button" data-collapse-toggle="metrics">Metric shape</button></h2>
|
|
||||||
</div>
|
|
||||||
<span class="muted">@Model.OrderedScenes.Count scene@(Model.OrderedScenes.Count == 1 ? "" : "s")</span>
|
|
||||||
</div>
|
|
||||||
<div data-collapse-body="metrics">
|
|
||||||
@if (!Model.MetricGraphs.Any() || !Model.OrderedScenes.Any())
|
|
||||||
{
|
|
||||||
<p class="muted">Add scenes to begin seeing emotional and pacing shape.</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="metric-shape-chart" data-metric-shape-chart>
|
|
||||||
<p class="metric-shape-intro">Compare selected metrics across the same scene timeline. Click a point to focus that scene.</p>
|
|
||||||
<div class="metric-toggle-row" data-metric-toggles></div>
|
|
||||||
<div class="metric-chart-frame">
|
|
||||||
<canvas aria-label="Comparative scene metric line graph"></canvas>
|
|
||||||
<p class="muted d-none" data-chart-fallback>Metric chart could not load. The raw metric rows are still available below.</p>
|
|
||||||
</div>
|
|
||||||
<script type="application/json">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(timelineMetricShapePayload))</script>
|
|
||||||
</div>
|
|
||||||
<details class="metric-raw-details">
|
|
||||||
<summary>Show raw metric rows</summary>
|
|
||||||
<div class="metric-graph-stack mt-2">
|
|
||||||
@foreach (var graph in Model.MetricGraphs)
|
|
||||||
{
|
|
||||||
<div class="metric-graph-row" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
|
|
||||||
<div class="metric-graph-label">@graph.MetricName</div>
|
|
||||||
<div class="metric-graph-points" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
|
|
||||||
@foreach (var column in timelineColumns)
|
|
||||||
{
|
|
||||||
if (column.Scene is null)
|
|
||||||
{
|
|
||||||
<span class="metric-point metric-point-placeholder timeline-placeholder-cell is-placeholder" title="@column.PlaceholderLabel"><span>No value</span></span>
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var point = graph.Points.FirstOrDefault(x => x.SceneID == column.Scene.SceneID);
|
|
||||||
if (point is null)
|
|
||||||
{
|
|
||||||
<span class="metric-point metric-point-placeholder timeline-placeholder-cell is-placeholder" title="No metric value"><span>No value</span></span>
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
<a asp-controller="Timeline"
|
|
||||||
asp-action="Index"
|
|
||||||
asp-route-projectId="@Model.Project.ProjectID"
|
|
||||||
asp-route-bookId="@Model.SelectedBookID"
|
|
||||||
asp-route-selectedSceneId="@point.SceneID"
|
|
||||||
class="metric-point"
|
|
||||||
style="--metric-height:@point.Percent%"
|
|
||||||
title="Scene @point.SceneNumber: @point.SceneTitle - @point.Value">
|
|
||||||
<span>@point.Value</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@if (Model.SelectedScene is not null)
|
@if (Model.SelectedScene is not null)
|
||||||
|
|||||||
@ -754,6 +754,58 @@ body.timeline-inspector-resizing {
|
|||||||
background: rgba(255, 255, 255, 0.45);
|
background: rgba(255, 255, 255, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-timeline-board {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
margin-top: 14px;
|
||||||
|
border-top: 1px solid var(--plotline-line);
|
||||||
|
padding-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-shape-controls {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 6;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
width: calc(var(--timeline-label-width) + (var(--scene-count) * var(--timeline-scene-width)) + (var(--scene-count) * var(--timeline-grid-gap)));
|
||||||
|
min-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-timeline-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--timeline-label-width) calc((var(--scene-count) * var(--timeline-scene-width)) + ((var(--scene-count) - 1) * var(--timeline-grid-gap)));
|
||||||
|
gap: var(--timeline-grid-gap);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-lane-label {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 12;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 320px;
|
||||||
|
border: 1px solid rgba(47, 111, 99, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
color: var(--plotline-accent-dark);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 6px 14px rgba(30, 37, 43, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-lane-label span {
|
||||||
|
color: var(--plotline-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.colour-swatch {
|
.colour-swatch {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
@ -774,6 +826,7 @@ body.timeline-inspector-resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lane-board-heading,
|
.lane-board-heading,
|
||||||
|
.metric-lane-board-heading,
|
||||||
.plot-lane-board-heading {
|
.plot-lane-board-heading {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -794,6 +847,7 @@ body.timeline-inspector-resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lane-board-heading small,
|
.lane-board-heading small,
|
||||||
|
.metric-lane-board-heading small,
|
||||||
.plot-lane-board-heading small {
|
.plot-lane-board-heading small {
|
||||||
color: var(--plotline-muted);
|
color: var(--plotline-muted);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -1841,11 +1895,36 @@ body.timeline-inspector-resizing {
|
|||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(247, 245, 240, 0.76));
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(247, 245, 240, 0.76));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-timeline-frame {
|
||||||
|
width: calc((var(--scene-count) * var(--timeline-scene-width)) + ((var(--scene-count) - 1) * var(--timeline-grid-gap)));
|
||||||
|
}
|
||||||
|
|
||||||
.metric-chart-frame canvas {
|
.metric-chart-frame canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-timeline-board .metric-graph-stack {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-timeline-board .metric-graph-row {
|
||||||
|
grid-template-columns: var(--timeline-label-width) repeat(var(--scene-count), var(--timeline-scene-width));
|
||||||
|
gap: var(--timeline-grid-gap);
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-timeline-board .metric-graph-points {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-timeline-board .metric-point {
|
||||||
|
border: 1px solid rgba(47, 111, 99, 0.12);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
.metric-raw-details {
|
.metric-raw-details {
|
||||||
border-top: 1px solid var(--plotline-line);
|
border-top: 1px solid var(--plotline-line);
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
|||||||
@ -278,6 +278,14 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const globalDefaultMetrics = new Set(["Overall Intensity", "Tension", "Emotional Weight", "Action"]);
|
const globalDefaultMetrics = new Set(["Overall Intensity", "Tension", "Emotional Weight", "Action"]);
|
||||||
|
const metricCharts = [];
|
||||||
|
const resizeMetricCharts = () => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
metricCharts.forEach((chart) => chart.resize());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.PlotLineMetricCharts = { resizeAll: resizeMetricCharts };
|
||||||
|
window.addEventListener("plotline:timeline-layout-changed", resizeMetricCharts);
|
||||||
|
|
||||||
const chapterGuidePlugin = {
|
const chapterGuidePlugin = {
|
||||||
id: "plotlineChapterGuides",
|
id: "plotlineChapterGuides",
|
||||||
@ -300,11 +308,12 @@
|
|||||||
chapterStarts.push({ scene, index });
|
chapterStarts.push({ scene, index });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const labelInterval = chapterStarts.length > 40 ? 5 : chapterStarts.length > 24 ? 4 : chapterStarts.length > 14 ? 2 : 1;
|
const realChapterStarts = chapterStarts.filter((entry) => !entry.scene.isPlaceholder);
|
||||||
|
const labelInterval = realChapterStarts.length > 40 ? 5 : realChapterStarts.length > 24 ? 4 : realChapterStarts.length > 14 ? 2 : 1;
|
||||||
|
|
||||||
chapterStarts.forEach((entry, chapterIndex) => {
|
realChapterStarts.forEach((entry, chapterIndex) => {
|
||||||
const { scene, index } = entry;
|
const { scene, index } = entry;
|
||||||
const x = scales.x.getPixelForValue(index);
|
const x = scales.x.getPixelForValue(index + 0.5);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x, chartArea.top);
|
ctx.moveTo(x, chartArea.top);
|
||||||
ctx.lineTo(x, chartArea.bottom);
|
ctx.lineTo(x, chartArea.bottom);
|
||||||
@ -312,7 +321,7 @@
|
|||||||
|
|
||||||
const chapterNumber = Number.parseInt(String(scene.chapterLabel).replace(/[^0-9]/g, ""), 10);
|
const chapterNumber = Number.parseInt(String(scene.chapterLabel).replace(/[^0-9]/g, ""), 10);
|
||||||
const shouldLabel = chapterIndex === 0 ||
|
const shouldLabel = chapterIndex === 0 ||
|
||||||
chapterIndex === chapterStarts.length - 1 ||
|
chapterIndex === realChapterStarts.length - 1 ||
|
||||||
labelInterval === 1 ||
|
labelInterval === 1 ||
|
||||||
(Number.isFinite(chapterNumber) && chapterNumber % labelInterval === 0);
|
(Number.isFinite(chapterNumber) && chapterNumber % labelInterval === 0);
|
||||||
if (shouldLabel) {
|
if (shouldLabel) {
|
||||||
@ -351,13 +360,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultMetrics = new Set(payload.defaultMetrics?.length ? payload.defaultMetrics : globalDefaultMetrics);
|
const defaultMetrics = new Set(payload.defaultMetrics?.length ? payload.defaultMetrics : globalDefaultMetrics);
|
||||||
const labels = payload.scenes.map((scene) => scene.axisLabel);
|
const columnCount = payload.scenes.length;
|
||||||
|
const tickLabelInterval = columnCount > 80 ? 10 : columnCount > 40 ? 5 : columnCount > 24 ? 4 : columnCount > 14 ? 2 : 1;
|
||||||
const datasets = payload.metrics.map((metric, metricIndex) => {
|
const datasets = payload.metrics.map((metric, metricIndex) => {
|
||||||
const pointMap = new Map(metric.points.map((point) => [point.sceneId, point.value]));
|
const pointMap = new Map(metric.points.map((point) => [point.sceneId, point.value]));
|
||||||
const colour = colours[metricIndex % colours.length];
|
const colour = colours[metricIndex % colours.length];
|
||||||
return {
|
return {
|
||||||
label: metric.metricName,
|
label: metric.metricName,
|
||||||
data: payload.scenes.map((scene) => pointMap.get(scene.sceneId) ?? null),
|
data: payload.scenes.map((scene, columnIndex) => {
|
||||||
|
const value = pointMap.get(scene.sceneId);
|
||||||
|
return value == null ? { x: columnIndex + 0.5, y: null } : { x: columnIndex + 0.5, y: value };
|
||||||
|
}),
|
||||||
borderColor: colour,
|
borderColor: colour,
|
||||||
backgroundColor: colour,
|
backgroundColor: colour,
|
||||||
pointBackgroundColor: colour,
|
pointBackgroundColor: colour,
|
||||||
@ -388,7 +401,7 @@
|
|||||||
|
|
||||||
const chart = new Chart(canvas, {
|
const chart = new Chart(canvas, {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: { labels, datasets },
|
data: { datasets },
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@ -401,10 +414,29 @@
|
|||||||
grid: { color: "rgba(82, 75, 65, 0.10)" }
|
grid: { color: "rgba(82, 75, 65, 0.10)" }
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
|
type: "linear",
|
||||||
|
min: 0,
|
||||||
|
max: columnCount,
|
||||||
|
bounds: "ticks",
|
||||||
ticks: {
|
ticks: {
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
autoSkip: true,
|
autoSkip: false,
|
||||||
maxTicksLimit: 16
|
maxTicksLimit: 16,
|
||||||
|
callback(value) {
|
||||||
|
const columnIndex = Math.floor(Number(value));
|
||||||
|
if (columnIndex < 0 || columnIndex >= columnCount) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const scene = payload.scenes[columnIndex];
|
||||||
|
if (columnIndex !== 0 && columnIndex !== columnCount - 1 && columnIndex % tickLabelInterval !== 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return scene?.isPlaceholder ? "" : scene?.axisLabel || "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
afterBuildTicks(scale) {
|
||||||
|
scale.ticks = payload.scenes.map((_, columnIndex) => ({ value: columnIndex + 0.5 }));
|
||||||
},
|
},
|
||||||
grid: { display: false }
|
grid: { display: false }
|
||||||
}
|
}
|
||||||
@ -415,14 +447,19 @@
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title(items) {
|
title(items) {
|
||||||
const scene = payload.scenes[items[0].dataIndex];
|
const columnIndex = Math.floor(items[0].parsed.x);
|
||||||
|
const scene = payload.scenes[columnIndex];
|
||||||
|
if (scene.isPlaceholder) {
|
||||||
|
return scene.sceneTitle || "No scene";
|
||||||
|
}
|
||||||
return `${scene.chapterLabel}, Scene ${scene.sceneNumber}: ${scene.sceneTitle}`;
|
return `${scene.chapterLabel}, Scene ${scene.sceneNumber}: ${scene.sceneTitle}`;
|
||||||
},
|
},
|
||||||
label(item) {
|
label(item) {
|
||||||
return `${item.dataset.label}: ${item.formattedValue}`;
|
return `${item.dataset.label}: ${item.formattedValue}`;
|
||||||
},
|
},
|
||||||
afterBody(items) {
|
afterBody(items) {
|
||||||
const scene = payload.scenes[items[0].dataIndex];
|
const columnIndex = Math.floor(items[0].parsed.x);
|
||||||
|
const scene = payload.scenes[columnIndex];
|
||||||
const lines = [];
|
const lines = [];
|
||||||
if (scene.characterPresence) {
|
if (scene.characterPresence) {
|
||||||
lines.push(scene.characterPresence);
|
lines.push(scene.characterPresence);
|
||||||
@ -448,12 +485,21 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scene = payload.scenes[points[0].index];
|
const point = chart.data.datasets[points[0].datasetIndex]?.data?.[points[0].index];
|
||||||
|
const columnIndex = point ? Math.floor(point.x) : points[0].index;
|
||||||
|
const scene = payload.scenes[columnIndex];
|
||||||
if (scene?.timelineUrl) {
|
if (scene?.timelineUrl) {
|
||||||
window.PlotLineTimelineScroll?.save();
|
window.PlotLineTimelineScroll?.save();
|
||||||
window.location.href = scene.timelineUrl;
|
window.location.href = scene.timelineUrl;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
metricCharts.push(chart);
|
||||||
|
const resizeObserver = window.ResizeObserver ? new ResizeObserver(() => chart.resize()) : null;
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.observe(root);
|
||||||
|
resizeObserver.observe(root.querySelector(".metric-chart-frame") || root);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -603,6 +649,7 @@
|
|||||||
if (width > 0) {
|
if (width > 0) {
|
||||||
localStorage.setItem(drawerWidthKey, String(width));
|
localStorage.setItem(drawerWidthKey, String(width));
|
||||||
}
|
}
|
||||||
|
window.dispatchEvent(new Event("plotline:timeline-layout-changed"));
|
||||||
inspectorResizer.removeEventListener("pointermove", resize);
|
inspectorResizer.removeEventListener("pointermove", resize);
|
||||||
inspectorResizer.removeEventListener("pointerup", finish);
|
inspectorResizer.removeEventListener("pointerup", finish);
|
||||||
inspectorResizer.removeEventListener("pointercancel", finish);
|
inspectorResizer.removeEventListener("pointercancel", finish);
|
||||||
@ -767,6 +814,7 @@
|
|||||||
}
|
}
|
||||||
setCollapsed(id, nextCollapsed);
|
setCollapsed(id, nextCollapsed);
|
||||||
saveCollapsed();
|
saveCollapsed();
|
||||||
|
window.dispatchEvent(new Event("plotline:timeline-layout-changed"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user