The Drupal module Embed Block allows embedding blocks in CKEditor. But I needed a way to both provide a button with dialog and to control what kinds of blocks are embedded.

Controller, Router, Dialog, Plugin

The following controller passes the list of allowed blocks:

<?php
...
  public function getVizBlocks(Request $request) {
    // Get config settings.
    $config = ??

    $block_field_manager = \Drupal::service('block_field.manager');
    $definitions = $block_field_manager->getBlockDefinitions();

    $selected_viz_blocks = [];

    foreach ($definitions as $id => $definition) {
      if ($definition['provider'] === 'mymodule') { // Only blocks provided by our custom module
        if (!empty($config->get('allowed_viz.' . $id))) {
          $selected_viz_blocks[] = $id;
        }
      }
    }

    return new JsonResponse($selected_viz_blocks);
  }

A router like /mymodule/blocks that points to the above controller and method would return a JSON object with the block names. And this route can be called by our CKEditor dialog (shown below) to allow the user to select a block:

(function ($, Drupal, drupalSettings) {
  CKEDITOR.dialog.add('mypluginDialog', function (editor) {

    // Get allowed list.
    let options = [];
    $.ajax({
      type: "GET",
      url: '/mymodule/blocks',
      success: function (result) {
        options = result;
      },
      async: false,
      cache: false
    });

    // Queue select list.
    let items = [];
    $.each(options, function (key, value) {
      items.push(...some array here...);
    });

    return {
      title: 'Add a visualization',
      contents: [
        {
          id: 'tab-basic',
          elements: [
            {
            },
          ]
        }
      ],
      onShow: function () {
      },
      onOk: function () {
      }
    };
  });
})(jQuery, Drupal, drupalSettings);

Finally, we need a plugin that calls this dialog:

(function ($, Drupal, drupalSettings) {
  CKEDITOR.plugins.add('myplugin', {
    requires: 'dialog',
    init: function (editor) {
      editor.addCommand('viz', new CKEDITOR.dialogCommand('mypluginDialog'));
      editor.ui.addButton('viz', {
        label: 'Add viz block',
        command: 'viz',
        icon: this.path + 'images/viz.png'
      });
      CKEDITOR.dialog.add('mypluginDialog', this.path + 'dialogs/myvizplugin.js');
    }
  });
})(jQuery, Drupal, drupalSettings);

Of course, we will need a few other things to complete this, including a settings form to manage allowed blocks, a filter to replace the tokens with a block, the CKEditor plugin definition (that points to the library), and an image for the CKEditor button.