nline / nline-plotlyjs-panel

Plotly.js for Grafana
https://grafana.com/grafana/plugins/nline-plotlyjs-panel/
Apache License 2.0
39 stars 4 forks source link

Download feature request/issue #51

Open EMCO-DEME opened 2 weeks ago

EMCO-DEME commented 2 weeks ago

Hi,

I was wondering if it is possible to add a download button to a plotly panel in Grafana. I was able to add the button , but it doesn't allow me to actually download the data.

Am I doing something wrong or is there an other way to add a download button?

Example script (provided by GPT): // Access the fields let time = [ 1696003200000, // 2023-09-30 00:00:00 1696006800000, // 2023-09-30 01:00:00 1696010400000, // 2023-09-30 02:00:00 1696014000000, // 2023-09-30 03:00:00 1696017600000, // 2023-09-30 04:00:00 1696021200000 // 2023-09-30 05:00:00 ];

// Hardcoded test data (some random values) let test = [5.1, 6.3, 4.8, 7.2, 8.0, 6.7];

// Prepare CSV data function generateCSV() { let csvRows = ["Time,test"]; for (let i = 0; i < time.length; i++) { const row = [ new Date(time[i]).toISOString(), // Format timestamp as ISO string pitch[i]?.toFixed(2) ]; csvRows.push(row.join(",")); } return csvRows.join("\n"); }

// Download CSV function function downloadCSV() { const csvData = generateCSV(); const blob = new Blob([csvData], { type: "text/csv" }); const url = URL.createObjectURL(blob);

// Create and click a temporary link to trigger download
const link = document.createElement("a");
link.href = url;
link.download = "test_data.csv";
document.body.appendChild(link);
link.click();

// Clean up
document.body.removeChild(link);
URL.revokeObjectURL(url);

}

// Set up the trace let tracetest = { x: time.map(t => new Date(t)), // Convert time to Date objects for proper formatting y: test, mode: 'lines', name: test };

// Define updatemenus with a pseudo-download button that triggers the downloadCSV function var updatemenus = [{ type: 'buttons', buttons: [ { label: 'Download Data as CSV', method: 'relayout', // Dummy method args: [{}, {downloadCSV: true}] // Pass a custom argument } ], direction: 'right', pad: {'r': 10, 't': 10}, showactive: false, x: 0.15, xanchor: 'left', y: 1.2, yanchor: 'top' }];

// Define layout let layout = { title: 'test Data Over Time', xaxis: { autorange: true }, yaxis: { title: 'test', autorange: true }, updatemenus: updatemenus };

// Add a listener to call downloadCSV if the button is clicked document.addEventListener('plotly_relayout', (event) => { if (event.detail && event.detail.downloadCSV) { downloadCSV(); } });

// Return the data and layout for the plot return { data: [tracetest], layout };

Screenshot 2024-10-30 162500

jacksongoode commented 2 weeks ago

Try some of this code...

  function getTime() {
    const time = window
      .grafanaRuntime
      .getDashboardTimeRange();
    const fromStr = new Date(time.from).toLocaleDateString("en-GB");
    const toStr = new Date(time.to).toLocaleDateString("en-GB");
    return [fromStr, toStr];
  }
  function downloadCSV(data, name, time) {
    // Create Blob from CSV data and download
    const csv = convertToCSV(data);
    const blob = new Blob([csv], {type: "text/csv;charset=utf-8;"});
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);

    const [fromStr, toStr] = time;
    const filename = stuff.csv`;
    a.download = filename;
    a.click();

    // Revoke the Object URL
    URL.revokeObjectURL(a.href);
  }
  const time = getTime();
  const data = window
        .grafanaRuntime
        .getPanelData();
  const fields = data[selectedId]
        .series[0]
        .fields;
  downloadCSV(fields, selectedName, time);
alexl04 commented 1 week ago

Nice this is exactly what Im looking for!

I tried various ways to let the user download the data via a button but I did not manage to get it working to my liking. Reason for having a download button for data from the script is that we can run complex calculations in JS which we cannot with grafana transformations. However our users often need to download the data as csv to include those values in deliverables other than screenshot/png export from grafana.

Ideally we would have a download button in the "displayModeBar" but I did not manage to add a custom button. So I tried to create a download button in the layout of the panel via "updatemenus" instead but executing a script from that button seems not allowed (and or I dont know how to get it working). I ended up with 2 working methods (that are not ideal). The first method that worked is via the "the On-event Handler" menu. The downloadCSV script runs sucesfully if you click on a point of the trace but not on a button or someting like that. The second option is a bit sketchy and If you ask me also outside this grafana panel plugin but i got it working via a button in the "document.body" where scripts can be executed. See below scripts.

===============================Via "the On-event Handler"================================

SCRIPT """ // Get the data from the first query // const fields = data.series[0].fields;

// Dummy data to mimic fields from the query const fields = [ { name: 'Time', type: 'time', typeInfo: { format: 'timestamp' }, config: { unit: 'dateTime' }, values: [ '2024-11-04T08:00:00Z', '2024-11-04T08:01:00Z', '2024-11-04T08:02:00Z', '2024-11-04T08:03:00Z', '2024-11-04T08:04:00Z' ] }, { name: 'Speed over ground (knots)', type: 'number', typeInfo: { format: 'float' }, labels: { unit: 'knots' }, config: {}, values: [5.2, 5.4, 5.3, 5.6, 5.7] } ];

// Define your traces based on the data const traces = [ { x: fields[0].values, // example x-values y: fields[1].values, // example y-values type: 'scatter', mode: 'lines+markers', name: 'Example Trace' } ];

// Return data and layout for Plotly to render in Grafana return { data: traces }; """

ON EVENT-HANDLER """ function convertToCSV(fields) { const headers = fields.map(field => field.name).join(","); const rows = fields[0].values.map((_, i) => fields.map(field => field.values.get(i)).join(",") ); return [headers, ...rows].join("\n"); }

function downloadCSV(fields, name) { const csv = convertToCSV(fields); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob);

const filename = (name + 'from' + variables.from + 'to' + variables.to + '__' + utils.dayjs() + '.csv'); a.download = filename; a.click();

URL.revokeObjectURL(a.href); }

// Dummy data to mimic fields from the query const fields = [ { name: 'Time', type: 'time', typeInfo: { format: 'timestamp' }, config: { unit: 'dateTime' }, values: [ '2024-11-04T08:00:00Z', '2024-11-04T08:01:00Z', '2024-11-04T08:02:00Z', '2024-11-04T08:03:00Z', '2024-11-04T08:04:00Z' ] }, { name: 'Speed over ground (knots)', type: 'number', typeInfo: { format: 'float' }, labels: { unit: 'knots' }, config: {}, values: [5.2, 5.4, 5.3, 5.6, 5.7] } ];

try { // Add data in the On-event Section //const fields = data.series[0].fields;

// Event handling for the Plotly panel const { type: eventType, data: eventData } = event;

switch (eventType) { case 'click': // Check if the click event is for the 'Download CSV' button >> THIS DOES NOT WORK if (eventData && eventData.points && eventData.points[0].label === 'Download CSV') { downloadCSV(fields, "exported_data"); } console.debug('clicked on the button!') console.debug('eventData',eventData) console.debug('eventData.points',eventData.points) console.debug('eventData.points[0].label', eventData.points[0].label) break; }

switch (eventType) { case 'click': // Check if there is a click event ont the plotted trace >> THIS DOES WORK if (eventData) { downloadCSV(fields, "exported_data"); } console.debug('clicked on the line!') console.debug('eventData',eventData) console.debug('eventData.points',eventData.points) console.debug('eventData.points[0].label', eventData.points[0].label) // The label is undefined break; } } catch (error) { console.error('Error in onclick handler:', error); } """

===============================Via the "document.body"================================

SCRIPT

""" function convertToCSV(fields) { const headers = fields.map(field => field.name).join(","); const rows = fields[0].values.map((_, i) => fields.map(field => field.values.get(i)).join(",") ); return [headers, ...rows].join("\n"); }

function downloadCSV(fields, name) { const csv = convertToCSV(fields); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob);

const filename = (name + 'from' + variables.from + 'to' + variables.to + '__' + utils.dayjs() + '.csv'); a.download = filename; a.click();

URL.revokeObjectURL(a.href); }

// Get the data from the Grafana panel //const fields = data.series[0].fields;

// Dummy data to mimic fields from the query const fields = [ { name: 'Time', type: 'time', typeInfo: { format: 'timestamp' }, config: { unit: 'dateTime' }, values: [ '2024-11-04T08:00:00Z', '2024-11-04T08:01:00Z', '2024-11-04T08:02:00Z', '2024-11-04T08:03:00Z', '2024-11-04T08:04:00Z' ] }, { name: 'Speed over ground (knots)', type: 'number', typeInfo: { format: 'float' }, labels: { unit: 'knots' }, config: {}, values: [5.2, 5.4, 5.3, 5.6, 5.7] } ];

// Define your traces based on the data const traces = [ { x: fields[0].values, // example x-values y: fields[1].values, // example y-values type: 'scatter', mode: 'lines+markers', name: 'Example Trace' } ];

// Add a custom download button with a longer delay setTimeout(() => { // Check if the button already exists to avoid adding multiple buttons if (!document.getElementById("downloadButton")) { const button = document.createElement("button"); button.id = "downloadButton"; button.innerText = "Download CSV"; button.style.position = "absolute"; button.style.top = "10px"; button.style.right = "10px"; button.style.zIndex = "1000"; button.style.padding = "8px"; button.style.backgroundColor = "#4CAF50"; button.style.color = "white"; button.style.border = "none"; button.style.cursor = "pointer";

// Attach download functionality
button.onclick = () => downloadCSV(fields, "exported_data");

// Try appending the button to a higher-level container
const panelContainer = document.querySelector(".panel-content") || document.body;
panelContainer.appendChild(button);

} }, 1000); // Increase delay to ensure the DOM is fully loaded

// Return traces and layout for Grafana to render return { data: traces }; """

=======================Failed Via the Layout "updatesmenu"===========================

SCRIPT

""" function convertToCSV(fields) { const headers = fields.map(field => field.name).join(","); const rows = fields[0].values.map((_, i) => fields.map(field => field.values.get(i)).join(",") ); return [headers, ...rows].join("\n"); }

function downloadCSV(fields, name) { const csv = convertToCSV(fields); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob);

const filename = (name + 'from' + variables.from + 'to' + variables.to + '__' + utils.dayjs() + '.csv'); a.download = filename; a.click();

URL.revokeObjectURL(a.href); }

// Get the data from the Grafana panel //const fields = data.series[0].fields;

// Dummy data to mimic fields from the query const fields = [ { name: 'Time', type: 'time', typeInfo: { format: 'timestamp' }, config: { unit: 'dateTime' }, values: [ '2024-11-04T08:00:00Z', '2024-11-04T08:01:00Z', '2024-11-04T08:02:00Z', '2024-11-04T08:03:00Z', '2024-11-04T08:04:00Z' ] }, { name: 'Speed over ground (knots)', type: 'number', typeInfo: { format: 'float' }, labels: { unit: 'knots' }, config: {}, values: [5.2, 5.4, 5.3, 5.6, 5.7] } ];

// Define your traces based on the data const traces = [ { x: fields[0].values, // example x-values y: fields[1].values, // example y-values type: 'scatter', mode: 'lines+markers', name: 'Example Trace' } ];

// Define the layout with a custom button for downloading CSV const layout = { updatemenus: [ { type: 'buttons', showactive: true, buttons: [ { label: 'Download CSV', method: 'none', args: [], execute: function () { downloadCSV(fields, "exported_data"); } } ] } ] };

// Return the data and layout for Plotly to render return { data: traces, layout: layout }; """

alexl04 commented 1 week ago

===============================Via "the On-event Handler"================================ THE SCRIPT

// Get the data from the first query // const fields = data.series[0].fields;

// Dummy data to mimic fields from the query const fields = [ { name: 'Time', type: 'time', typeInfo: { format: 'timestamp' }, config: { unit: 'dateTime' }, values: [ '2024-11-04T08:00:00Z', '2024-11-04T08:01:00Z', '2024-11-04T08:02:00Z', '2024-11-04T08:03:00Z', '2024-11-04T08:04:00Z' ] }, { name: 'Speed over ground (knots)', type: 'number', typeInfo: { format: 'float' }, labels: { unit: 'knots' }, config: {}, values: [5.2, 5.4, 5.3, 5.6, 5.7] } ];

// Define your traces based on the data const traces = [ { x: fields[0].values, // example x-values y: fields[1].values, // example y-values type: 'scatter', mode: 'lines+markers', name: 'Example Trace' } ];

// Return data and layout for Plotly to render in Grafana return { data: traces };

THE ON-EVENT HANDLER

function convertToCSV(fields) { const headers = fields.map(field => field.name).join(","); const rows = fields[0].values.map((_, i) => fields.map(field => field.values.get(i)).join(",") ); return [headers, ...rows].join("\n"); }

function downloadCSV(fields, name) { const csv = convertToCSV(fields); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob);

const filename = (name + 'from' + variables.from + 'to' + variables.to + '__' + utils.dayjs() + '.csv'); a.download = filename; a.click();

URL.revokeObjectURL(a.href); }

// Dummy data to mimic fields from the query const fields = [ { name: 'Time', type: 'time', typeInfo: { format: 'timestamp' }, config: { unit: 'dateTime' }, values: [ '2024-11-04T08:00:00Z', '2024-11-04T08:01:00Z', '2024-11-04T08:02:00Z', '2024-11-04T08:03:00Z', '2024-11-04T08:04:00Z' ] }, { name: 'Speed over ground (knots)', type: 'number', typeInfo: { format: 'float' }, labels: { unit: 'knots' }, config: {}, values: [5.2, 5.4, 5.3, 5.6, 5.7] } ];

try { // Add data in the On-event Section //const fields = data.series[0].fields;

// Event handling for the Plotly panel const { type: eventType, data: eventData } = event;

switch (eventType) { case 'click': // Check if the click event is for the 'Download CSV' button >> THIS DOES NOT WORK if (eventData && eventData.points && eventData.points[0].label === 'Download CSV') { downloadCSV(fields, "exported_data"); } console.debug('clicked on the button!') console.debug('eventData',eventData) console.debug('eventData.points',eventData.points) console.debug('eventData.points[0].label', eventData.points[0].label) break; }

switch (eventType) { case 'click': // Check if there is a click event ont the plotted trace >> THIS DOES WORK if (eventData) { downloadCSV(fields, "exported_data"); } console.debug('clicked on the line!') console.debug('eventData',eventData) console.debug('eventData.points',eventData.points) console.debug('eventData.points[0].label', eventData.points[0].label) // The label is undefined break; } } catch (error) { console.error('Error in onclick handler:', error); }

alexl04 commented 1 week ago

=======================Failed Via the Layout "updatesmenu"===========================

SCRIPT

function convertToCSV(fields) { const headers = fields.map(field => field.name).join(","); const rows = fields[0].values.map((_, i) => fields.map(field => field.values.get(i)).join(",") ); return [headers, ...rows].join("\n"); }

function downloadCSV(fields, name) { const csv = convertToCSV(fields); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob);

const filename = (name + 'from' + variables.from + 'to' + variables.to + '__' + utils.dayjs() + '.csv'); a.download = filename; a.click();

URL.revokeObjectURL(a.href); }

// Get the data from the Grafana panel //const fields = data.series[0].fields;

// Dummy data to mimic fields from the query const fields = [ { name: 'Time', type: 'time', typeInfo: { format: 'timestamp' }, config: { unit: 'dateTime' }, values: [ '2024-11-04T08:00:00Z', '2024-11-04T08:01:00Z', '2024-11-04T08:02:00Z', '2024-11-04T08:03:00Z', '2024-11-04T08:04:00Z' ] }, { name: 'Speed over ground (knots)', type: 'number', typeInfo: { format: 'float' }, labels: { unit: 'knots' }, config: {}, values: [5.2, 5.4, 5.3, 5.6, 5.7] } ];

// Define your traces based on the data const traces = [ { x: fields[0].values, // example x-values y: fields[1].values, // example y-values type: 'scatter', mode: 'lines+markers', name: 'Example Trace' } ];

// Define the layout with a custom button for downloading CSV const layout = { updatemenus: [ { type: 'buttons', showactive: true, buttons: [ { label: 'Download CSV', method: 'none', args: [], execute: function () { downloadCSV(fields, "exported_data"); } } ] } ] };

// Return the data and layout for Plotly to render return { data: traces, layout: layout };

alexl04 commented 1 week ago

===============================Via the "document.body"================================

SCRIPT

function convertToCSV(fields) { const headers = fields.map(field => field.name).join(","); const rows = fields[0].values.map((_, i) => fields.map(field => field.values.get(i)).join(",") ); return [headers, ...rows].join("\n"); }

function downloadCSV(fields, name) { const csv = convertToCSV(fields); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob);

const filename = (name + 'from' + variables.from + 'to' + variables.to + '__' + utils.dayjs() + '.csv'); a.download = filename; a.click();

URL.revokeObjectURL(a.href); }

// Get the data from the Grafana panel //const fields = data.series[0].fields;

// Dummy data to mimic fields from the query const fields = [ { name: 'Time', type: 'time', typeInfo: { format: 'timestamp' }, config: { unit: 'dateTime' }, values: [ '2024-11-04T08:00:00Z', '2024-11-04T08:01:00Z', '2024-11-04T08:02:00Z', '2024-11-04T08:03:00Z', '2024-11-04T08:04:00Z' ] }, { name: 'Speed over ground (knots)', type: 'number', typeInfo: { format: 'float' }, labels: { unit: 'knots' }, config: {}, values: [5.2, 5.4, 5.3, 5.6, 5.7] } ];

// Define your traces based on the data const traces = [ { x: fields[0].values, // example x-values y: fields[1].values, // example y-values type: 'scatter', mode: 'lines+markers', name: 'Example Trace' } ];

// Add a custom download button with a longer delay setTimeout(() => { // Check if the button already exists to avoid adding multiple buttons if (!document.getElementById("downloadButton")) { const button = document.createElement("button"); button.id = "downloadButton"; button.innerText = "Download CSV"; button.style.position = "absolute"; button.style.top = "10px"; button.style.right = "10px"; button.style.zIndex = "1000"; button.style.padding = "8px"; button.style.backgroundColor = "#4CAF50"; button.style.color = "white"; button.style.border = "none"; button.style.cursor = "pointer";

// Attach download functionality
button.onclick = () => downloadCSV(fields, "exported_data");

// Try appending the button to a higher-level container
const panelContainer = document.querySelector(".panel-content") || document.body;
panelContainer.appendChild(button);

} }, 1000); // Increase delay to ensure the DOM is fully loaded

// Return traces and layout for Grafana to render return { data: traces };