Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Chart export to bitmap (PNG, PDF, TIFF, JPG) #167

Merged
merged 10 commits into from

2 participants

@nurkiewicz

I implemented chart export functionality. It consists of client-side support (converting chart to SVG and posting it to the server) and a servlet in saiku that receives SVG (XML) and returns binary representation. Conversion is backed by batik library, thus it supports PNG, PDF (exported in vector format, high quality zoom available), TIFF or JPG. Also we can download SVG file as-is.

Unfortunately server-side component was required (you may wish to port servlet to REST service). Hidden form is used to make sure all files pop-up with browser's Save... dialog rather than replacing current page with image.

Known issues:

  • doesn't work on Google Chrome. Most likely some JavaScript issue.

This pull request contains all commits (first 8) from Multiple chart fixes. Last two implement the actual export on the client side.

See also

Chart export (server-side support) for server side support.

@pstoellberger
Collaborator

i was wondering if you could do the check + counter stuff within the first pass of the resultset as well?
e.g. the first item doesn't have a counter on it, the second will look like "item 1"

i have bad experience introducing several pass algorithms

appreciate this pull request though!

@pstoellberger pstoellberger merged commit d843b1d into from
@pstoellberger
Collaborator

when an export fails the UI is forwarded to an error page, maybe this can be handled better?
for the excel export i use window.open() so it wont affect the currently open UI state in case something goes wrong

maybe you have an idea for that!?
thanks!

@nurkiewicz
  1. By second pass do you mean makeSureUniqueLabels() function in this commit? I extracted it purposefully to keep the main code cleaner, but sure. I guess I can keep a list of already seen labels and in case of duplicate add [2]. I'll look into this.

  2. I am not very experienced with JavaScript, so I chose Content-Disposition header on the server side. window.open() indeed seems cleaner, but I am not sure if I can handle POSTing at the same time (I send a lot of data, SVG contents, unsuitable for query parameter) Feel free to look at it, I can't promise I will manage to fix this in foreseeable future.

@pstoellberger
Collaborator
  1. yes. that would be great
  2. dont worry about that too much if you cant think of a quick/good solution. its not that bad
@nurkiewicz nurkiewicz deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 10, 2013
  1. @nurkiewicz
  2. @nurkiewicz

    * Fixing charts when rows using multiple levels of the same hierarchy

    nurkiewicz authored
    * Labels from all levels are now joined, e.g. 'Feb/Q1/2012' instead of 'Feb'
  3. @nurkiewicz
  4. @nurkiewicz
  5. @nurkiewicz
  6. @nurkiewicz
  7. @nurkiewicz

    Fixing stacked bar chart by implicitly changing empty strings to 0

    nurkiewicz authored
    Without it empty strings caused maximum value in stacked bar chart to be incorrectly computed (NaN?) and nothing was displayed
  8. @nurkiewicz
Commits on Jan 11, 2013
  1. @nurkiewicz
Commits on Jan 17, 2013
  1. @nurkiewicz

    Fixed SVG servlet URI

    nurkiewicz authored
This page is out of date. Refresh to see the latest.
Showing with 89 additions and 36 deletions.
  1. +2 −0  .gitignore
  2. +87 −36 js/saiku/plugins/Chart/plugin.js
View
2  .gitignore
@@ -4,3 +4,5 @@ target/
node_modules/
*.DS_Store
.*.swp
+*.iml
+.idea
View
123 js/saiku/plugins/Chart/plugin.js
@@ -39,23 +39,34 @@ var Chart = Backbone.View.extend({
// Create navigation
this.nav = $("<div class='chart-switcher'>" +
- "<a href='#bar' class='i18n'>bar</a>" +
- "<a href='#stackedBar' class='i18n'>stacked bar</a>" +
- "<a href='#line' class='i18n'>line</a>" +
- "<a href='#pie' class='i18n'>pie</a>" +
- "<a href='#heatgrid' class='i18n'>heatgrid</a>" +
+ "<a class='type' href='#bar' class='i18n'>bar</a>" +
+ "<a class='type' href='#stackedBar' class='i18n'>stacked bar</a>" +
+ "<a class='type' href='#line' class='i18n'>line</a>" +
+ "<a class='type' href='#pie' class='i18n'>pie</a>" +
+ "<a class='type' href='#heatgrid' class='i18n'>heatgrid</a>" +
+ "Export to: " +
+ "<a class='export' href='#png' class='i18n'>PNG</a>, " +
+ "<a class='export' href='#pdf' class='i18n'>PDF</a>, " +
+ "<a class='export' href='#tiff' class='i18n'>TIFF</a>, " +
+ "<a class='export' href='#svg' class='i18n'>SVG</a>, " +
+ "<a class='export' href='#jpg' class='i18n'>JPG</a>" +
+ "<form id='svgChartPseudoForm' action='/saiku/svg' method='POST'>" +
+ "<input type='hidden' name='type' class='type'/>" +
+ "<input type='hidden' name='svg' class='svg'/>" +
+ "</form>" +
"</div>").css({
'padding-bottom': '10px'
});
- this.nav.find('a').css({
- color: '#666',
- 'margin-right': '5px',
- 'text-decoration': 'none',
- 'border': '1px solid #ccc',
- padding: '5px'
+ this.nav.find('a.type').css({
+ color: '#666',
+ 'margin-right': '5px',
+ 'text-decoration': 'none',
+ 'border': '1px solid #ccc',
+ padding: '5px'
})
.click(this.setOptions);
-
+ this.nav.find('a.export').click(this.exportChart);
+
// Append chart to workspace
$(this.workspace.el).find('.workspace_results')
.prepend($(this.el).hide())
@@ -94,6 +105,16 @@ var Chart = Backbone.View.extend({
return false;
},
+ exportChart: function(event) {
+ var type = $(event.target).attr('href').replace('#', '');
+ var svgContent = new XMLSerializer().serializeToString($('svg')[0]);
+ var form = $('#svgChartPseudoForm');
+ form.find('.type').val(type);
+ form.find('.svg').val(svgContent);
+ form.submit();
+ return false;
+ },
+
stackedBar: function() {
this.options.stacked = true;
this.options.type = "BarChart";
@@ -141,7 +162,7 @@ var Chart = Backbone.View.extend({
legend: true,
legendPosition:"top",
legendAlign: "right",
- colors: ["#B40010", "#CCC8B4", "#DDB965", "#72839D", "#1D2D40"],
+ colors: ["#4bb2c5", "#c5b47f", "#EAA228", "#579575", "#839557", "#958c12", "#953579", "#4b5de4", "#d8b83f", "#ff5800", "#0085cc"],
type: 'BarChart'
}, this.options);
@@ -204,50 +225,80 @@ var Chart = Backbone.View.extend({
this.data.metadata = [];
this.data.height = 0;
this.data.width = 0;
-
- if (args.data.cellset && args.data.cellset.length > 0) {
+
+ function makeSureUniqueLabels(resultset) {
+ function appendUniqueCounter() {
+ for(var i = 0; i < resultset.length; ++i) {
+ var record = resultset[i];
+ record[0] = record[0] + ' [' + (i + 1) + ']';
+ }
+ }
+
+ var labelsSet = {};
+ for(var i = 0; i < resultset.length; ++i) {
+ var record = resultset[i];
+ var label = record[0];
+ if(labelsSet[label]) {
+ appendUniqueCounter();
+ return;
+ } else {
+ labelsSet[label] = true;
+ }
+ }
+ }
+
+ var cellset = args.data.cellset;
+ if (cellset && cellset.length > 0) {
var lowest_level = 0;
- for (var row = 0; row < args.data.cellset.length; row++) {
- if (args.data.cellset[row][0].type == "ROW_HEADER_HEADER") {
+ for (var row = 0; row < cellset.length; row++) {
+ if (cellset[row][0].type == "ROW_HEADER_HEADER") {
this.data.metadata = [];
- for (var field = 0; field < args.data.cellset[row].length; field++) {
- if (args.data.cellset[row][field].type == "ROW_HEADER_HEADER") {
+ for (var field = 0; field < cellset[row].length; field++) {
+ if (cellset[row][field].type == "ROW_HEADER_HEADER") {
this.data.metadata.shift();
lowest_level = field;
}
this.data.metadata.push({
colIndex: field,
- colType: typeof(args.data.cellset[row + 1][field].value) !== "number" &&
- isNaN(args.data.cellset[row + 1][field].value
+ colType: typeof(cellset[row + 1][field].value) !== "number" &&
+ isNaN(cellset[row + 1][field].value
.replace(/[^a-zA-Z 0-9.]+/g,'')) ? "String" : "Numeric",
- colName: args.data.cellset[row][field].value
+ colName: cellset[row][field].value
});
}
- } else if (args.data.cellset[row][0].value !== "null" && args.data.cellset[row][0].value !== "") {
+ } else if (cellset[row][0].value !== "") {
var record = [];
- this.data.width = args.data.cellset[row].length;
- for (var col = lowest_level; col < args.data.cellset[row].length; col++) {
- var value = args.data.cellset[row][col].value;
- // check if the resultset contains the raw value, if not try to parse the given value
- if (args.data.cellset[row][col].properties.raw && args.data.cellset[row][col].properties.raw !== "null")
- {
- value = parseFloat(args.data.cellset[row][col].properties.raw);
- } else if (typeof(args.data.cellset[row][col].value) !== "number" &&
- parseFloat(args.data.cellset[row][col].value.replace(/[^a-zA-Z 0-9.]+/g,'')))
- {
- value = parseFloat(args.data.cellset[row][col].value.replace(/[^a-zA-Z 0-9.]+/g,''));
+ this.data.width = cellset[row].length;
+ var label = [];
+ for (var labelCol = lowest_level; labelCol >= 0; labelCol--) {
+ var lastKnownUpperLevelRow = row;
+ while(cellset[lastKnownUpperLevelRow] && cellset[lastKnownUpperLevelRow][labelCol].value === 'null') {
+ --lastKnownUpperLevelRow;
+ }
+ if(cellset[lastKnownUpperLevelRow]) {
+ label.push(cellset[lastKnownUpperLevelRow][labelCol].value);
}
- if (col == lowest_level) {
- value += " [" + row + "]";
+ }
+ record.push(label.join('/'));
+ for (var col = lowest_level + 1; col < cellset[row].length; col++) {
+ var cell = cellset[row][col];
+ var value = cell.value || 0;
+ // check if the resultset contains the raw value, if not try to parse the given value
+ var raw = cell.properties.raw;
+ if (raw && raw !== "null") {
+ value = parseFloat(raw);
+ } else if (typeof(cell.value) !== "number" && parseFloat(cell.value.replace(/[^a-zA-Z 0-9.]+/g,''))) {
+ value = parseFloat(cell.value.replace(/[^a-zA-Z 0-9.]+/g,''));
}
record.push(value);
}
this.data.resultset.push(record);
}
}
+ makeSureUniqueLabels(this.data.resultset);
this.data.height = this.data.resultset.length;
this.render();
} else {
Something went wrong with that request. Please try again.