-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[Enhancement] Export plot as PDF, JPG, or PNG #1164
Conversation
This will be used for exporting an image of the plot.
Extends PlotController by adding three new scoped methods: exportPDF, exportPNG, exportJPG. All three methods use basically the same steps. The HTML node of the plot is passed through html2canvas which generates a canvas. From the canvas we export a blob, PNG, or JPG then save the file.
$scope.exportJPG = function () { | ||
PlotController.prototype.exportJPG(self.$element, 'plot.jpg'); | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PlotController is exposed as plot
in the Angular scope already, so I believe these could be omitted by instead having ng-click="plot.exportPDF()"
in the plot template. This would make the code and changeset simpler.
Sure! Left a couple of comments in-line.
I agree that a user-specified name is better UX, but think this can also be deferred (particularly if collecting that input proves non-obvious/non-trivial for some reason); saving as "plot.png" or "plot.pdf" is sufficient to meet the use case. A user can always rename the file after it's downloaded. |
Set export functions on the ExportImageService prototype. Insantiated ExportImageService in the PlotController for better dependcy injection.
It's worth noting that I changed the .gl-plot position from relative to absolute in an attempt to match the results to a similar requirement in MCT Table. Setting to absolute caused no regressions as far as I could tell, but I have not attempted browsers outside of Chrome.
Buttons temporarily hide until export completes.
I would prefer this be passed in via the bundle, but it continues to fail saying "Unknown Provider". I have chosen to require them into the module the old-fashioned way, then allow an injectible dependency to override.
@VWoeltjen unit test has been added. I did try to set up dependency injection with html2canvas, jsPDF, and saveAs, which in the end I did successfully, but I am dissatisfied with my method. For whatever reason, adding any of those to the dependencies in plot/bundle.js wind up with "Unknown provider" errors. If you have time for one more code review, all I have left is to make sure the plot never gets cut off at the bottom (almost certainly a CSS issue) and we're good to go. |
Based on updates from 0457f7b these styles are no longer necessary and have been removed.
@charlesh88 cool. I cherry-picked 0457f7b and merged. +1 on moving this to the context menu long-term. |
@hudsonfoo Hi Hudson: The changes in _plots-main.scss lines 25 - 39 shouldn't be there - we actually want the export buttons to always display when the user is viewing the object directly to aid discoverability. We hide and show them when that object is in a Layout because often it's a smaller size; that's what the work in #1167 was about generalizing. Sorry to have to correct this after the fact; I'm pretty behind on getting our style guide together which should include this info. Also: a bit nervous about changing .gl-plot from position: relative to absolute, but I assume that was done for the needs of exporting? |
@charlesh88 cool no prob. After some additional testing with the new cherry-pick, looks like those styles are no longer required, including the .gl-plot change. I've put the stylesheet back to its original state and fixed a minor typo. As for style guides, I purchased several hardcopy NASA Graphics Standards manuals awhile back and distributed them throughout my office as a reference point for our own :) Pretty cool. |
@@ -84,12 +88,30 @@ define([ | |||
] | |||
} | |||
], | |||
"services": [ | |||
{ | |||
"key": "ExportImageService", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit, but exportImageService
would be more consistent with naming conventions used elsewhere. (Same goes for its usage in PlotController; we use UpperCamelCase as a convention to identify constructor functions.)
} | ||
} | ||
|
||
ExportImageService.prototype.exportPDF = function (element, filename) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use some brief JSDoc here for this and the other export methods
Another good catch @VWoeltjen, thanks. |
@@ -54,7 +54,7 @@ define( | |||
* @throws {Error} an error is thrown if WebGL is unavailable. | |||
*/ | |||
function GLChart(canvas) { | |||
var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"), | |||
var gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }) || canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would like to break this up to reduce line length, e.g.
var gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }) ||
canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }),
mockSaveAs | ||
); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the verification of high-level interactions is good here, but would also like to pass in a testElement
(don't think there's anything that needs to be mocked, so this could just be some object) to the exportXYZ
methods, in order to expect(mockHtml2Canvas).toHaveBeenCalledWith(testElement, { onrendered: jasmine.any(Function) })
Testing of the onrendered
callback itself would also be good. In particular, it would be nice to check interactions with the canvas object to make sure exportPNG isn't producing JPGs and so forth.
@hudsonfoo Thanks! I'm good to go on this PR with my eval. |
@VWoeltjen please see updated tests which look for proper html2canvas call as well as ensuring canvas.toBlob requests the correct image type from the canvas. |
Everything's looking really good, thanks for working on this! Double bonus extra gratitude for including licensing info for the new run-time dependencies. Reviewer Checklist
|
For issue #967.
I'm reviewing methods for adding testing, and there are two minor issues left, but I would appreciate a code review. I'm doing my best to make updates the same way you would internally but it may take a few swings to get it right.
Remaining issues are trivial: