Extend carry-forward to KPIs with hollow-point chart rendering
- KPI months without an entry now carry the most recent earlier value forward, matching the existing project carry-forward behavior - Chart renders carried-forward points as hollow (surface fill, status-colored ring) with dashed connecting legs to visually distinguish them from recorded values - Milestone KPIs show hollow met/missed markers for carried-forward months - Tooltips and detail rows annotate carried-forward values with "Carried forward from …" - README and grid footnote updated to reflect that both KPIs and projects now carry forward
This commit is contained in:
parent
b810854880
commit
f5a138178d
11
README.md
11
README.md
|
|
@ -92,12 +92,13 @@ items first. (Items grandfathered from before teams existed appear under "No Tea
|
||||||
"Uncategorized".)
|
"Uncategorized".)
|
||||||
|
|
||||||
The grid shows the trailing 12 months; each cell is the **most recent entry in that month**
|
The grid shows the trailing 12 months; each cell is the **most recent entry in that month**
|
||||||
(color + value for KPIs, color for projects, tooltip with date and notes). Project months without
|
(color + value for KPIs, color for projects, tooltip with date and notes). KPI and project months
|
||||||
their own entry **carry the most recent earlier entry forward** (marked "Carried forward from …" in the
|
without their own entry **carry the most recent earlier entry forward** (marked "Carried forward from …"
|
||||||
tooltip and detail row), so a project's latest status stays visible through the current month —
|
in the tooltip, and in a project's detail row), so the latest value or status stays visible through the
|
||||||
months before its first entry remain blank. Rows expand in place:
|
current month — months before the first entry remain blank. Rows expand in place:
|
||||||
**click a KPI row** for an inline SVG trend chart with its threshold zones shaded (line + status-colored
|
**click a KPI row** for an inline SVG trend chart with its threshold zones shaded (line + status-colored
|
||||||
points for numeric KPIs, a met/missed timeline for milestones); **click a project's month cell** for that
|
points for numeric KPIs, a met/missed timeline for milestones; carried-forward months show as hollow
|
||||||
|
points joined by dashed legs); **click a project's month cell** for that
|
||||||
month's notes/headwinds/tailwinds/highlights/blockers. Clicking anywhere else collapses the row.
|
month's notes/headwinds/tailwinds/highlights/blockers. Clicking anywhere else collapses the row.
|
||||||
Group members can click through to any charted item's entries read-only; only the owner (or an
|
Group members can click through to any charted item's entries read-only; only the owner (or an
|
||||||
admin) can edit. Everyone also has a personal chart at `/my-bowler` covering all of their own items.
|
admin) can edit. Everyone also has a personal chart at `/my-bowler` covering all of their own items.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Support;
|
||||||
|
|
||||||
use App\Enums\KpiEvaluation;
|
use App\Enums\KpiEvaluation;
|
||||||
use App\Models\Kpi;
|
use App\Models\Kpi;
|
||||||
|
use App\Models\KpiEntry;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\ProjectEntry;
|
use App\Models\ProjectEntry;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
@ -12,9 +13,9 @@ use Illuminate\Support\Collection;
|
||||||
* Builds the rolling 12-month bowler grid. Rows are grouped
|
* Builds the rolling 12-month bowler grid. Rows are grouped
|
||||||
* Team -> Category (by name, case-insensitively, so same-named teams
|
* Team -> Category (by name, case-insensitively, so same-named teams
|
||||||
* from different owners merge on shared charts); within each month the
|
* from different owners merge on shared charts); within each month the
|
||||||
* most recent entry wins (entry_date desc, id desc). Project months
|
* most recent entry wins (entry_date desc, id desc). Project and KPI
|
||||||
* without their own entry carry the most recent earlier entry forward,
|
* months without their own entry carry the most recent earlier entry
|
||||||
* so the latest status stays visible through the current month. Items
|
* forward, so the latest status stays visible through the current month. Items
|
||||||
* without a team/category fall into "No Team" / "Uncategorized", sorted last.
|
* without a team/category fall into "No Team" / "Uncategorized", sorted last.
|
||||||
*/
|
*/
|
||||||
class BowlerGrid
|
class BowlerGrid
|
||||||
|
|
@ -58,14 +59,14 @@ class BowlerGrid
|
||||||
|
|
||||||
$months = self::months();
|
$months = self::months();
|
||||||
|
|
||||||
$rows = $kpis->map(function (Kpi $kpi) use ($months) {
|
$rows = $kpis->map(function (Kpi $kpi) use ($months, $start) {
|
||||||
$cells = self::kpiCells($kpi);
|
$monthEntries = self::kpiMonthEntries($kpi, $months, $start);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'type' => 'kpi',
|
'type' => 'kpi',
|
||||||
'item' => $kpi,
|
'item' => $kpi,
|
||||||
'cells' => $cells,
|
'cells' => self::kpiCells($kpi, $monthEntries),
|
||||||
'chart' => KpiChart::build($kpi, $cells, $months),
|
'chart' => KpiChart::build($kpi, $monthEntries, $months),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->concat($projects->map(fn (Project $p) => ['type' => 'project', 'item' => $p, 'cells' => self::projectCells($p, $months, $start)]));
|
->concat($projects->map(fn (Project $p) => ['type' => 'project', 'item' => $p, 'cells' => self::projectCells($p, $months, $start)]));
|
||||||
|
|
@ -103,22 +104,66 @@ class BowlerGrid
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string}>
|
* The entry that represents each month for a KPI: its own latest entry,
|
||||||
|
* or (for months without one) the most recent earlier entry carried
|
||||||
|
* forward, including the current month. Months before the first entry
|
||||||
|
* ever stay absent. The fill is seeded with the latest entry that
|
||||||
|
* predates the window, so an old value carries across the gap up to the
|
||||||
|
* first recent entry. Shared by the grid cells and the expanded chart.
|
||||||
|
*
|
||||||
|
* @param array<int, array{key: string, label: string}> $months
|
||||||
|
* @param string $start first day of the 12-month window (Y-m-d)
|
||||||
|
* @return array<string, array{entry: KpiEntry, carriedForward: bool}>
|
||||||
*/
|
*/
|
||||||
private static function kpiCells(Kpi $kpi): array
|
private static function kpiMonthEntries(Kpi $kpi, array $months, string $start): array
|
||||||
{
|
{
|
||||||
$cells = [];
|
$byMonth = [];
|
||||||
|
|
||||||
// Entries are ordered ascending, so the last write per month wins.
|
// Entries are ordered ascending, so the last write per month wins.
|
||||||
foreach ($kpi->entries as $entry) {
|
foreach ($kpi->entries as $entry) {
|
||||||
|
$byMonth[$entry->entry_date->format('Y-m')] = $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filler = $kpi->entries()
|
||||||
|
->where('entry_date', '<', $start)
|
||||||
|
->orderByDesc('entry_date')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($months as $month) {
|
||||||
|
$key = $month['key'];
|
||||||
|
if (isset($byMonth[$key])) {
|
||||||
|
$filler = $byMonth[$key];
|
||||||
|
$result[$key] = ['entry' => $filler, 'carriedForward' => false];
|
||||||
|
} elseif ($filler !== null) {
|
||||||
|
$result[$key] = ['entry' => $filler, 'carriedForward' => true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array{entry: KpiEntry, carriedForward: bool}> $monthEntries
|
||||||
|
* @return array<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string, carriedForward: bool}>
|
||||||
|
*/
|
||||||
|
private static function kpiCells(Kpi $kpi, array $monthEntries): array
|
||||||
|
{
|
||||||
|
$cells = [];
|
||||||
|
|
||||||
|
foreach ($monthEntries as $key => ['entry' => $entry, 'carriedForward' => $carriedForward]) {
|
||||||
$display = $kpi->evaluation === KpiEvaluation::Milestone
|
$display = $kpi->evaluation === KpiEvaluation::Milestone
|
||||||
? ($entry->value >= 1 ? 'Met' : 'Miss')
|
? ($entry->value >= 1 ? 'Met' : 'Miss')
|
||||||
: $kpi->value_type->format($entry->value, $kpi->unit_label);
|
: $kpi->value_type->format($entry->value, $kpi->unit_label);
|
||||||
|
|
||||||
$cells[$entry->entry_date->format('Y-m')] = [
|
$tooltip = trim($entry->entry_date->format('Y-m-d') . ($entry->notes ? " — {$entry->notes}" : ''));
|
||||||
|
|
||||||
|
$cells[$key] = [
|
||||||
'status' => $kpi->evaluate($entry->value),
|
'status' => $kpi->evaluate($entry->value),
|
||||||
'display' => $display,
|
'display' => $display,
|
||||||
'tooltip' => trim($entry->entry_date->format('Y-m-d') . ($entry->notes ? " — {$entry->notes}" : '')),
|
'tooltip' => $carriedForward ? 'Carried forward from ' . $tooltip : $tooltip,
|
||||||
|
'carriedForward' => $carriedForward,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ use App\Models\Kpi;
|
||||||
* View-model builder for the click-to-expand KPI chart on bowler charts.
|
* View-model builder for the click-to-expand KPI chart on bowler charts.
|
||||||
* Produces SVG-ready geometry: threshold zone bands, gridlines, the value
|
* Produces SVG-ready geometry: threshold zone bands, gridlines, the value
|
||||||
* line (split on missing months), status-colored points, and axis labels.
|
* line (split on missing months), status-colored points, and axis labels.
|
||||||
|
* Months without their own entry show the most recent earlier entry carried
|
||||||
|
* forward: hollow points joined by dashed legs (or hollow milestone markers).
|
||||||
*
|
*
|
||||||
* Colors follow the dataviz status palette (state, not series identity):
|
* Colors follow the dataviz status palette (state, not series identity):
|
||||||
* good/warning/critical washes for the zones, points colored by each
|
* good/warning/critical washes for the zones, points colored by each
|
||||||
|
|
@ -36,31 +38,25 @@ class KpiChart
|
||||||
private const PAD_BOTTOM = 30;
|
private const PAD_BOTTOM = 30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string}> $cells
|
* @param array<string, array{entry: \App\Models\KpiEntry, carriedForward: bool}> $monthEntries per-month entry, with earlier entries carried forward into empty months
|
||||||
* @param array<int, array{key: string, label: string}> $months
|
* @param array<int, array{key: string, label: string}> $months
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public static function build(Kpi $kpi, array $cells, array $months): array
|
public static function build(Kpi $kpi, array $monthEntries, array $months): array
|
||||||
{
|
{
|
||||||
if ($kpi->evaluation === KpiEvaluation::Milestone) {
|
if ($kpi->evaluation === KpiEvaluation::Milestone) {
|
||||||
return self::milestone($kpi, $cells, $months);
|
return self::milestone($kpi, $monthEntries, $months);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::line($kpi, $cells, $months);
|
return self::line($kpi, $monthEntries, $months);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private static function line(Kpi $kpi, array $cells, array $months): array
|
private static function line(Kpi $kpi, array $monthEntries, array $months): array
|
||||||
{
|
{
|
||||||
// Values come from the loaded entries window (latest per month, ascending order).
|
$numbers = array_map(fn ($m) => $m['entry']->value, $monthEntries);
|
||||||
$byMonth = [];
|
|
||||||
foreach ($kpi->entries as $entry) {
|
|
||||||
$byMonth[$entry->entry_date->format('Y-m')] = $entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
$numbers = array_map(fn ($e) => $e->value, $byMonth);
|
|
||||||
$thresholds = match ($kpi->evaluation) {
|
$thresholds = match ($kpi->evaluation) {
|
||||||
KpiEvaluation::TargetBand => [
|
KpiEvaluation::TargetBand => [
|
||||||
$kpi->target - $kpi->yellow_tolerance,
|
$kpi->target - $kpi->yellow_tolerance,
|
||||||
|
|
@ -96,41 +92,66 @@ class KpiChart
|
||||||
$gridlines[] = ['y' => $y($tick), 'label' => self::fmtTick($tick, $kpi)];
|
$gridlines[] = ['y' => $y($tick), 'label' => self::fmtTick($tick, $kpi)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Points + line segments (gaps split the path).
|
// Points + line segments. Legs between two real entries are solid (a
|
||||||
|
// gap splits the path); any leg touching a carried-forward point is
|
||||||
|
// dashed so brought-forward values read as "not recorded this month".
|
||||||
$points = [];
|
$points = [];
|
||||||
$segments = [];
|
$solidRuns = [];
|
||||||
$current = [];
|
$dashedLegs = [];
|
||||||
|
$run = [];
|
||||||
|
$prev = null;
|
||||||
|
$lastText = null;
|
||||||
foreach ($months as $i => $month) {
|
foreach ($months as $i => $month) {
|
||||||
$entry = $byMonth[$month['key']] ?? null;
|
$item = $monthEntries[$month['key']] ?? null;
|
||||||
if (! $entry) {
|
if (! $item) {
|
||||||
if (count($current) > 1) {
|
if (count($run) > 1) {
|
||||||
$segments[] = $current;
|
$solidRuns[] = $run;
|
||||||
}
|
}
|
||||||
$current = [];
|
$run = [];
|
||||||
|
$prev = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$entry = $item['entry'];
|
||||||
|
$carried = $item['carriedForward'];
|
||||||
$px = round(self::PAD_LEFT + $slot * ($i + 0.5), 1);
|
$px = round(self::PAD_LEFT + $slot * ($i + 0.5), 1);
|
||||||
$py = $y($entry->value);
|
$py = $y($entry->value);
|
||||||
$status = $kpi->evaluate($entry->value);
|
$status = $kpi->evaluate($entry->value);
|
||||||
|
$valueText = $kpi->value_type->format($entry->value, $kpi->unit_label);
|
||||||
|
$title = $entry->entry_date->format('M Y') . ': ' . $valueText . ' — ' . $status->label();
|
||||||
$points[] = [
|
$points[] = [
|
||||||
'x' => $px,
|
'x' => $px,
|
||||||
'y' => $py,
|
'y' => $py,
|
||||||
'color' => self::statusColor($status->value),
|
'color' => self::statusColor($status->value),
|
||||||
'title' => $entry->entry_date->format('M Y') . ': '
|
'title' => $carried ? 'Carried forward from ' . $title : $title,
|
||||||
. $kpi->value_type->format($entry->value, $kpi->unit_label)
|
|
||||||
. ' — ' . $status->label(),
|
|
||||||
'note' => $entry->notes,
|
'note' => $entry->notes,
|
||||||
|
'carriedForward' => $carried,
|
||||||
];
|
];
|
||||||
$current[] = [$px, $py];
|
$lastText = $valueText;
|
||||||
|
|
||||||
|
if ($prev !== null) {
|
||||||
|
if (! $prev['carried'] && ! $carried) {
|
||||||
|
if ($run === []) {
|
||||||
|
$run[] = $prev['xy'];
|
||||||
}
|
}
|
||||||
if (count($current) > 1) {
|
$run[] = [$px, $py];
|
||||||
$segments[] = $current;
|
} else {
|
||||||
|
if (count($run) > 1) {
|
||||||
|
$solidRuns[] = $run;
|
||||||
|
}
|
||||||
|
$run = [];
|
||||||
|
$dashedLegs[] = [$prev['xy'], [$px, $py]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$prev = ['xy' => [$px, $py], 'carried' => $carried];
|
||||||
|
}
|
||||||
|
if (count($run) > 1) {
|
||||||
|
$solidRuns[] = $run;
|
||||||
}
|
}
|
||||||
|
|
||||||
$paths = array_map(
|
$toPath = fn ($seg) => 'M' . implode(' L', array_map(fn ($p) => "{$p[0]},{$p[1]}", $seg));
|
||||||
fn ($seg) => 'M' . implode(' L', array_map(fn ($p) => "{$p[0]},{$p[1]}", $seg)),
|
$paths = array_map($toPath, $solidRuns);
|
||||||
$segments
|
$carriedPaths = array_map($toPath, $dashedLegs);
|
||||||
);
|
|
||||||
|
|
||||||
// Selective labeling: the value at the line's end only.
|
// Selective labeling: the value at the line's end only.
|
||||||
$endLabel = null;
|
$endLabel = null;
|
||||||
|
|
@ -139,7 +160,7 @@ class KpiChart
|
||||||
$endLabel = [
|
$endLabel = [
|
||||||
'x' => $last['x'] + 9,
|
'x' => $last['x'] + 9,
|
||||||
'y' => $last['y'] + 4,
|
'y' => $last['y'] + 4,
|
||||||
'text' => $kpi->value_type->format($byMonth[array_key_last($byMonth)]->value, $kpi->unit_label),
|
'text' => $lastText,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,6 +173,7 @@ class KpiChart
|
||||||
'refLines' => $refLines,
|
'refLines' => $refLines,
|
||||||
'gridlines' => $gridlines,
|
'gridlines' => $gridlines,
|
||||||
'paths' => $paths,
|
'paths' => $paths,
|
||||||
|
'carriedPaths' => $carriedPaths,
|
||||||
'lineColor' => self::LINE,
|
'lineColor' => self::LINE,
|
||||||
'points' => $points,
|
'points' => $points,
|
||||||
'endLabel' => $endLabel,
|
'endLabel' => $endLabel,
|
||||||
|
|
@ -215,13 +237,8 @@ class KpiChart
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private static function milestone(Kpi $kpi, array $cells, array $months): array
|
private static function milestone(Kpi $kpi, array $monthEntries, array $months): array
|
||||||
{
|
{
|
||||||
$byMonth = [];
|
|
||||||
foreach ($kpi->entries as $entry) {
|
|
||||||
$byMonth[$entry->entry_date->format('Y-m')] = $entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
$height = 110;
|
$height = 110;
|
||||||
$plotW = self::W - self::PAD_LEFT - self::PAD_RIGHT;
|
$plotW = self::W - self::PAD_LEFT - self::PAD_RIGHT;
|
||||||
$slot = $plotW / count($months);
|
$slot = $plotW / count($months);
|
||||||
|
|
@ -229,18 +246,21 @@ class KpiChart
|
||||||
|
|
||||||
$markers = [];
|
$markers = [];
|
||||||
foreach ($months as $i => $month) {
|
foreach ($months as $i => $month) {
|
||||||
$entry = $byMonth[$month['key']] ?? null;
|
$item = $monthEntries[$month['key']] ?? null;
|
||||||
if (! $entry) {
|
if (! $item) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
$entry = $item['entry'];
|
||||||
$met = $entry->value >= 1;
|
$met = $entry->value >= 1;
|
||||||
|
$title = $entry->entry_date->format('M Y') . ': ' . ($met ? 'Met' : 'Not met');
|
||||||
$markers[] = [
|
$markers[] = [
|
||||||
'x' => round(self::PAD_LEFT + $slot * ($i + 0.5), 1),
|
'x' => round(self::PAD_LEFT + $slot * ($i + 0.5), 1),
|
||||||
'y' => $baseline,
|
'y' => $baseline,
|
||||||
'color' => $met ? self::GOOD : self::CRIT,
|
'color' => $met ? self::GOOD : self::CRIT,
|
||||||
'glyph' => $met ? '✓' : '✕',
|
'glyph' => $met ? '✓' : '✕',
|
||||||
'title' => $entry->entry_date->format('M Y') . ': ' . ($met ? 'Met' : 'Not met'),
|
'title' => $item['carriedForward'] ? 'Carried forward from ' . $title : $title,
|
||||||
'note' => $entry->notes,
|
'note' => $entry->notes,
|
||||||
|
'carriedForward' => $item['carriedForward'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,8 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-gray-400">
|
<p class="mt-2 text-xs text-gray-400">
|
||||||
Each cell shows the most recent entry in that month; project months without an entry carry the most recent
|
Each cell shows the most recent entry in that month; months without an entry carry the most recent
|
||||||
status forward (the tooltip marks it). The current month is highlighted.
|
KPI value or project status forward (the tooltip marks it). The current month is highlighted.
|
||||||
Click a KPI row to expand its trend chart; click a project row for its latest details, or a specific cell for that month.
|
Click a KPI row to expand its trend chart; click a project row for its latest details, or a specific cell for that month.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,20 +40,32 @@
|
||||||
stroke-linecap="round" stroke-linejoin="round" />
|
stroke-linecap="round" stroke-linejoin="round" />
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
{{-- Points, status-colored with a 2px surface ring. Noted points get a
|
{{-- Carried-forward legs (any leg touching a brought-forward month): dashed, no draw-in --}}
|
||||||
pulsing halo and a custom hover tooltip instead of the native <title>. --}}
|
@foreach ($chart['carriedPaths'] as $path)
|
||||||
|
<path d="{{ $path }}" fill="none" style="stroke: var(--theme-chart-line)" stroke-width="2"
|
||||||
|
stroke-dasharray="4 3" stroke-linecap="round" stroke-opacity="0.7" />
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
{{-- Points, status-colored with a 2px surface ring (carried-forward points are hollow:
|
||||||
|
surface fill, status-colored ring). Noted points get a pulsing halo and a custom
|
||||||
|
hover tooltip instead of the native <title>. --}}
|
||||||
@foreach ($chart['points'] as $point)
|
@foreach ($chart['points'] as $point)
|
||||||
|
@php
|
||||||
|
$hollow = !empty($point['carriedForward']);
|
||||||
|
$pointFill = $hollow ? '#ffffff' : $point['color'];
|
||||||
|
$pointStroke = $hollow ? $point['color'] : '#ffffff';
|
||||||
|
@endphp
|
||||||
@if (!empty($point['note']))
|
@if (!empty($point['note']))
|
||||||
<g class="kpi-pop js-note-point" style="--i: {{ $loop->index }}"
|
<g class="kpi-pop js-note-point" style="--i: {{ $loop->index }}"
|
||||||
data-note="{{ $point['note'] }}" data-title="{{ $point['title'] }}">
|
data-note="{{ $point['note'] }}" data-title="{{ $point['title'] }}">
|
||||||
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="8" class="kpi-note-ring" />
|
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="8" class="kpi-note-ring" />
|
||||||
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="4.5"
|
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="4.5"
|
||||||
fill="{{ $point['color'] }}" stroke="#ffffff" stroke-width="2" />
|
fill="{{ $pointFill }}" stroke="{{ $pointStroke }}" stroke-width="2" />
|
||||||
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="12" fill="transparent" class="js-note-hit" />
|
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="12" fill="transparent" class="js-note-hit" />
|
||||||
</g>
|
</g>
|
||||||
@else
|
@else
|
||||||
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="4.5" class="kpi-pop" style="--i: {{ $loop->index }}"
|
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="4.5" class="kpi-pop" style="--i: {{ $loop->index }}"
|
||||||
fill="{{ $point['color'] }}" stroke="#ffffff" stroke-width="2">
|
fill="{{ $pointFill }}" stroke="{{ $pointStroke }}" stroke-width="2">
|
||||||
<title>{{ $point['title'] }}</title>
|
<title>{{ $point['title'] }}</title>
|
||||||
</circle>
|
</circle>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -78,22 +90,29 @@
|
||||||
<line x1="{{ $chart['plot']['x'] }}" y1="{{ $chart['baseline'] }}" x2="{{ $chart['plot']['x'] + $chart['plot']['w'] }}" y2="{{ $chart['baseline'] }}"
|
<line x1="{{ $chart['plot']['x'] }}" y1="{{ $chart['baseline'] }}" x2="{{ $chart['plot']['x'] + $chart['plot']['w'] }}" y2="{{ $chart['baseline'] }}"
|
||||||
stroke="{{ \App\Support\KpiChart::GRIDLINE }}" stroke-width="1" />
|
stroke="{{ \App\Support\KpiChart::GRIDLINE }}" stroke-width="1" />
|
||||||
@foreach ($chart['markers'] as $marker)
|
@foreach ($chart['markers'] as $marker)
|
||||||
|
{{-- Carried-forward markers are hollow: surface fill, status-colored ring and glyph --}}
|
||||||
|
@php
|
||||||
|
$hollow = !empty($marker['carriedForward']);
|
||||||
|
$markerFill = $hollow ? '#ffffff' : $marker['color'];
|
||||||
|
$markerStroke = $hollow ? $marker['color'] : '#ffffff';
|
||||||
|
$glyphFill = $hollow ? $marker['color'] : '#ffffff';
|
||||||
|
@endphp
|
||||||
@if (!empty($marker['note']))
|
@if (!empty($marker['note']))
|
||||||
<g class="kpi-pop js-note-point" style="--i: {{ $loop->index }}"
|
<g class="kpi-pop js-note-point" style="--i: {{ $loop->index }}"
|
||||||
data-note="{{ $marker['note'] }}" data-title="{{ $marker['title'] }}">
|
data-note="{{ $marker['note'] }}" data-title="{{ $marker['title'] }}">
|
||||||
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="13" class="kpi-note-ring" />
|
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="13" class="kpi-note-ring" />
|
||||||
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="9"
|
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="9"
|
||||||
fill="{{ $marker['color'] }}" stroke="#ffffff" stroke-width="2" />
|
fill="{{ $markerFill }}" stroke="{{ $markerStroke }}" stroke-width="2" />
|
||||||
<text x="{{ $marker['x'] }}" y="{{ $marker['y'] + 3.5 }}" text-anchor="middle"
|
<text x="{{ $marker['x'] }}" y="{{ $marker['y'] + 3.5 }}" text-anchor="middle"
|
||||||
font-size="10" font-weight="700" fill="#ffffff">{{ $marker['glyph'] }}</text>
|
font-size="10" font-weight="700" fill="{{ $glyphFill }}">{{ $marker['glyph'] }}</text>
|
||||||
</g>
|
</g>
|
||||||
@else
|
@else
|
||||||
<g class="kpi-pop" style="--i: {{ $loop->index }}">
|
<g class="kpi-pop" style="--i: {{ $loop->index }}">
|
||||||
<title>{{ $marker['title'] }}</title>
|
<title>{{ $marker['title'] }}</title>
|
||||||
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="9"
|
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="9"
|
||||||
fill="{{ $marker['color'] }}" stroke="#ffffff" stroke-width="2" />
|
fill="{{ $markerFill }}" stroke="{{ $markerStroke }}" stroke-width="2" />
|
||||||
<text x="{{ $marker['x'] }}" y="{{ $marker['y'] + 3.5 }}" text-anchor="middle"
|
<text x="{{ $marker['x'] }}" y="{{ $marker['y'] + 3.5 }}" text-anchor="middle"
|
||||||
font-size="10" font-weight="700" fill="#ffffff">{{ $marker['glyph'] }}</text>
|
font-size="10" font-weight="700" fill="{{ $glyphFill }}">{{ $marker['glyph'] }}</text>
|
||||||
</g>
|
</g>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue