bobbingwide / sb-chart-block

Chart block for Gutenberg
GNU General Public License v3.0
6 stars 0 forks source link

Be able to customize the chart font #21

Closed misaki-web closed 1 year ago

misaki-web commented 2 years ago

The Chart.js documentation gives an example about font customization using the options object when creating a chart instance:

let chart = new Chart(ctx, {
    type: 'line',
    data: data,
    options: {
        plugins: {
            legend: {
                labels: {
                    font: {
                        size: 14
                    }
                }
            }
        }
    }
});

SB Chart Block creates the instance here:

https://github.com/bobbingwide/sb-chart-block/blob/22ebbf178529f30d678e2c1d558a6660f2625386/libs/class-sb-chart-block.php#L524-L527

@bobbingwide There are many ways to add font customization, if you are open to this new feature:

  1. in the Gutenberg block settings
  2. as new shortcode attributes (like labelFontSize="16")
  3. as a more generic way to set any option supported by Chart.js, for example a textarea in the Gutenberg block settings or a new shortcode attribute like moreOptions="...".

The third point could be done like that (brainstorming, so not tested):

Shortcode:

[chartjs ... moreOptions='...json...']data[/chartjs]

Example:

[chartjs ... moreOptions='{"plugins":{"legend":{"labels":{"font":{"size":14}}}}}']data[/chartjs]

PHP:

$this->atts['moreOptions'] = isset( $this->atts['moreOptions'] ) ? json_decode($this->atts['moreOptions'], true) : [];

json_decode returns null if the string can't be decoded, but it can also return any PHP type, so it may be a good idea to ensure we have an array:

$this->atts['moreOptions'] = $this->validate_moreoptions( $this->atts['moreOptions'] );
function validate_moreoptions( $data ) {
    return is_array($data) ? $data : [];
}

Then, we could merge $this->atts['moreOptions'] with the options used to create the chart instance.

Objects could be used instead of arrays. I see that $options is a string, but there's a TODO note about it:

https://github.com/bobbingwide/sb-chart-block/blob/22ebbf178529f30d678e2c1d558a6660f2625386/libs/class-sb-chart-block.php#L415-L423

misaki-web commented 2 years ago

For the record, there's a workaround with the filter do_shortcode_tag. Example:

# Add this code in the `functions.php` theme file.
function chartjs_filter($output, $tag, $atts) {
    if ($tag == 'chartjs') {
        $my_custom_options = 'plugins: {legend: {labels: {font: {size: 24}}}}';
        $output = str_replace('options = {', 'options = {' . $my_custom_options . ',', $output);
    }

    return $output;
}
add_filter('do_shortcode_tag', 'chartjs_filter', 10, 3);
bobbingwide commented 2 years ago

@misaki-web thanks for your contributions. I'm working through them slowly.
Re:

There are many ways to add font customization

  1. in the Gutenberg block settings
  2. as new shortcode attributes (like labelFontSize="16")
  3. as a more generic way to set any option supported by Chart.js, for example a textarea in the Gutenberg block settings or a new shortcode attribute like moreOptions="...".

1.1 Setting typography.fontSize true in block.json enables the font size where you enter the CSV data to be adjusted 1.2 But a range control is needed to allow the legend font size to be changed. 1.3 Is it also necessary to set the font size for the labels on the axes? If so, how is this done?

  1. I'd use labelsFontSize for the attribute/shortcode parameter name

  2. I'm not sure how to implement moreOptions in the block editor.

misaki-web commented 2 years ago

Is it also necessary to set the font size for the labels on the axes? If so, how is this done?

Actually, I think it would be useful to be able to customize the font everywhere. For example, IMO the default Chart.js font size is too small for legends, axis labels and tooltips.

Here's the Chart.js documentation for each one:

The following example customizes the font for all of them (so you can see what's the namespace for each one):

function customize_chart_font($options, $atts, $series) {
    $custom_options = to_array($options);

    $custom_options['plugins']['legend']['labels']['font']['size'] = 16;
    $custom_options['plugins']['legend']['labels']['color'] = '#000000';

    $custom_options['scales']['x']['ticks']['font']['size'] = 14;
    $custom_options['scales']['x']['ticks']['color'] = '#000000';

    $custom_options['scales']['y']['ticks']['font']['size'] = 14;
    $custom_options['scales']['y']['ticks']['color'] = '#000000';

    if (isset($custom_options['scales']['y1'])) {
        $custom_options['scales']['y1']['ticks']['font']['size'] = 14;
        $custom_options['scales']['y1']['ticks']['color'] = '#000000';
    }

    $custom_options['plugins']['tooltip']['titleFont']['size'] = 16;
    $custom_options['plugins']['tooltip']['bodyFont']['size'] = 15;
    $custom_options['plugins']['tooltip']['footerFont']['size'] = 15;

    return json_decode(json_encode($custom_options));
}
add_filter('sb_chart_block_options', 'customize_chart_font', 10, 3);

function to_array($data) {
    $array = [];

    if (is_array($data) || is_object($data)) {
        foreach ($data as $key => $value) {
            $array[$key] = (is_array($value) || is_object($value)) ? to_array($value) : $value;
        }
    } else {
        $array[] = $data;
    }

    return $array;
}

I'm not sure how to implement moreOptions in the block editor.

I think it could be a textarea (bonus: with code highlighting for json), something similar to the style/CSS textarea added by some plugins. Example with Blocks CSS:

Blocks CSS screenshot

bobbingwide commented 1 year ago

PR #23 contained some changes for this issue but not all of them.

bobbingwide commented 1 year ago

I struggled to find the best code for setting the nested properties font size options.

Where the original code to set the font size for the legend labels was

$custom_options['plugins']['legend']['labels']['font']['size'] = 16;

I wanted to be able to achieve the equivalent of

 $options->plugins->legend->labels->font->size = $this->atts['labelsFontSize'];

but the above only works when all the intermediate properties are objects. It produces a Fatal error when attempting to assign a new property to a null property.

Stack overflow searches helped a bit. One way of achieving this is to use json_decode & json_encode to convert an array to an object.

$temp = [];
$temp['legend']['labels']['font']['size'] = $this->atts['labelsFontSize'];
$options->plugins = json_decode( json_encode( $temp));

or, for simple objects like this casting to an object seems to work.

$options->plugins = (object) ['legend' => ['labels' => ['font' => ['size' => $this->atts['labelsFontSize']]]]];

Here I didn't use $temp; the array is created on the fly.

The code's a bit ugly but it seems to work.

I could have also tried the json_decode() method documented in Example 2 in https://www.php.net/manual/en/class.stdclass.php

misaki-web commented 1 year ago

In relation to this topic, see the section Are there hooks available for developers? I added in the README file some time ago:

https://github.com/bobbingwide/sb-chart-block/blob/3e5b787fc5a2c535915bf55fd9d47a884d8b0441/README.md?plain=1#L125-L175

Also, what about a function that would take the value and the object path to be set? Let's say we have the following variables:

$options = new stdClass();
$options->abc = 'def';
$options->plugins = new stdClass();
$options->plugins->ghi = 'jkl';

$labelsFontSize = 14;

Here's another way to update the object:

set_object($options, 'legend->labels->font->size', $labelsFontSize);

function set_object(&$object, $path, $value) {
    if (is_object($object)) {
        if (is_array($path)) {
            $keys = $path;
        } else {
            $keys = explode('->', $path);
        }

        $tmp_object =& $object;

        foreach ($keys as $key) {
            if (!isset($tmp_object->$key)) {
                $tmp_object->$key = new stdClass();
            }

            $tmp_object =& $tmp_object->$key;
        }

        $tmp_object = $value;
        $success = true;
    }

    return;
}

The object is updated as expected:

var_dump($options);
// Output:
/*
object(stdClass)#1 (3) {
  ["abc"]=>
  string(3) "def"
  ["plugins"]=>
  object(stdClass)#2 (1) {
    ["ghi"]=>
    string(3) "jkl"
  }
  ["legend"]=>
  object(stdClass)#3 (1) {
    ["labels"]=>
    object(stdClass)#4 (1) {
      ["font"]=>
      object(stdClass)#5 (1) {
        ["size"]=>
        int(14)
      }
    }
  }
}
*/

The object could also be updated with the path as an array:

set_object($options, ['legend', 'labels', 'font', 'size'], $labelsFontSize);
bobbingwide commented 1 year ago

set_object() is similar to the method I was thinking about/started writing yesterday...setProperty().

Rather than using -> for the separator I would have used . and I would have returned the value to be set. eg

$options->plugins = setProperty( $options, 'legend.labels.font.size', $this->atts['labelsFontSize'] ); 

Also, I'm thinking of extracting the utility functions from the main plugin file into a library file. I've got some non WordPress code that also creates charts. Functions needed:

misaki-web commented 1 year ago

Rather than using -> for the separator I would have used . and I would have returned the value to be set.

Brainstorming: a way to create the object:

$path = 'plugins.legend.labels.font.size';
$value = $this->atts['labelsFontSize'];
$props = explode('.', $path);
while ($prop = array_pop($props)) {
    $value = (object) [$prop => $value];
}

var_dump($value);
/*
object(stdClass)#10 (1) {
  ["plugins"]=>
  object(stdClass)#9 (1) {
    ["legend"]=>
    object(stdClass)#8 (1) {
      ["labels"]=>
      object(stdClass)#7 (1) {
        ["font"]=>
        object(stdClass)#2 (1) {
          ["size"]=>
          int(14)
        }
      }
    }
  }
}
*/
bobbingwide commented 1 year ago

Tests for this issue are part of the tests for #22. https://s.b/oikcom/block_example/chart-block-multiple-y-axes/

bobbingwide commented 1 year ago

Delivered in v1.2.0