You have requested a non-existent service "charts.settings" during upgrade from charts 3.2.0 to 5.0.7

Created on 25 July 2023, over 1 year ago

Problem/Motivation

I am getting the following when running config import:

> Unexpected error during import with operation create for core.entity_view_display.my_view_with_chart.default: You have requested a non-existent service "charts.settings"

This happens after upgrading from charts 3.2.0 to 5.0.7. I did run `drush updb`

πŸ› Bug report
Status

Active

Version

5.0

Component

Code

Created by

πŸ‡ΊπŸ‡ΈUnited States loopy1492

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @loopy1492
  • πŸ‡ΊπŸ‡ΈUnited States loopy1492
  • πŸ‡ΊπŸ‡ΈUnited States loopy1492

    So, when I downgrade to 4.x, Charts runs the following:

    > charts 8302 hook_update_n 8302 - Update existing views to use the new chart settings.

    but when I run cex, I'm not seeing any changes to my views config.

  • πŸ‡ΊπŸ‡ΈUnited States loopy1492

    So it appears this module has changed significantly between 3.x and 5.x. We have a custom module which uses a file upload field to get csv data and display it using a field preprocess function and a corresponding template.

    It appears that the new version has this conversion built in. You just upload the file to the field and it generates your data points for you. This is really cool, but we already have hundreds of nodes and there's no way I can go through and re-upload everything.

    I was hoping to use the charts view display and simpy embed the view on the nodes. However, the chart view mode will not accept a file field as a source of data.

    Do the maintainers have any suggestions for migrating between 3.x and 5x with so many nodes or are we stuck re-uploading all the csv files?

  • πŸ‡ΊπŸ‡ΈUnited States andileco

    @loopy1492 - very happy to hear about such extensive use! Definitely willing to assist with a way to ease migration. Would you feel comfortable reaching out directly with some specific examples that could be used for testing?

  • πŸ‡ΊπŸ‡ΈUnited States bluegeek9

    @loopy1492,

    Is this still an outstanding issue, or can the issue be closed?

  • πŸ‡ΊπŸ‡ΈUnited States loopy1492

    We have worked very hard to upgrade this code. It really did require a full re-understanding on how both the module and the newer version of chartjs works. You can't do one without the other. It was still possible to hook into the file object and create a chart from that rather than having to re-upload all of the files and completely change the field we were using, which is nice. To be honest, rather than using the default chart settings from the module, we ended up just hard-coding them either in the new formatter or the javascript. I'll include the code here for others who might be trying to do the same thing.

    This would go in your custom module's `src/Plugin/Field/FieldFormatter/ElevationChartFormatter`. Note that you could get the chart's default settings if you wanted to.

    
    namespace Drupal\trail_module\Plugin\Field\FieldFormatter;
    
    use Drupal\Core\Field\FieldItemListInterface;
    use Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase;
    
    /**
     * File field formatter for the Elevation chart.
     *
     * @FieldFormatter(
     *   id = "elevation_chart_formatter",
     *   label = @Translation("Elevation Chart File Formatter"),
     *   field_types = {
     *     "file"
     *   }
     * )
     */
    class ElevationChartFormatter extends FileFormatterBase {
    
      /**
       * {@inheritdoc}
       */
      public function viewElements(FieldItemListInterface $items, $langcode) {
        $elements = [];
        // Define Chart.js default options for the chart formatting.
        $chart_formatting_options = [
          'label' => '',
          'fill' => TRUE,
          'backgroundColor' => 'rgba(36, 63, 43, 1)',
          'borderColor' => '#000000',
          'borderWidth' => 0,
          'tension' => 0,
          'pointRadius' => 0,
        ];
        // Get the point data for our graph.
        foreach ($items as $delta => $item) {
          if ($file = $item->entity) {
            $data = [];
            $categories = [];
            // Read file contents.
            if ($file_contents = @file_get_contents($file->getFileUri())) {
              // Split into rows.
              $csv_rows = str_getcsv($file_contents, "\n");
              // Loop through the rows.
              foreach ($csv_rows as $index => $row) {
                // Skip the header.
                if ($index > 0) {
                  $row = str_getcsv($row);
                  if (isset($row[1], $row[2])) {
                    // Fill the data array with x,y coordinates.
                    $data[] = [
                      'x' => $row[1],
                      'y' => $row[2],
                    ];
                    $categories[] = $row[1];
                  }
                }
              }
            }
    
            // Create the chart options array. Will be merged on js with other options.
            $chart_options = [
              'plugins' => [
                'legend' => [
                  'display' => FALSE,
                ],
              ],
              'interaction' => [
                'mode' => 'nearest',
                'intersect' => FALSE,
              ],
            ];
    
            // Assign variables for the template which are added to data attributes for use by the js.
            $elements[$delta] = [
              '#id' => 'elevation_chart_' . $file->id(),
              '#theme' => 'elevation_chart_formatter',
              '#library' => 'trail_module/elevation_chart',
              '#categories' => $categories,
              '#seriesData' => $data,
              '#chartFormattingOptions' => $chart_formatting_options,
              '#options' => $chart_options,
            ];
          }
        }
        return $elements;
      }
    
    }
    
    

    Theme hook:

    /**
     * Implements hook_theme().
     */
    function trail_module_theme() {
      return [
        'elevation_chart_formatter' => [
          'template' => 'elevation-chart',
          'variables' => [
            'library' => '',
            'categories' => [],
            'seriesData' => [],
            'options' => [],
            'chartFormattingOptions' => [],
            'id' => '',
          ],
        ],
      ];
    }
    

    trail_module.libraries.yml:

    chartjs:
      js:
        js/trail_module_chartjs.js: {  }
      dependencies:
        - core/drupal
        - core/once
        - charts_chartjs/charts_chartjs
    

    We probably could have used Drupal behaviors' context in the javascript in order to grab variables directly from the php instead of using the data attributes, but we did the latter. Here's the elevation-chart.html.twig template in the module's /templates folder...

    {{ attach_library('trail_module/chartjs') }}
    <div id="elevation_chart_{{ id }}" class="trail-module-elevation-chart" 
         data-categories="{{ categories|json_encode() }}" 
         data-seriesdata="{{ seriesData|json_encode() }}" 
         data-options="{{ options|json_encode() }}"
         data-chartFormattingOptions="{{ chartFormattingOptions|json_encode() }}">
      <canvas id="chart_{{ id }}"></canvas>
    </div>
    

    We couldn't get anything that required the use of a callback to work from the PHP at all. Here is our Javascript altering the chart:

    (function ($, Drupal, once) {
      Drupal.behaviors.elevationChart = {
        attach: function (context, settings) {
          // Loop through each chart
          const elevationCharts = once('elevationChart', '.trail-module-elevation-chart', context);
          elevationCharts.forEach(function (chartDiv) {
            // The chart entities
            var $chartDiv = $(chartDiv);
            var chartCanvas = $chartDiv.find('canvas')[0];
            // Prepare canvas for 2D drawing
            var ctx = chartCanvas.getContext('2d');
    
            // Get the chart configuration and data from data attributes assigned to the chart div
            var categories = $chartDiv.data('categories');
            var seriesData = $chartDiv.data('seriesdata');
            var options = $chartDiv.data('options') || {};
            var formattingOptions = $chartDiv.data('chartformattingoptions') || '{}';
            // Ensure the data are arrays.
            if (!Array.isArray(categories)) {
              categories = [];
            }
            if (!Array.isArray(seriesData)) {
              seriesData = [];
            }
    
            // Prepare chart data and options
            var chartData = {
              labels: categories,
              datasets: [{
                label: formattingOptions.label,
                data: seriesData,
                fill: formattingOptions.fill,
                backgroundColor: formattingOptions.backgroundColor,
                borderColor: formattingOptions.borderColor,
                borderWidth: formattingOptions.borderWidth,
                tension: formattingOptions.tension,
                pointRadius: formattingOptions.pointRadius,
              }]
            };
    
            // Extract x and y values from the dataset
            var xValues = [];
            var yValues = [];
            $.each(seriesData, function () {
              xValues.push(this.x);
              yValues.push(this.y);
            });
    
            // Calculate maxLength and maxElev
            const maxLength = Math.max(...xValues);
            const maxElev = Math.max(...yValues);
    
            // We want the chart to always be at least 500'
            var maxChart = 500;
            if (maxElev > maxChart) {
              maxChart = maxElev;
            }
    
            // Any option with a callback needs to be calculated in javascript instead of php
    
            // Set the scales options and ensure values and ticks are in the format we want to display.
            options.scales = {
              x: {
                type: 'linear',
                position: 'bottom',
                bounds: 'ticks',
                max: maxLength,
                ticks: {
                  callback: function(value, index, values) {
                    // round to tenths of a mile
                    return Math.floor(value * 10) / 10 + ' miles';
                  },
                  // Since distance could be anything, the steps should be based on that
                  // Step size is very interesting. The below number divides the maxLength and retains the 
                  // tenths of a mile. If we wanted it to just show at every tenth of a mile, the
                  // stepSize would be .01 instead.
                  stepSize: Math.floor((maxLength / 10) * 10) / 10,
                }
              },
              y: {
                bounds: 'ticks',
                // the original request was for the chart to be true to the actual elevation, not just show the change.
                min: 0,
                // Give a little room at the top of the chart when above 500'
                max: Math.ceil(maxChart / 100) * 100,
                ticks: {
                  callback: function(value, index, values) {
                    // Round up to the nearest foot.
                    return Math.ceil(value) + ' feet';
                  },
                  // since we are always showing the full elevation, the step size can be set manually
                  stepSize: 50,
                }
              }
            };
    
            // Enables the tooltip and sets options (generic plugins options already exist)
            options.plugins.tooltip = {
              enabled: true,
              yAlign: 'bottom',
              xAlign: 'left',
              caretSize: 0,
              caretPadding: 20,
              callbacks: {
                label: function(tooltipItem) {
                  let xValue = Math.floor(tooltipItem.raw.x * 10) / 10;
                  let yValue = Math.floor(tooltipItem.raw.y);
                  return `Distance: ${xValue} miles, Elevation: ${yValue} feet`;
                },
                title: function() {
                  // Prevents default title
                  return '';
                }
              }
            }
    
            // Initialize the chart
            new Chart(ctx, {
              type: 'line',
              data: chartData,
              options: options
            });
          });
        }
      };
    })(jQuery, Drupal, once);
    
    
  • πŸ‡ΊπŸ‡ΈUnited States andileco

    Thank you, @loopy1492! I really appreciate the effort you took to understand the new structure and to upgrade - this really helps me (and others) focus on the current version and make that the best it can be. Thank you also for contributing the code examples!

Production build 0.71.5 2024