2

From JavaScript, I make an ajax/xhr d3.csv() call which triggers a lengthy MySQL query (which can sometimes take more than 30 seconds to run). An HTML table is then generated (via d3.js) from the data.

I want the user to be able to download the data as a CSV file via a button click, but

  • I don't want to create a tmp file on the server for this
  • Running the query again on the server is not an option -- I don't want to make the user wait another 30 seconds (nor tie up the database again)
  • I want to specify the filename, e.g., descriptiveName-some_datetime_here.csv
  • It needs to work in IE (corporate America thing) and Safari (corporate Executive thing)

Converting the JSON data that d3 created into CSV is not an issue (I know how to do that part).

There are many similar SO questions, and the general consensus seems to be:
use a data URI and specify the filename in a download attribute (Q1, Q2, etc.).
But that attribute is sadly not supported on IE or Safari.

Community
  • 1
  • 1
Mark Rajcok
  • 348,511
  • 112
  • 482
  • 482

1 Answers1

2

Maybe there is a better way, but here's one way to do it: submit a form with the desired filename and the data as two hidden form elements. Have the server simply return the data with the appropriate headers set for a file download. No need for tmp files; works on all browsers.

HTML:

<form id="download-form" method="post">
   <input type="button" value="download CSV">
</form>
<!-- the button is right above the HTML table -->
<table>... </table>

JavaScript/D3:

var jsonData;
var filenameDateFormat = d3.time.format("%Y%m%d-%H%M%S");
// ... after loading the data, and setting jsonData to the data returned from d3.csv()
jsonData = data;
// display the form/button, which is initially hidden
d3.select("#download-form").style("display", "block");

d3.select("#download-form input[type=button]").on('click', function() {
    var downloadForm = d3.select("#download-form");
    // remove any existing hidden fields, because maybe the data changed
    downloadForm.selectAll("input[type=hidden]").remove();
    downloadForm
        .each(function() {
            d3.select(this).append("input")
                .attr({ type:  "hidden",
                        name:  "filename",
                        value: CHART_NAME + "-" 
                               + filenameDateFormat(new Date()) + ".csv"});
            d3.select(this).append("input")
                .attr({ type:  "hidden",
                        name:  "data",
                        value: convertToCsv(jsonData) });
        });
    document.getElementById("download-form").submit();
});
function convertToCsv(data) {
    var csvArray = ['field_name1_here,field_name2_here,...'];
    data.forEach(function(d) {
        csvArray.push(d.field_name1_here + ',' + d.field_name2_here + ...);
    });
    return csvArray.join("\n");
}

Server (Python, using Bottle):

@app.route('/download', method='POST')
def download():
    if request.environ.get('HTTP_USER_AGENT').find('Chrome'):
        # don't add the Content-Type, as this causes Chrome to output the following
        # to the console:
        #  Resource interpreted as Document but transferred with MIME type text/csv
        pass
    else:
        response.set_header('Content-Type', 'text/csv')
    response.set_header('Content-Disposition',
        'attachment; filename="' + request.forms.filename + '"')
    return request.forms.data

Not pretty, but it works.

Mark Rajcok
  • 348,511
  • 112
  • 482
  • 482