Skip to main content
Version: 26.2.0

Charts Built with Third-Party Libraries

When building custom visualizations using third-party libraries like D3.js within Muze Studio, you need to manually integrate with Muze's export system to enable PNG, PDF, and XLSX downloads. This tutorial shows you how to properly configure your custom charts for seamless exporting.

PNG/PDF Export Integration

When creating charts with libraries like D3.js, you need to notify Muze when rendering (including animations) is complete so exports capture the full visualization.

Key Concepts

  1. Disable Auto-Emit: Set autoEmitRenderCompletedEvent: false to take manual control
  2. Check Print Mode: Use env.isPrintMode to detect export/print operations
  3. Emit Completion Event: Call events.emitRenderCompletedEvent() when rendering is done
Required: Render Completion Event

When using non-Muze visualizations in the Muze Studio environment, you must call events.emitRenderCompletedEvent() after your chart finishes rendering. This notifies the application that rendering is complete and is essential for proper visualisation lifecycle management.

Complete Example

/**
 * Available Columns:
 * "Ship Mode"
 * "Total Discount"
 * "Measure names" // If 'measureValues' is enabled.
 * "Measure values" // If 'measureValues' is enabled.
 * --- END --- 
 */

/**
 * Available Columns:
 * "Ship Mode"
 * "Total Discount"
 */

// Load D3.js library dynamically
const loadD3 = () => {
    return new Promise((resolve, reject) => {
        if (typeof d3 !== 'undefined') {
            resolve(d3);
            return;
        }

        const script = document.createElement('script');
        script.src = 'https://d3js.org/d3.v7.min.js';
        script.onload = () => resolve(window.d3);
        script.onerror = reject;
        document.head.appendChild(script);
    });
};

// Wait for D3 to load, then create chart
loadD3().then(() => {
    const {
        setGlobalOptions,
        events,
        env,
        getDataFromSearchQuery
    } = viz;

    setGlobalOptions({
        autoEmitRenderCompletedEvent: false
    });

    const data = getDataFromSearchQuery();
    const rawData = data.getData().data;

    // Transform to D3 format: array of {name, value}
    const data1 = rawData.map(row => ({
        name: row["0"],
        value: row["1"]
    }));

    const margin = { top: 20, right: 20, bottom: 30, left: 40 },
        width = 500 - margin.left - margin.right,
        height = 300 - margin.top - margin.bottom;

    const svg = d3.select("#chart")
        .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", `translate(${margin.left},${margin.top})`);

    const x = d3.scaleBand()
        .domain(data1.map(d => d.name))
        .range([0, width])
        .padding(0.2);

    const y = d3.scaleLinear()
        .domain([0, d3.max(data1, d => d.value)])
        .nice()
        .range([height, 0]);

    // Axes
    svg.append("g")
        .attr("transform", `translate(0,${height})`)
        .call(d3.axisBottom(x));

    svg.append("g")
        .call(d3.axisLeft(y));

    // Bars with animation
    const bars = svg.selectAll(".bar")
        .data(data1)
        .enter()
        .append("rect")
        .attr("class", "bar")
        .attr("x", d => x(d.name))
        .attr("y", y(0))
        .attr("width", x.bandwidth())
        .attr("height", 0)
        .attr("fill", "steelblue");

    if (!env.isPrintMode) {
        bars
            .transition()
            .duration(800)
            .attr("y", d => y(d.value))
            .attr("height", d => height - y(d.value))
            .on("end", function (_, i, nodes) {
                if (i === nodes.length - 1) {
                    console.log("✅ Chart fully rendered with animation");
                    events.emitRenderCompletedEvent();
                }
            });
    } else {
        bars
            .attr("y", d => y(d.value))
            .attr("height", d => height - y(d.value));

        events.emitRenderCompletedEvent();
    }
}).catch(error => {
    console.error("Failed to load D3.js:", error);
});

XLSX Export Integration

For XLSX exports, you need to provide custom logic to convert your third-party chart's data into a spreadsheet format.

Complete Example

/**
 * Available Columns:
 * "Ship Mode"
 * "Total Discount"
 */

// Load D3.js library dynamically
const loadD3 = () => {
  return new Promise((resolve, reject) => {
    if (typeof d3 !== 'undefined') {
      resolve(d3);
      return;
    }
    
    const script = document.createElement('script');
    script.src = 'https://d3js.org/d3.v7.min.js';
    script.onload = () => resolve(window.d3);
    script.onerror = reject;
    document.head.appendChild(script);
  });
};

// Wait for D3 to load, then create chart
loadD3().then(() => {
  const {
      setGlobalOptions,
      events,
      env,
      getDataFromSearchQuery
  } = viz;

  setGlobalOptions({
      autoEmitRenderCompletedEvent: false,
      // Take manual control of XLSX export
      autoHandledXLSXDownload: false
  });

  events.handleXLSXDownloadEvent((payload) => {
      console.log("Custom XLSX download payload", payload);

      // Here, write your own logic to export the chart built using
      // third party library to XLSX file.

      return {
          isDownloadHandled: true
      };
  });

  const data = getDataFromSearchQuery();
  const rawData = data.getData().data;

  // Transform to D3 format using array indices (not string keys)
  const data1 = rawData.map(row => ({
      name: row[0],   // Access by index, not row["0"]
      value: row[1]   // Access by index, not row["1"]
  }));

  const margin = { top: 20, right: 20, bottom: 30, left: 40 },
      width = 500 - margin.left - margin.right,
      height = 300 - margin.top - margin.bottom;

  const svg = d3.select("#chart")
      .append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", `translate(${margin.left},${margin.top})`);

  const x = d3.scaleBand()
      .domain(data1.map(d => d.name))
      .range([0, width])
      .padding(0.2);

  const y = d3.scaleLinear()
      .domain([0, d3.max(data1, d => d.value)])
      .nice()
      .range([height, 0]);

  // Axes
  svg.append("g")
      .attr("transform", `translate(0,${height})`)
      .call(d3.axisBottom(x));

  svg.append("g")
      .call(d3.axisLeft(y));

  // Bars with animation
  const bars = svg.selectAll(".bar")
      .data(data1)
      .enter()
      .append("rect")
      .attr("class", "bar")
      .attr("x", d => x(d.name))
      .attr("y", y(0))
      .attr("width", x.bandwidth())
      .attr("height", 0)
      .attr("fill", "steelblue");

  // Render with animation when in normal mode
  if (!env.isPrintMode) {
      bars
          .transition()
          .duration(800)
          .attr("y", d => y(d.value))
          .attr("height", d => height - y(d.value))
          .on("end", function (_, i, nodes) {
              if (i === nodes.length - 1) {
                  console.log("✅ Chart fully rendered with animation");
                  events.emitRenderCompletedEvent();
              }
          });
  } else {
      // Disable animations when in print mode
      bars
          .attr("y", d => y(d.value))
          .attr("height", d => height - y(d.value));

      events.emitRenderCompletedEvent();
  }
}).catch(error => {
  console.error("Failed to load D3.js:", error);
});