trying to port web app starter from PHP
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('sortable_table_format_bytes')) {
|
||||
function sortable_table_format_bytes($bytes, $disk = false)
|
||||
{
|
||||
if ($bytes === null || $bytes === '') return '--';
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
$value = (float)$bytes;
|
||||
$unitIndex = 0;
|
||||
while (abs($value) >= 1024 && $unitIndex < count($units) - 1) {
|
||||
$value /= 1024;
|
||||
$unitIndex += 1;
|
||||
}
|
||||
$decimals = $disk ? ($unitIndex >= 4 ? 2 : ($unitIndex >= 1 ? 1 : 0)) : ($unitIndex === 0 ? 0 : 1);
|
||||
return number_format($value, $decimals) . ' ' . $units[$unitIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sortable_table_format_value')) {
|
||||
function sortable_table_format_value($value, $row, $column)
|
||||
{
|
||||
$format = $column['format'] ?? null;
|
||||
if (is_callable($format)) {
|
||||
return [$format($value, $row, $column), $value];
|
||||
}
|
||||
|
||||
switch ($format) {
|
||||
case 'number':
|
||||
return [number_format((float)$value), (float)$value];
|
||||
case 'bytes':
|
||||
return [sortable_table_format_bytes($value, false), (float)$value];
|
||||
case 'disk-bytes':
|
||||
return [sortable_table_format_bytes($value, true), (float)$value];
|
||||
case 'percent':
|
||||
return [number_format((float)$value, 1) . '%', (float)$value];
|
||||
case 'duration-ms':
|
||||
$number = (float)$value;
|
||||
if ($number >= 1000) {
|
||||
return [number_format($number / 1000, $number >= 10000 ? 0 : 1) . ' s', $number];
|
||||
}
|
||||
return [number_format($number, $number >= 100 ? 0 : 1) . ' ms', $number];
|
||||
case 'bool':
|
||||
return [$value ? 'Yes' : 'No', $value ? 1 : 0];
|
||||
default:
|
||||
if (is_bool($value)) {
|
||||
return [$value ? 'Yes' : 'No', $value ? 1 : 0];
|
||||
}
|
||||
return [(string)$value, is_scalar($value) ? $value : ''];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'render' => function($prop) {
|
||||
include_js('js/u-format.js');
|
||||
include_js('js/u-sortable-table.js');
|
||||
|
||||
$tableId = (string)($prop['id'] ?? ('sortable-table-' . uniqid()));
|
||||
$title = (string)($prop['title'] ?? '');
|
||||
$subtitle = (string)($prop['subtitle'] ?? '');
|
||||
$columns = is_array($prop['columns'] ?? null) ? $prop['columns'] : [];
|
||||
$rows = is_array($prop['rows'] ?? null) ? $prop['rows'] : [];
|
||||
$emptyLabel = (string)($prop['empty_label'] ?? 'No data available');
|
||||
$storageKey = (string)($prop['storage_key'] ?? ('starter.sort.' . $tableId));
|
||||
$sort = is_array($prop['sort'] ?? null) ? $prop['sort'] : [];
|
||||
$initOptions = ['storageKey' => $storageKey];
|
||||
if (isset($sort['column'])) {
|
||||
$initOptions['initialSort'] = [
|
||||
'column' => (int)$sort['column'],
|
||||
'direction' => (($sort['direction'] ?? 'asc') === 'desc') ? 'desc' : 'asc',
|
||||
];
|
||||
}
|
||||
$jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
|
||||
ob_start();
|
||||
?>
|
||||
<div class="dashboard-panel">
|
||||
<?php if ($title !== '' || $subtitle !== ''): ?>
|
||||
<div class="dashboard-panel-header">
|
||||
<?php if ($title !== ''): ?><h2><?= safe($title) ?></h2><?php endif; ?>
|
||||
<?php if ($subtitle !== ''): ?><p><?= safe($subtitle) ?></p><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="dashboard-table-wrap">
|
||||
<table id="<?= asafe($tableId) ?>" class="u-sortable-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<?php foreach ($columns as $column): ?>
|
||||
<?php
|
||||
$align = (string)($column['align'] ?? 'left');
|
||||
$sortable = !isset($column['sortable']) || $column['sortable'];
|
||||
?>
|
||||
<th scope="col" class="align-<?= asafe($align) ?>"<?= !$sortable ? ' data-sortable="false"' : '' ?>><?= safe((string)($column['label'] ?? $column['key'] ?? 'Column')) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!$rows): ?>
|
||||
<tr data-empty-row="1"><td colspan="<?= count($columns) ?: 1 ?>" class="muted"><?= safe($emptyLabel) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<tr>
|
||||
<?php foreach ($columns as $column): ?>
|
||||
<?php
|
||||
$key = (string)($column['key'] ?? '');
|
||||
$value = $row[$key] ?? '';
|
||||
list($displayValue, $sortValue) = sortable_table_format_value($value, $row, $column);
|
||||
if (isset($column['sort_value']) && is_callable($column['sort_value'])) {
|
||||
$sortValue = $column['sort_value']($value, $row, $column);
|
||||
}
|
||||
?>
|
||||
<td class="align-<?= asafe((string)($column['align'] ?? 'left')) ?>" data-sort-value="<?= asafe((string)$sortValue) ?>"><?= safe((string)$displayValue) ?></td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
if (typeof USortableTable === 'undefined') return;
|
||||
USortableTable.init(<?= jsafe($tableId) ?>, <?= json_encode($initOptions, $jsonFlags) ?>);
|
||||
}());
|
||||
</script>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
},
|
||||
|
||||
'about' => 'Lightweight sortable HTML table with remembered sort state and human-readable data formatting',
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php return [
|
||||
'render' => function($prop) {
|
||||
$items = is_array($prop['items'] ?? null) ? $prop['items'] : [];
|
||||
$title = (string)($prop['title'] ?? '');
|
||||
$subtitle = (string)($prop['subtitle'] ?? '');
|
||||
ob_start();
|
||||
?>
|
||||
<div class="dashboard-panel">
|
||||
<?php if ($title !== '' || $subtitle !== ''): ?>
|
||||
<div class="dashboard-panel-header">
|
||||
<?php if ($title !== ''): ?><h2><?= safe($title) ?></h2><?php endif; ?>
|
||||
<?php if ($subtitle !== ''): ?><p><?= safe($subtitle) ?></p><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="dashboard-stat-grid">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<?php
|
||||
$tone = preg_replace('/[^a-z0-9_-]+/i', '', (string)($item['tone'] ?? 'info'));
|
||||
$tag = !empty($item['href']) ? 'a' : 'div';
|
||||
$href = !empty($item['href']) ? ' href="' . asafe((string)$item['href']) . '"' : '';
|
||||
?>
|
||||
<<?= $tag ?> class="dashboard-stat-card tone-<?= asafe($tone) ?>"<?= $href ?>>
|
||||
<div class="dashboard-stat-label"><?= safe((string)($item['label'] ?? 'Metric')) ?></div>
|
||||
<div class="dashboard-stat-value"><?= safe((string)($item['value'] ?? '--')) ?></div>
|
||||
<?php if (!empty($item['meta'])): ?>
|
||||
<div class="dashboard-stat-meta"><?= safe((string)$item['meta']) ?></div>
|
||||
<?php endif; ?>
|
||||
</<?= $tag ?>>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
},
|
||||
|
||||
'about' => 'Responsive metric cards extracted from dashboard-style pages and adapted for generic starter overviews',
|
||||
];
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
<?php return [
|
||||
'render' => function($prop) {
|
||||
// Generate unique ID for this table instance
|
||||
$tableId = $prop['id'] ?? 'data-grid-' . uniqid();
|
||||
$data = $prop['items'] ?? [];
|
||||
$columns = $prop['columns'] ?? null;
|
||||
$options = $prop['options'] ?? [];
|
||||
|
||||
// Auto-generate columns if not provided
|
||||
if (!$columns && !empty($data)) {
|
||||
$columns = [];
|
||||
$firstItem = reset($data);
|
||||
if (is_array($firstItem)) {
|
||||
foreach (array_keys($firstItem) as $key) {
|
||||
$columns[] = [
|
||||
'field' => $key,
|
||||
'headerName' => ucfirst(str_replace('_', ' ', $key)),
|
||||
'sortable' => true,
|
||||
'filter' => true,
|
||||
'resizable' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default grid options (Community edition compatible)
|
||||
$defaultOptions = [
|
||||
'pagination' => true,
|
||||
'paginationPageSize' => 20,
|
||||
'suppressMenuHide' => true,
|
||||
'animateRows' => true,
|
||||
'defaultColDef' => [
|
||||
'sortable' => true,
|
||||
'filter' => true,
|
||||
'resizable' => true,
|
||||
'minWidth' => 100
|
||||
]
|
||||
];
|
||||
|
||||
$gridOptions = array_merge($defaultOptions, $options);
|
||||
$gridOptions['columnDefs'] = $columns;
|
||||
$gridOptions['rowData'] = $data;
|
||||
|
||||
?>
|
||||
<!-- ag-Grid Data Table Component -->
|
||||
<div class="ag-grid-container" style="margin: 1rem 0;">
|
||||
<div class="ag-grid-toolbar" style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--surface-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
border-bottom: none;
|
||||
">
|
||||
<div style="display: flex; gap: 1rem; align-items: center; flex: 1;">
|
||||
<span style="color: var(--text-secondary); font-size: 0.875rem; font-weight: 500;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 0.25rem;">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="9" y1="9" x2="15" y2="9"/>
|
||||
<line x1="9" y1="15" x2="15" y2="15"/>
|
||||
</svg>
|
||||
<?= count($data) ?> rows
|
||||
</span>
|
||||
|
||||
<!-- Search Input -->
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)" stroke-width="2" style="position: absolute; left: 0.75rem; z-index: 1;">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
id="<?= $tableId ?>-search"
|
||||
placeholder="Search all columns..."
|
||||
style="
|
||||
padding: 0.5rem 0.75rem 0.5rem 2.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
width: 250px;
|
||||
transition: all 0.2s ease;
|
||||
"
|
||||
onFocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 2px var(--primary-light)';"
|
||||
onBlur="this.style.borderColor='var(--border)'; this.style.boxShadow='none';"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button id="<?= $tableId ?>-export-csv" style="
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
"
|
||||
onmouseover="this.style.background='var(--primary)'; this.style.color='white'; this.style.borderColor='var(--primary)';"
|
||||
onmouseout="this.style.background='var(--surface)'; this.style.color='var(--text-secondary)'; this.style.borderColor='var(--border)';">
|
||||
📊 Export CSV
|
||||
</button>
|
||||
<button id="<?= $tableId ?>-clear-filters" style="
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
"
|
||||
onmouseover="this.style.background='var(--surface-elevated)'; this.style.borderColor='var(--primary)';"
|
||||
onmouseout="this.style.background='var(--surface)'; this.style.borderColor='var(--border)';">
|
||||
🔄 Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="<?= $tableId ?>" class="ag-theme-alpine-dark" style="
|
||||
height: <?= $prop['height'] ?? '400px' ?>;
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
overflow: hidden;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- ag-Grid Theme Override Styles -->
|
||||
<style>
|
||||
.ag-theme-alpine-dark {
|
||||
--ag-background-color: var(--surface);
|
||||
--ag-foreground-color: var(--text-primary);
|
||||
--ag-border-color: var(--border);
|
||||
--ag-secondary-border-color: var(--border);
|
||||
--ag-header-background-color: var(--surface-elevated);
|
||||
--ag-header-foreground-color: var(--text-primary);
|
||||
--ag-odd-row-background-color: var(--surface);
|
||||
--ag-even-row-background-color: var(--surface-elevated);
|
||||
--ag-row-hover-color: var(--surface-hover);
|
||||
--ag-selected-row-background-color: var(--primary-light);
|
||||
--ag-range-selection-background-color: var(--primary-light);
|
||||
--ag-range-selection-border-color: var(--primary);
|
||||
--ag-input-focus-border-color: var(--primary);
|
||||
--ag-minier-foreground-color: var(--text-secondary);
|
||||
--ag-subtle-text-color: var(--text-secondary);
|
||||
--ag-disabled-foreground-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark .ag-header-cell-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark .ag-cell {
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark .ag-header-cell {
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark .ag-paging-panel {
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--surface-elevated);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.ag-grid-toolbar {
|
||||
flex-direction: column !important;
|
||||
gap: 0.75rem !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.ag-grid-toolbar > div {
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
if (typeof agGrid === 'undefined') {
|
||||
const agGridCSS = document.createElement('link');
|
||||
agGridCSS.rel = 'stylesheet';
|
||||
agGridCSS.href = 'js/ag-grid/ag-grid.css';
|
||||
document.head.appendChild(agGridCSS);
|
||||
|
||||
const agGridThemeCSS = document.createElement('link');
|
||||
agGridThemeCSS.rel = 'stylesheet';
|
||||
agGridThemeCSS.href = 'js/ag-grid/ag-theme-alpine.css';
|
||||
document.head.appendChild(agGridThemeCSS);
|
||||
|
||||
const agGridScript = document.createElement('script');
|
||||
agGridScript.src = 'js/ag-grid/ag-grid-community.min.js';
|
||||
agGridScript.onload = function() {
|
||||
initializeGrid_<?= $tableId ?>();
|
||||
};
|
||||
document.head.appendChild(agGridScript);
|
||||
} else {
|
||||
initializeGrid_<?= $tableId ?>();
|
||||
}
|
||||
|
||||
function initializeGrid_<?= $tableId ?>() {
|
||||
const gridOptions = <?= json_encode($gridOptions, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||
|
||||
// Enhanced column definitions with better formatting
|
||||
gridOptions.columnDefs = gridOptions.columnDefs.map(col => {
|
||||
// Add value formatters based on data type
|
||||
if (col.field && gridOptions.rowData.length > 0) {
|
||||
const sampleValue = gridOptions.rowData[0][col.field];
|
||||
|
||||
if (typeof sampleValue === 'number') {
|
||||
col.type = 'numericColumn';
|
||||
col.cellClass = 'ag-right-aligned-cell';
|
||||
col.valueFormatter = params => {
|
||||
if (params.value != null) {
|
||||
return new Intl.NumberFormat().format(params.value);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
} else if (typeof sampleValue === 'string' && !isNaN(Date.parse(sampleValue)) && sampleValue.includes('-')) {
|
||||
col.valueFormatter = params => {
|
||||
if (params.value) {
|
||||
const date = new Date(params.value);
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
} else if (typeof sampleValue === 'boolean') {
|
||||
col.cellRenderer = params => {
|
||||
return params.value ?
|
||||
'<span style="color: #10b981;">✓ Yes</span>' :
|
||||
'<span style="color: #ef4444;">✗ No</span>';
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return col;
|
||||
});
|
||||
|
||||
// Initialize the grid
|
||||
const gridDiv = document.querySelector('#<?= $tableId ?>');
|
||||
if (gridDiv && window.agGrid) {
|
||||
const gridApi = agGrid.createGrid(gridDiv, gridOptions);
|
||||
|
||||
// Toolbar button handlers
|
||||
document.getElementById('<?= $tableId ?>-export-csv')?.addEventListener('click', () => {
|
||||
gridApi.exportDataAsCsv({
|
||||
fileName: 'data-export-' + new Date().toISOString().split('T')[0] + '.csv'
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('<?= $tableId ?>-clear-filters')?.addEventListener('click', () => {
|
||||
gridApi.setFilterModel(null);
|
||||
if (gridApi.setQuickFilter) {
|
||||
gridApi.setQuickFilter('');
|
||||
}
|
||||
// Clear search input
|
||||
const searchInput = document.getElementById('<?= $tableId ?>-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-size columns on first data render
|
||||
gridApi.addEventListener('firstDataRendered', () => {
|
||||
gridApi.autoSizeAllColumns();
|
||||
});
|
||||
|
||||
// Connect search input to quick filter
|
||||
const searchInput = document.getElementById('<?= $tableId ?>-search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const filterText = e.target.value;
|
||||
if (gridApi.setQuickFilter) {
|
||||
gridApi.setQuickFilter(filterText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Store grid API for external access
|
||||
window['gridApi_<?= $tableId ?>'] = gridApi;
|
||||
|
||||
console.log('ag-Grid initialized for <?= $tableId ?> with', gridOptions.rowData.length, 'rows');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
},
|
||||
|
||||
'about' => 'A powerful data grid component powered by ag-Grid with sorting, filtering, pagination, and export capabilities'
|
||||
];
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php return [
|
||||
'render' => function($prop) {
|
||||
include_js('js/u-format.js');
|
||||
include_js('js/u-timeseries-chart.js');
|
||||
|
||||
$chartId = (string)($prop['id'] ?? ('ts-chart-' . uniqid()));
|
||||
$canvasId = $chartId . '-canvas';
|
||||
$title = (string)($prop['title'] ?? '');
|
||||
$subtitle = (string)($prop['subtitle'] ?? '');
|
||||
$height = max(180, (int)($prop['height'] ?? 320));
|
||||
$series = array_values(is_array($prop['series'] ?? null) ? $prop['series'] : []);
|
||||
$xLabels = array_values(is_array($prop['x_labels'] ?? null) ? $prop['x_labels'] : []);
|
||||
$jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
|
||||
ob_start();
|
||||
?>
|
||||
<div class="dashboard-panel">
|
||||
<?php if ($title !== '' || $subtitle !== ''): ?>
|
||||
<div class="dashboard-panel-header">
|
||||
<?php if ($title !== ''): ?><h2><?= safe($title) ?></h2><?php endif; ?>
|
||||
<?php if ($subtitle !== ''): ?><p><?= safe($subtitle) ?></p><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<canvas id="<?= asafe($canvasId) ?>" class="dashboard-chart-canvas" style="height: <?= $height ?>px"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
if (typeof UTimeSeriesChart === 'undefined') return;
|
||||
var chart = new UTimeSeriesChart(<?= jsafe($canvasId) ?>, {
|
||||
xAxisLabel: <?= jsafe((string)($prop['x_axis_label'] ?? 'Time')) ?>,
|
||||
yAxisLeftLabel: <?= jsafe((string)($prop['y_axis_left_label'] ?? 'Value')) ?>,
|
||||
yAxisRightLabel: <?= jsafe((string)($prop['y_axis_right_label'] ?? '')) ?>,
|
||||
yAxisLeftFormat: <?= jsafe((string)($prop['y_axis_left_format'] ?? 'number')) ?>,
|
||||
yAxisRightFormat: <?= jsafe((string)($prop['y_axis_right_format'] ?? 'number')) ?>
|
||||
});
|
||||
chart.setData(<?= json_encode($series, $jsonFlags) ?>, <?= json_encode($xLabels, $jsonFlags) ?>);
|
||||
window[<?= jsafe('chart_' . $chartId) ?>] = chart;
|
||||
}());
|
||||
</script>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
},
|
||||
|
||||
'about' => 'Canvas-based multi-series time-series chart component backported from a production dashboard',
|
||||
];
|
||||
Reference in New Issue
Block a user