GWM : chart improvemnet
This commit is contained in:
parent
d4dcb1d270
commit
ade83e50bb
@ -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>
|
||||
*/
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user