GWM : chart improvemnet

This commit is contained in:
Gowtham M 2026-03-31 14:44:42 +05:30
parent d4dcb1d270
commit ade83e50bb
4 changed files with 208 additions and 30 deletions

View File

@ -9,12 +9,12 @@ class ChartRenderer
{
/** @var array<string, array<int, string>> */
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<int, string> $base
* @return array<int, string>
*/
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<string, mixed>
*/

View File

@ -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 {

View File

@ -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,

View File

@ -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 {