From ade83e50bb7798f12645206c9bb2fca197085924 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Tue, 31 Mar 2026 14:44:42 +0530 Subject: [PATCH] GWM : chart improvemnet --- app/Libraries/ChartRenderer.php | 107 ++++++++++++++++++++++---- app/Views/chart/builder_scripts.php | 45 ++++++++++- app/Views/dashboard/_view_scripts.php | 47 ++++++++++- public/assets/css/dashboard.css | 39 ++++++++-- 4 files changed, 208 insertions(+), 30 deletions(-) diff --git a/app/Libraries/ChartRenderer.php b/app/Libraries/ChartRenderer.php index 54f142f..952a568 100644 --- a/app/Libraries/ChartRenderer.php +++ b/app/Libraries/ChartRenderer.php @@ -9,12 +9,12 @@ class ChartRenderer { /** @var array> */ private const PALETTES = [ - 'mono' => ['#111827', '#1e3a5f', '#57657a', '#93c5fd', '#e5e7eb'], - 'ocean' => ['#0c4a6e', '#0369a1', '#0ea5e9', '#38bdf8', '#bae6fd'], - 'forest' => ['#14532d', '#166534', '#22c55e', '#4ade80', '#bbf7d0'], - 'sunset' => ['#7f1d1d', '#dc2626', '#f97316', '#fbbf24', '#fef3c7'], - 'slate' => ['#0f172a', '#334155', '#64748b', '#94a3b8', '#e2e8f0'], - 'violet' => ['#4c1d95', '#6d28d9', '#8b5cf6', '#a78bfa', '#ddd6fe'], + 'mono' => ['#334155', '#475569', '#64748b', '#94a3b8', '#cbd5e1'], + 'ocean' => ['#36a2eb', '#4bc0c0', '#a8e6ff', '#5b8ff9', '#2f7ed8'], + 'forest' => ['#22c55e', '#16a34a', '#86efac', '#65a30d', '#14532d'], + 'sunset' => ['#ff9f40', '#ffd166', '#f97316', '#fb7185', '#ef4444'], + 'slate' => ['#0f172a', '#1e293b', '#334155', '#475569', '#64748b'], + 'violet' => ['#8b5cf6', '#a78bfa', '#c4b5fd', '#d946ef', '#7c3aed'], ]; /** @@ -145,7 +145,7 @@ class ChartRenderer 'subtitle' => $subtitleText !== '' ? ['text' => $subtitleText, 'align' => 'left', 'style' => ['fontSize' => '12px', 'color' => '#64748b']] : ['text' => ''], 'labels' => $labels, 'series' => $series, - 'colors' => array_slice($colors, 0, max(count($series), 1)), + 'colors' => $this->ensureColorCount($colors, max(count($series), 1)), 'legend' => $legend, 'dataLabels' => $dataLabels, 'stroke' => ['width' => $type === 'polar_area' ? 1 : 0], @@ -244,7 +244,7 @@ class ChartRenderer 'fill' => ['type' => 'solid', 'opacity' => 0.2], 'yaxis' => ['show' => false], 'dataLabels' => $dataLabels, - 'colors' => array_slice($colors, 0, max(count($series), 3)), + 'colors' => $this->ensureColorCount($colors, max(count($series), 3)), 'legend' => $legend, 'tooltip' => ['theme' => 'light'], ]; @@ -273,7 +273,7 @@ class ChartRenderer 'borderRadius' => 6, ], ], - 'colors' => array_slice($colors, 0, max(count($vals), 1)), + 'colors' => $this->ensureColorCount($colors, max(count($vals), 1)), 'dataLabels' => array_merge($dataLabels, ['enabled' => true]), 'xaxis' => ['categories' => $cats, 'labels' => ['style' => ['colors' => '#64748b']]], 'yaxis' => ['labels' => ['style' => ['colors' => '#64748b']]], @@ -433,7 +433,7 @@ class ChartRenderer ], 'yaxis' => $this->buildYAxis($display, $y, false), 'plotOptions' => ['bar' => ['columnWidth' => '55%', 'borderRadius' => 5]], - 'colors' => array_slice($colors, 0, 4), + 'colors' => $this->ensureColorCount($colors, 4), 'grid' => $grid, 'legend' => $legend, 'fill' => ['type' => 'solid', 'opacity' => [1, 1]], @@ -481,7 +481,7 @@ class ChartRenderer 'stroke' => ['curve' => $curve, 'width' => $type === 'bar' ? 0 : 2.5], 'markers' => ['size' => in_array($type, ['line', 'area', 'spline', 'stepline'], true) ? 0 : 4], 'dataLabels' => $dataLabels, - 'colors' => array_slice($colors, 0, max(count($series), 3)), + 'colors' => $this->ensureColorCount($colors, max(count($series), 3)), 'grid' => $grid, 'legend' => $legend, 'plotOptions' => ['bar' => $plotBar], @@ -677,19 +677,94 @@ class ChartRenderer return $mono; } - if ($mode !== 'custom' && is_array($custom) && $custom !== []) { - $fromLegacy = array_values(array_filter(array_map('strval', $custom))); + $name = (string) ($display['palette'] ?? 'mono'); + $pal = self::PALETTES[$name] ?? $mono; + + // Backward compatibility: use old palette_colors only if palette name is missing. + if (! isset($display['palette']) && is_array($custom) && $custom !== []) { + $fromLegacy = []; + foreach ($custom as $c) { + $s = is_scalar($c) ? trim((string) $c) : ''; + if ($s !== '' && preg_match('/^#[0-9A-Fa-f]{6}$/', $s)) { + $fromLegacy[] = $s; + } + } if ($fromLegacy !== []) { return $fromLegacy; } } - $name = (string) ($display['palette'] ?? 'mono'); - $pal = self::PALETTES[$name] ?? $mono; - return $pal !== [] ? $pal : $mono; } + /** + * Extend a base palette to provide enough visually distinct colors. + * + * @param array $base + * @return array + */ + private function ensureColorCount(array $base, int $count): array + { + $count = max(1, $count); + $colors = []; + foreach ($base as $c) { + if (is_string($c) && preg_match('/^#[0-9A-Fa-f]{6}$/', $c)) { + $colors[] = strtolower($c); + } + } + if ($colors === []) { + $colors = self::PALETTES['ocean']; + } + if (count($colors) >= $count) { + return array_slice($colors, 0, $count); + } + + // Golden-angle hue steps avoid near-neighbor collisions when many series exist. + for ($i = count($colors); $i < $count; $i++) { + $hue = fmod(212.0 + ($i * 137.508), 360.0); + $sat = 62.0 + (($i % 3) * 6.0); + $light = 52.0 - (($i % 4) * 4.0); + $colors[] = $this->hslToHex($hue, min(78.0, $sat), max(36.0, $light)); + } + + return $colors; + } + + private function hslToHex(float $h, float $s, float $l): string + { + $h = fmod(max(0.0, $h), 360.0); + $s = max(0.0, min(100.0, $s)) / 100.0; + $l = max(0.0, min(100.0, $l)) / 100.0; + + $c = (1.0 - abs((2.0 * $l) - 1.0)) * $s; + $hp = $h / 60.0; + $x = $c * (1.0 - abs(fmod($hp, 2.0) - 1.0)); + $r1 = 0.0; + $g1 = 0.0; + $b1 = 0.0; + + if ($hp >= 0.0 && $hp < 1.0) { + $r1 = $c; $g1 = $x; $b1 = 0.0; + } elseif ($hp < 2.0) { + $r1 = $x; $g1 = $c; $b1 = 0.0; + } elseif ($hp < 3.0) { + $r1 = 0.0; $g1 = $c; $b1 = $x; + } elseif ($hp < 4.0) { + $r1 = 0.0; $g1 = $x; $b1 = $c; + } elseif ($hp < 5.0) { + $r1 = $x; $g1 = 0.0; $b1 = $c; + } else { + $r1 = $c; $g1 = 0.0; $b1 = $x; + } + + $m = $l - ($c / 2.0); + $r = (int) round(($r1 + $m) * 255); + $g = (int) round(($g1 + $m) * 255); + $b = (int) round(($b1 + $m) * 255); + + return sprintf('#%02x%02x%02x', max(0, min(255, $r)), max(0, min(255, $g)), max(0, min(255, $b))); + } + /** * @return array */ diff --git a/app/Views/chart/builder_scripts.php b/app/Views/chart/builder_scripts.php index d1f802b..dc80493 100644 --- a/app/Views/chart/builder_scripts.php +++ b/app/Views/chart/builder_scripts.php @@ -45,6 +45,44 @@ return opts; } + function hslToHex(h, s, l) { + const hh = ((Number(h) % 360) + 360) % 360; + const ss = Math.max(0, Math.min(100, Number(s))) / 100; + const ll = Math.max(0, Math.min(100, Number(l))) / 100; + const c = (1 - Math.abs((2 * ll) - 1)) * ss; + const hp = hh / 60; + const x = c * (1 - Math.abs((hp % 2) - 1)); + let r1 = 0, g1 = 0, b1 = 0; + if (hp >= 0 && hp < 1) { r1 = c; g1 = x; } + else if (hp < 2) { r1 = x; g1 = c; } + else if (hp < 3) { g1 = c; b1 = x; } + else if (hp < 4) { g1 = x; b1 = c; } + else if (hp < 5) { r1 = x; b1 = c; } + else { r1 = c; b1 = x; } + const m = ll - (c / 2); + const r = Math.round((r1 + m) * 255); + const g = Math.round((g1 + m) * 255); + const b = Math.round((b1 + m) * 255); + return '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join(''); + } + + function ensureApexColorCoverage(opts) { + if (!opts || typeof opts !== 'object') return opts; + const seriesCount = Array.isArray(opts.series) ? opts.series.length : 0; + const labelCount = Array.isArray(opts.labels) ? opts.labels.length : 0; + const needed = Math.max(seriesCount, labelCount, 1); + const base = Array.isArray(opts.colors) ? opts.colors.filter((c) => /^#[0-9A-Fa-f]{6}$/.test(String(c))) : []; + const colors = base.length ? base.slice() : ['#36a2eb', '#4bc0c0', '#ffcd56', '#ff9f40', '#9966ff']; + for (let i = colors.length; i < needed; i++) { + const hue = (220 + (i * 137.508)) % 360; + const sat = 64 + ((i % 3) * 6); + const light = 50 - ((i % 4) * 4); + colors.push(hslToHex(hue, Math.min(78, sat), Math.max(36, light))); + } + opts.colors = colors.slice(0, needed); + return opts; + } + function patchCsrfFromResponse(j) { if (!j || !j.csrf) return; CB_CSRF.name = j.csrf.name; @@ -89,6 +127,7 @@ if (payload.engine === 'apex' && payload.options && typeof ApexCharts !== 'undefined') { const opts = JSON.parse(JSON.stringify(payload.options)); applyFormatHints(opts, hints || payload.format_hints); + ensureApexColorCoverage(opts); const chart = new ApexCharts(el, opts); el._apexChart = chart; const done = chart.render(); @@ -114,8 +153,8 @@ horizontal_bar: false, stacked: false, color_mode: 'preset', - palette: 'mono', - palette_colors: ['#111827', '#2563eb', '#059669', '#d97706', '#dc2626'], + palette: 'ocean', + palette_colors: ['#36a2eb', '#4bc0c0', '#ffcd56', '#ff9f40', '#9966ff'], number_format: 'auto', decimal_places: 1, y_min: '', @@ -183,7 +222,7 @@ }, ensurePaletteColors() { - const defs = ['#111827', '#2563eb', '#059669', '#d97706', '#dc2626', '#7c3aed', '#db2777', '#0891b2']; + const defs = ['#36a2eb', '#4bc0c0', '#ffcd56', '#ff9f40', '#9966ff', '#2f7ed8', '#26c6da', '#f87171']; if (!Array.isArray(this.display.palette_colors)) { this.display.palette_colors = defs.slice(0, 5); } else { diff --git a/app/Views/dashboard/_view_scripts.php b/app/Views/dashboard/_view_scripts.php index 0a0f446..cc5eaf5 100644 --- a/app/Views/dashboard/_view_scripts.php +++ b/app/Views/dashboard/_view_scripts.php @@ -64,6 +64,44 @@ return opts; } + function hslToHex(h, s, l) { + const hh = ((Number(h) % 360) + 360) % 360; + const ss = Math.max(0, Math.min(100, Number(s))) / 100; + const ll = Math.max(0, Math.min(100, Number(l))) / 100; + const c = (1 - Math.abs((2 * ll) - 1)) * ss; + const hp = hh / 60; + const x = c * (1 - Math.abs((hp % 2) - 1)); + let r1 = 0, g1 = 0, b1 = 0; + if (hp >= 0 && hp < 1) { r1 = c; g1 = x; } + else if (hp < 2) { r1 = x; g1 = c; } + else if (hp < 3) { g1 = c; b1 = x; } + else if (hp < 4) { g1 = x; b1 = c; } + else if (hp < 5) { r1 = x; b1 = c; } + else { r1 = c; b1 = x; } + const m = ll - (c / 2); + const r = Math.round((r1 + m) * 255); + const g = Math.round((g1 + m) * 255); + const b = Math.round((b1 + m) * 255); + return '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join(''); + } + + function ensureApexColorCoverage(opts) { + if (!opts || typeof opts !== 'object') return opts; + const seriesCount = Array.isArray(opts.series) ? opts.series.length : 0; + const labelCount = Array.isArray(opts.labels) ? opts.labels.length : 0; + const needed = Math.max(seriesCount, labelCount, 1); + const base = Array.isArray(opts.colors) ? opts.colors.filter((c) => /^#[0-9A-Fa-f]{6}$/.test(String(c))) : []; + const colors = base.length ? base.slice() : ['#36a2eb', '#4bc0c0', '#ffcd56', '#ff9f40', '#9966ff']; + for (let i = colors.length; i < needed; i++) { + const hue = (220 + (i * 137.508)) % 360; + const sat = 64 + ((i % 3) * 6); + const light = 50 - ((i % 4) * 4); + colors.push(hslToHex(hue, Math.min(78, sat), Math.max(36, light))); + } + opts.colors = colors.slice(0, needed); + return opts; + } + function mountPayload(el, payload, hints) { if (!el) return; if (el._apexChart) { @@ -103,6 +141,7 @@ if (payload.engine === 'apex' && payload.options && typeof ApexCharts !== 'undefined') { const opts = JSON.parse(JSON.stringify(payload.options)); applyFormatHints(opts, hints || payload.format_hints); + ensureApexColorCoverage(opts); const chart = new ApexCharts(el, opts); el._apexChart = chart; const done = chart.render(); @@ -396,10 +435,10 @@ grid = GridStack.init({ column: 12, cellHeight: 76, - marginTop: 6, - marginBottom: 6, - marginLeft: 5, - marginRight: 5, + marginTop: 10, + marginBottom: 10, + marginLeft: 8, + marginRight: 8, animate: true, float: false, staticGrid: true, diff --git a/public/assets/css/dashboard.css b/public/assets/css/dashboard.css index c6e5373..163dadc 100644 --- a/public/assets/css/dashboard.css +++ b/public/assets/css/dashboard.css @@ -247,7 +247,7 @@ align-items: center; justify-content: space-between; gap: 8px; - padding: 10px 14px; + padding: 11px 14px; border-bottom: 1px solid #f1f5f9; font-size: 13px; font-weight: 700; @@ -263,7 +263,7 @@ flex: 1 1 auto; min-height: 0; position: relative; - padding: 8px 10px 12px; + padding: 10px 12px 14px; display: flex; flex-direction: column; } @@ -305,20 +305,45 @@ } .cb-dash-prose { - font-size: 13px; - line-height: 1.55; + font-size: 14px; + line-height: 1.65; color: #334155; - padding: 4px 6px; + padding: 8px 10px; + background: #fcfdff; + border: 1px solid #edf1f6; + border-radius: 10px; + height: 100%; + overflow: auto; } .cb-dash-shell.cb-theme-dark .cb-dash-prose { color: #cbd5e1; + background: rgba(15, 23, 42, 0.4); + border-color: rgba(148, 163, 184, 0.2); } .cb-dash-prose h1, .cb-dash-prose h2, .cb-dash-prose h3 { - font-size: 1rem; + font-size: 1.02rem; font-weight: 800; - margin: 0.5em 0 0.25em; + margin: 0.45em 0 0.3em; +} + +.cb-dash-prose p { + margin: 0 0 0.65em; +} + +.cb-dash-prose p:last-child { + margin-bottom: 0; +} + +.cb-dash-prose ul, +.cb-dash-prose ol { + margin: 0 0 0.7em 1.15rem; + padding: 0; +} + +.cb-dash-prose li { + margin-bottom: 0.3em; } .cb-dash-img {