Using Plot Externally, Unable to Add Legend

I’m using the Plot externally, meaning not on Observable, and I’m having an issue where if I try and include the legend. I’m getting the error message “Function.prototype.apply was called on undefined, which is a undefined and not a function”. I also noticed this happened when I tried to use the caption option.

I’m using a framework similar to React or Vue, the LWC framework from Salesforce, and so I’m injecting Plot into the DOM with appendChild. Could this be some kind of issue with “this” being out of scope? Any ideas?

Can you share an example that reproduces the issue? Otherwise it’s hard to investigate.

This code might be a bit abstract since it’s based on the external framework. Just to be clear, everything else works perfect, just when I try and include the legend or caption I get the error.

Here’s the code for the plot:

createOppPlot(currentGoal) {
    return Plot.plot({
      marginLeft: 100,
      marginRight: 50,
      marginBottom: 50,
      height: 100,
      color: {
        scheme: 'bugn',
        legend: true,
      },
      y: {
        label: null,
        grid: true,
        axis: null,
      },
      x: {
        domain: [0, d3.max([currentGoal], d => d.OpportunityGoal)],
        label: `${d3.format('$,.2f')(currentGoal.currentOppTotalValue)} / ${d3.format('$,.0f')(
          currentGoal.OpportunityGoal
        )}`,
        tickFormat: '$,.0f',
        ticks: 4,
        labelAnchor: 'center',
        labelOffset: 36,
        clamp: true,
      },
      marks: [
        Plot.barX(
          currentGoal.Gifts,
          Plot.stackX(
            Plot.groupZ(
              { x: 'sum' },
              {
                x: 'amount',
                fill: 'type',
                title: d => `${d.type} - ${d3.format('$,.2f')(d.amount)}`,
              }
            )
          )
        ),
        Plot.ruleX([0]),
        Plot.text([currentGoal], {
          x: 0,
          textAnchor: 'start',
          dx: +4,
          text: d => `${d3.format('.0%')(d.currentOppTotalValue / d.OpportunityGoal)}`,
          y: 'rm',
          fill: 'black',
        }),
        Plot.frame(),
      ],
    });
  }

I’m passing in a pretty simple data structure that looks like this:

[
  {
    "rm": "Josh Kenzer",
    "Gifts": [
      {
        "type": "Owner + Co-Owner Value",
        "amount": 2025000
      },
      {
        "type": "Stewarded",
        "amount": 106529.46
      },
      {
        "type": "Team Value",
        "amount": 799424.39
      }
    ],
    "PlannedGifts": [
      {
        "type": "Owner + Co-Owner Count",
        "amount": 0
      },
      {
        "type": "Team Count",
        "amount": 0
      }
    ],
    "currentOppTotalValue": 2930953.85,
    "OpportunityGoal": 12000000,
    "PlannedGiftGoal": 7,
    "currentPlannedGiftTotal": 0
  }
]

And the output I get when I don’t pass legend: true:

SCR-20220617-n0f

When I pass legend: true, then I get this error:

Uncaught (in promise) TypeError: Function.prototype.apply was called on undefined, which is a undefined and not a function
at Object.br [as plot] (plot:2:56162)
at U.createOppPlot (displayDevelopmentGoals.js:1:5709)
at U.renderPlot (displayDevelopmentGoals.js:1:5428)
at eval (displayDevelopmentGoals.js:1:4625)

Here are the relevant parts of the web component using the lwc.dev framework (the link is to an off-Salesforce implementation by Salesforce). I am doing this on platform.

renderPlot() {
    this.renderedPlot = true;
    console.log(Plot.version);

    // LWC syntax for selecting dom element
    let thePlots = this.template.querySelector('div.plot');

    // clear out dom elements in case the user toggles the fiscal year to view
    while (thePlots.firstChild) {
      thePlots.removeChild(thePlots.firstChild);
    }

   // sort the results by relationship manager last name
    this.currentGoalValues = this.currentGoalValues.sort((a, b) => {
      return d3.ascending(a.rm.split(' ')[1], b.rm.split(' ')[1]);
    });

   // loop through each relationship manager creating a separate horizontal bar chart for each relationship manager
    for (let index = 0; index < this.currentGoalValues.length; index++) {
      let currentGoal = this.currentGoalValues[index];

      // create containing row
      let rowEle = document.createElement('div');
      rowEle.classList.add('slds-grid', 'slds-gutters', 'thePlots', 'slds-m-bottom_x-large');

      // display the rm name
      let rmEle = document.createElement('div');
      rmEle.classList.add('slds-col', 'slds-size_2-of-12', 'slds-align-middle');
      let rmHeaderEle = document.createElement('div');
      rmHeaderEle.classList.add('slds-text-heading_small');
      rmHeaderEle.innerHTML = currentGoal.rm;
      rmEle.appendChild(rmHeaderEle);
      rowEle.appendChild(rmEle);

      // add the plot
      let elePlotGoal = document.createElement('div');
      elePlotGoal.classList.add('slds-col', 'slds-size_10-of-12');
      let plot = this.createOppPlot(currentGoal);
      elePlotGoal.appendChild(plot);
      rowEle.appendChild(elePlotGoal);

      // append to the dom
      this.template.querySelector('div.plot').appendChild(rowEle);
    }
  }

I know this is complex out of context but if you have any thoughts, that would great. As an alternative, could I create the legend separately and insert that into the DOM? How would I grab the color scale into some structure to work with separately?

Is the crash happening inside of Plot here, or is happening in your own code? It’s hard to tell from the stack trace.

I just ran in Firefox (as opposed to Chrome) and got a slightly different error:

Uncaught (in promise) TypeError: N.append is not a function

I traced before and after calling plot and the error is happening in the Plot.plot() call.

Is the framework you are using supplying the DOM implementation (e.g., for server-side rendering)? Element.append is a relatively new alternative to Element.appendChild, so perhaps the DOM implementation you are using does not support it.

Yeah, that appears to be it. I commented out all the Plot code and just did a very simple .append() example and it failed with the same error.

I guess I’m going to have to construct my legend manually. I guess I can define my color range in Plot and then just add my own elements with the same colors in the dom.

Thanks for your help @mbostock

It would be easy for us to switch back to using the older appendChild method if you want to contribute a PR.