yf_charts_plugin_chartjs classChart.js driver of the charts plugin: _class('charts')->chartjs($type, $data, $extra). The data goes to the page as JSON (never as JavaScript code), so labels and values from a database cannot inject script.
| File | plugins/charts/classes/charts/yf_charts_plugin_chartjs.class.php |
|---|---|
| Use | _class('charts_plugin_chartjs', 'classes/charts/') |
A site sets them in its config with $PROJECT_CONF['charts_plugin_chartjs']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$HEIGHTint |
300 |
default height in px of the chart box (Chart.js fills its parent) |
$PALETTEarray |
|
series colours, Bootstrap 5 palette |
$TYPESarray |
['line', 'bar', 'pie', 'doughnut', 'radar', 'polarArea'] |
chart types; any other falls back to bar |
$_idsint |
0 |
public function config($type, array $data, array $extra = []): array
Chart.js configuration for the data.
$type string one of $TYPES$data array one series ['label' => value, ...] or several ['Series name' => ['label' => value, ...], ...]$extra array title, stacked (bar / line), horizontal (bar), fill (line), colors (list of #rgb / #rrggbb), options (merged into the Chart.js options) public function config($type, array $data, array $extra = []): array
{
$type = in_array($type, $this->TYPES, true) ? $type : 'bar';
$multi = $data && count(array_filter($data, 'is_array')) === count($data);
$series = $multi ? $data : ['' => $data];
$labels = [];
foreach ($series as $points) {
foreach (array_keys((array) $points) as $label) {
$labels[(string) $label] = true;
}
}
$labels = array_map('strval', array_keys($labels));
$colors = array_values(array_filter((array) ($extra['colors'] ?? []), [$this, '_is_color'])) ?: $this->PALETTE;
$per_point = in_array($type, ['pie', 'doughnut', 'polarArea'], true);
$datasets = [];
$i = 0;
foreach ($series as $name => $points) {
$values = [];
foreach ($labels as $label) {
$v = $points[$label] ?? null;
$values[] = is_numeric($v) && is_finite($v + 0) ? $v + 0 : null;
}
$color = $colors[$i % count($colors)];
$set = ['label' => (string) $name, 'data' => $values];
if ($per_point) {
$set['backgroundColor'] = array_map(function ($n) use ($colors) {
return $colors[$n % count($colors)];
}, array_keys($values));
} else {
$set['borderColor'] = $color;
$set['backgroundColor'] = $type === 'bar' ? $color : $this->_hex6($color) . '33';
if ($type === 'line') {
$set['fill'] = (bool) ($extra['fill'] ?? false);
$set['tension'] = 0.25;
}
}
$datasets[] = $set;
$i++;
}
$options = [
'responsive' => true,
'maintainAspectRatio' => false,
'plugins' => ['legend' => ['display' => $multi || $per_point]],
];
if (strlen((string) ($extra['title'] ?? ''))) {
$options['plugins']['title'] = ['display' => true, 'text' => (string) $extra['title']];
}
if ($type === 'bar' && ! empty($extra['horizontal'])) {
$options['indexAxis'] = 'y';
}
if (in_array($type, ['bar', 'line'], true) && ! empty($extra['stacked'])) {
$options['scales'] = ['x' => ['stacked' => true], 'y' => ['stacked' => true]];
}
if (is_array($extra['options'] ?? null)) {
$options = array_replace_recursive($options, $extra['options']);
}
return ['type' => $type, 'data' => ['labels' => $labels, 'datasets' => $datasets], 'options' => $options];
}
public function render($type, array $data, array $extra = []): string
The canvas, with the Chart.js asset and its init script added to the page.
$type string$data array see config()$extra array see config(); also id ([a-z0-9_-]) and height (px number or a css length) public function render($type, array $data, array $extra = []): string
{
[$id, $html] = $this->_canvas($extra);
asset('chartjs');
js($this->_init_js($id, $this->config($type, $data, $extra)));
return $html;
}
_ hooks and helpers; not routable as page actionspublic function _canvas(array $extra = []): array
[id, html] of the chart box.
$extra array id, height, title public function _canvas(array $extra = []): array
{
$id = preg_replace('~[^a-z0-9_-]~i', '', (string) ($extra['id'] ?? '')) ?: 'yf_chartjs_' . ++$this->_ids;
$height = $extra['height'] ?? $this->HEIGHT;
$height = is_numeric($height) ? (int) $height . 'px' : (preg_match('~^\d{1,4}(\.\d+)?(px|em|rem|vh|%)$~D', (string) $height) ? $height : $this->HEIGHT . 'px');
$label = strlen((string) ($extra['title'] ?? '')) ? (string) $extra['title'] : 'chart';
return [$id, '<div class="yf-chartjs" style="position:relative; height:' . $height . '"><canvas id="' . $id . '" role="img" aria-label="'
. htmlspecialchars($label, ENT_QUOTES) . '"></canvas></div>'];
}
public function _hex6($color): string
#rgb as #rrggbb, so an alpha suffix can be appended.
$color string public function _hex6($color): string
{
return preg_match('~^#([0-9a-f])([0-9a-f])([0-9a-f])$~i', $color, $m) ? '#' . $m[1] . $m[1] . $m[2] . $m[2] . $m[3] . $m[3] : $color;
}
public function _init_js($id, array $config): string
Init script: waits for the DOM, does nothing when Chart.js did not load.
$id string$config array public function _init_js($id, array $config): string
{
$flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
return '(function () { var f = function () { var el = document.getElementById(' . json_encode((string) $id, $flags) . ');'
. ' if (window.Chart && el) { new Chart(el, ' . json_encode($config, $flags) . '); } };'
. ' document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", f) : f(); })();';
}
public function _is_color($color): bool
public function _is_color($color): bool
{
return is_string($color) && (bool) preg_match('~^#(?:[0-9a-f]{3}|[0-9a-f]{6})$~iD', $color);
}