d3fc icon indicating copy to clipboard operation
d3fc copied to clipboard

`chartCartesian` "draw" handler created in `decorate` is called with stale data

Open EndilWayfare opened this issue 4 years ago • 2 comments

const legend = /* some component */
const chart = fc
  .chartCartesian(d3.scaleLinear(), d3.scaleLinear())
  .decorate((selection) => {
    selection
      .enter()
      .append("d3fc-svg")
      .style("grid-column", 3)
      .style("grid-row", 5)
      .style("height", "2em")
      .on("measure.legend", (event) => {
        legend.width(event.detail.width);
      })
      .on("draw.legend", (event, data) => {
        // `data` is always equal to `firstData`
        d3.select(event.currentTarget).select("svg").datum(data.criteria).call(legend);
      });
  });

const render = (el, data) => {
  d3.select("#chart").datum(data).transition().call(chart);
}

const firstData = /* some data */
render("#chart", firstData)

const nextData = /* some different data */
render("#chart", nextData)

Hacky workaround:

let localData = null;

const legend = /* some component */
const chart = fc
  .chartCartesian(d3.scaleLinear(), d3.scaleLinear())
  .decorate((selection) => {
    selection
      .enter()
      .append("d3fc-svg")
      .style("grid-column", 3)
      .style("grid-row", 5)
      .style("height", "2em")
      .on("measure.legend", (event) => {
        legend.width(event.detail.width);
      })
      .on("draw.legend", (event) => {
        const data = localData;
        d3.select(event.currentTarget).select("svg").datum(data.criteria).call(legend);
      });
  });

const render = (el, data) => {
  localData = data
  d3.select("#chart").datum(data).transition().call(chart);
}

const firstData = /* some data */
render("#chart", firstData)

const nextData = /* some different data */
render("#chart", nextData)

EndilWayfare avatar Jan 21 '21 22:01 EndilWayfare

The problem here is that D3 doesn't automatically propagate the joined data from the parent element. I you look at the sourcecode for d3fc chart, you can see that we propagate data for each of the child element - notice that the plot area elements receive the data joined with the chart, whereas the axes are joined to their orientation:

https://github.com/d3fc/d3fc/blob/master/packages/d3fc-chart/src/cartesian.js#L55

You can used exactly this pattern within your decorate function to provide the data you need:

 
const legend = /* some component */

const legendDataJoin = fc.dataJoin("d3fc-svg", "legend");


const chart = fc
  .chartCartesian(d3.scaleLinear(), d3.scaleLinear())
  .decorate((selection, data) => {
    legendDataJoin(selection, [data])
      .style("grid-column", 3)
      .style("grid-row", 5)
      .style("height", "2em")
      .on("measure.legend", (event) => {
        legend.width(event.detail.width);
      })
      .on("draw.legend", (event, data) => {
        d3.select(event.currentTarget).select("svg").datum(data.criteria).call(legend);
      });
  });

This will ensure that the correct data is propagated to the draw event

ColinEberhardt avatar Jan 22 '21 09:01 ColinEberhardt

Ah, ok. I think I understand. I suppose I misunderstood how the events are dispatched.

Related: The container passed to decorate is not "transition propagated" https://github.com/d3fc/d3fc/blob/b294f0a6f326b9eb001498e6760c0f0c8772620f/packages/d3fc-chart/src/cartesian.js#L168 vs https://github.com/d3fc/d3fc/blob/b294f0a6f326b9eb001498e6760c0f0c8772620f/packages/d3fc-chart/src/cartesian.js#L118-L126

There seems to be no way to access the original selection to propagateTransition after the fact inside decorate, so I'm using a similar localSelection trick...

const propagateTransition = (maybeTransition) => (selection) => {
  if (isTransition(maybeTransition)) {
    try {
      selection = selection.transition(maybeTransition);
    } finally {
    }
  }

  return selection;
};

const legend = annotationLegend().key((d) => d.name);
const legendDataJoin = fc.dataJoin("d3fc-svg", "legend");

const chart = fc
  .chartCartesian(d3.scaleLinear(), d3.scaleLinear())
  .svgPlotArea(createArea())
  .decorate((selection, data) => {
    legendDataJoin(selection, [data])
      .style("grid-column", 3)
      .style("grid-row", 5)
      .on("measure.legend", (event) => {
        legend.width(event.detail.width);
      })
      .on("draw.legend", (event, data) => {
        propagateTransition(localSelection)(
          d3.select(event.currentTarget).select("svg").datum(data.criteria)
        ).call(legend);
      });
  });

let localSelection = null;

export const render = (el, data) => {
   //...
  localSelection = d3.select(el)
    .datum(data)
    .transition()
    .duration(500)
    .on("end", () => {
      chart.svgPlotArea(createArea());

      // TODO: I have no idea why `propagateTransition` is not catching the "can't find transition"
      //       error, forcing me to re-assign `localSelection`...
      localSelection = d3.select(el).datum(data).call(chart);
    })
    .call(chart);
};

EndilWayfare avatar Jan 22 '21 21:01 EndilWayfare