Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download feature request/issue #51

Open
EMCO-DEME opened this issue Oct 30, 2024 · 5 comments
Open

Download feature request/issue #51

EMCO-DEME opened this issue Oct 30, 2024 · 5 comments

Comments

@EMCO-DEME
Copy link

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
Copy link
Collaborator

jacksongoode commented Oct 31, 2024

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
Copy link

alexl04 commented Nov 4, 2024

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
Copy link

alexl04 commented Nov 4, 2024

===============================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
Copy link

alexl04 commented Nov 4, 2024

=======================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
Copy link

alexl04 commented Nov 4, 2024

===============================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
};



```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants