Cannot figure out why my Vega plot and JS Hook do not work

I was trying to add timeline graphs to my current application.

The application uses Surface UI 0.6.1 and LiveView 0.16.x.

Throughout the application I made use of JS Hooks every now and then, e.g. to draw or update markers on a Leaflet Map and all worked very well so far.

Now I am running into 2 problems:

  1. I cannot figure out how to add vega lite to my phoenix project. I followed the Third-party JS packages
    section from GitHub - phoenixframework/esbuild: An installer for esbuild, as in running npm install in the assets directory. However, I cannot see any files being added anywhere, but the compiler is also not complaining on import vegaEmbed from 'vega-embed' . Where are the packages located? :thinking:
  2. It seems like my Hooks do not work, but I do not see a difference to the Hooks I used before. Here is the code, the console.log statement is not showing in the console output in Chrome (always using “empty cache and hard reload” when reloading the page):

JS

import vegaEmbed from 'vega-embed' 

const XyDiagram = {
  mounted() {
    this.handleEvent("draw", ({spec}) => {
      console.log("XY Diagram Hook")
      vegaEmbed(this.el, spec)
        .then((result) => result.view)
        .catch((error) => console.error(error))
    })
  },
}

export {XyDiagram}

EX

defmodule Components.XyDiagram do
  use Surface.LiveComponent

  require Logger

  prop data, :list, required: true
  prop height, :decimal, default: 200
  prop value_key, :atom, required: true

  @impl true
  def update(assigns, socket) do
    data =
      Enum.map(assigns.data, & %{x: &1.timestamp, y: &1[assigns.value_key]})

    spec =
      VegaLite.new(title: "Title", width: :container, height: :container, padding: 5)
      |> VegaLite.data_from_values(data)
      |> VegaLite.mark(:line)
      |> VegaLite.encode_field(:x, "x", type: :temporal)
      |> VegaLite.encode_field(:y, "y", type: :quantitative)
      |> VegaLite.to_spec()
      |> IO.inspect() # print looks good here :)

    {:ok, push_event(socket, "draw", %{"spec" => spec})}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div style={"width:100%; height: 300px"} id="graph" :hook="XyDiagram" phx-update="ignore" />
    """
  end
end

I am a bit lost and do not know what else to try or search for.

Can you spot what I am doing wrong here?

This is how I have mine set up … if it helps you.

assets/vendor/vega_lite.js

/* import vegaEmbed from "vega-embed"; */

/**
 * A hook used to render graphics according to the given
 * Vega-Lite specification.
 *
 * The hook expects a `vega_lite:<id>:init` event with `{ spec }` payload,
 * where `spec` is the graphic definition as an object.
 *
 * Configuration:
 *
 *   * `data-id` - plot id
 * 
 * 
 * OPTIONS : FULL LIST @ https://github.com/vega/vega-embed#options
 * Eg,
 * actions:	Boolean / Object	
 * Determines if action links ("Export as PNG/SVG", "View Source", "View Vega" 
 * (only for Vega-Lite), "Open in Vega Editor") are included with the embedded view. If the value is true, 
 * all action links will be shown and none if the value is false. This property can take a key-value mapping 
 * object that maps keys (export, source, compiled, editor) to boolean values for determining if each action 
 * link should be shown. By default, export, source, and editor are true and compiled is false. These defaults 
 * can be overridden: for example, if actions is {export: false, source: true}, the embedded visualization 
 * will have two links – "View Source" and "Open in Vega Editor". The export property can take a key-value 
 * mapping object that maps keys (svg, png) to boolean values for determining if each export action 
 * link should be shown. By default, svg and png are true.
 */


const VegaLite = {
  mounted() {
    this.id = this.el.getAttribute("data-id");
    this.viewPromise = null;

    const container = document.createElement("div");
    this.el.appendChild(container);

    this.handleEvent(`vega_lite:${this.id}:init`, ({ spec }) => {
      this.viewPromise = vegaEmbed(container, spec, {"actions": false})
        .then((result) => result.view)
        .catch((error) => {
          console.error(
            `Failed to render the given Vega-Lite specification, got the following error:\n\n    ${error.message}\n\nMake sure to check for typos.`
          );
        });
    });
  },

  destroyed() {
    if (this.viewPromise) {
      this.viewPromise.then((view) => view.finalize());
    }
  },
};

export default VegaLite;

assets/js/app.js

import VegaLite from '../vendor/vega_lite';

...

Hooks.VegaLite = VegaLite;

mix.exs

      {:vega_lite, "~> 0.1.2"},
1 Like

Thanks for the snippet!

My mistake was actually very simple …
I accidentally used ~H instead of ~F as required for Semantic UI for the rendering block.