yf_charts_plugin_chartjs class

Chart.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.

Fileplugins/charts/classes/charts/yf_charts_plugin_chartjs.class.php
Use_class('charts_plugin_chartjs', 'classes/charts/')

Public properties

A site sets them in its config with $PROJECT_CONF['charts_plugin_chartjs']['NAME'] = ...;

PropertyDefaultDescription
$HEIGHT
int
300 default height in px of the chart box (Chart.js fills its parent)
$PALETTE
array
['#0d6efd', '#198754', '#dc3545', '#fd7e14', '#6f42c1', '#20c997', ... (10 items)]
array (
  0 => '#0d6efd',
  1 => '#198754',
  2 => '#dc3545',
  3 => '#fd7e14',
  4 => '#6f42c1',
  5 => '#20c997',
  6 => '#ffc107',
  7 => '#6c757d',
  8 => '#0dcaf0',
  9 => '#d63384',
)
series colours, Bootstrap 5 palette
$TYPES
array
['line', 'bar', 'pie', 'doughnut', 'radar', 'polarArea'] chart types; any other falls back to bar
$_ids
int
0

Public methods 6

config

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];
    }

render

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;
    }

Methods starting with _ hooks and helpers; not routable as page actions

_canvas

public 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>'];
    }

_hex6

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;
    }

_init_js

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(); })();';
    }

_is_color

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);
    }