Skip to content
This repository was archived by the owner on Jan 10, 2023. It is now read-only.

Commit c8f66a5

Browse files
committed
Merge pull request #282 from GoogleCloudPlatform/dev-joemu-copy-chart
Add copy to image functionality
2 parents ea87b42 + 778c4f9 commit c8f66a5

File tree

10 files changed

+113
-15
lines changed

10 files changed

+113
-15
lines changed

client/components/dashboard/dashboard-directive.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
<span class="glyphicon glyphicon-refresh pk-widget-button"
3333
ng-show="widget.model.type === widgetFactorySvc.widgetTypes.CHART"
3434
ng-click="clickRefreshWidget($event, widget)"></span>
35+
<span class="fa fa-camera pk-widget-button"
36+
ng-show="widgetSvc.isCopyableAsImage(widget)"
37+
ng-click="clickCopyAsImage($event, widget)">
38+
<md-tooltip>Copy chart image to clipboard</md-tooltip>
39+
</span>
3540
<span class="glyphicon glyphicon-resize-full pk-widget-button"
3641
ng-click="maximizeWidget(widget)"></span>
3742
<a class="pk-widget-button"

client/components/dashboard/dashboard-directive.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ explorer.components.dashboard.DashboardDirective = function() {
6565
/** @export */
6666
$scope.widgetFactorySvc = widgetFactoryService;
6767

68+
/** @export */
69+
$scope.widgetSvc = widgetService;
70+
6871
/** @export */
6972
$scope.clickRefreshWidget = function(event, widget) {
7073
dashboardService.refreshWidget(widget);
@@ -79,6 +82,13 @@ explorer.components.dashboard.DashboardDirective = function() {
7982
sidebarTabService.resolveSelectedTabForContainer();
8083
}
8184

85+
/** @export */
86+
$scope.clickCopyAsImage = function(event, widget) {
87+
event.stopPropagation();
88+
89+
widgetService.copyAsImage(widget);
90+
}
91+
8292
/** @export */
8393
$scope.maximizeWidget = function(widget) {
8494
$scope.dashboardSvc.maximizeWidget(widget);

client/components/dashboard/dashboard-widget-focus-directive.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
<div class="pk-widget-toolbar">
1010
<span class="glyphicon glyphicon-refresh pk-widget-button"
1111
ng-show="ngModel.model.type === widgetFactorySvc.widgetTypes.CHART"
12-
ng-click="clickRefreshWidget($event, widget)"></span>
12+
ng-click="clickRefreshWidget($event, ngModel)"></span>
13+
<span class="fa fa-camera pk-widget-button"
14+
ng-show="widgetSvc.isCopyableAsImage(ngModel)"
15+
ng-click="clickCopyAsImage($event, ngModel)"
16+
aria-hidden="true">
17+
<md-tooltip>Copy chart image to clipboard</md-tooltip>
18+
</span>
1319
<span class="glyphicon glyphicon-resize-small pk-widget-button"
1420
ng-click="restoreWidget()"></span>
1521
<a class="pk-widget-button"

client/components/dashboard/dashboard-widget-focus-directive.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ explorer.components.dashboard.DashboardWidgetFocusDirective = function() {
5757
/** @export */
5858
$scope.widgetFactorySvc = widgetFactoryService;
5959

60+
/** @export */
61+
$scope.widgetSvc = widgetService;
62+
6063
Object.defineProperty($scope, 'container', {
6164
get: function() {
6265
if ($scope.ngModel) {
@@ -78,6 +81,14 @@ explorer.components.dashboard.DashboardWidgetFocusDirective = function() {
7881
dashboardService.restoreWidget($scope.ngModel);
7982
}
8083

84+
/** @export */
85+
$scope.clickCopyAsImage = function(event, widget) {
86+
event.stopPropagation();
87+
event.preventDefault();
88+
89+
widgetService.copyAsImage(widget);
90+
}
91+
8192
/**
8293
* Returns true if the widget should scroll its overflow, otherwise stretch.
8394
*/

client/components/explorer/explorer-page-directive.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<div class="pk-page-body" ng-controller="DashboardCtrl as dashboardCtrl"
44
layout="row" flex>
55

6+
<img id="pk-chart-image-buffer">
7+
68
<sidebar-tabs class="pk-sidebar-tabs" layout="column"></sidebar-tabs>
79

810
<div class="pk-page-leftwell" id="pk-page-leftwell" layout="column"
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
[
22
{"title": "Annotation Chart", "className": "AnnotationChart"},
33
{"title": "Area", "className": "AreaChart",
4-
"seriesColor": true},
4+
"seriesColor": true, "canScreenshot": true},
55
{"title": "Bar", "className": "BarChart",
6-
"seriesColor": true},
6+
"seriesColor": true, "canScreenshot": true},
77
{"title": "Candlestick", "className": "CandlestickChart",
8-
"seriesColor": true},
8+
"seriesColor": true, "canScreenshot": true},
99
{"title": "Column", "className": "ColumnChart",
10-
"seriesColor": true},
10+
"seriesColor": true, "canScreenshot": true},
1111
{"title": "Combo", "className": "ComboChart",
12-
"seriesColor": true},
12+
"seriesColor": true, "canScreenshot": true},
1313
{"title": "Gauge", "className": "Gauge"},
14-
{"title": "Geo", "className": "GeoChart"},
14+
{"title": "Geo", "className": "GeoChart", "canScreenshot": true},
1515
{"title": "Histogram", "className": "Histogram",
16-
"seriesColor": true, "seriesStartColumnIndex": 0},
16+
"seriesColor": true, "seriesStartColumnIndex": 0, "canScreenshot": true},
1717
{"title": "Line", "className": "LineChart",
18-
"seriesColor": true},
18+
"seriesColor": true, "canScreenshot": true},
1919
{"title": "Org", "className": "OrgChart"},
20-
{"title": "Pie", "className": "PieChart"},
20+
{"title": "Pie", "className": "PieChart", "canScreenshot": true},
2121
{"title": "Sankey", "className": "Sankey"},
2222
{"title": "Scatter", "className": "ScatterChart",
23-
"seriesColor": true},
23+
"seriesColor": true, "canScreenshot": true},
2424
{"title": "Stepped Area", "className": "SteppedAreaChart",
25-
"seriesColor": true},
26-
{"title": "Table", "className": "Table"},
25+
"seriesColor": true, "canScreenshot": true},
26+
{"title": "Table", "className": "Table", "canScroll": true},
2727
{"title": "Word Tree", "className": "WordTree"}
2828
]

client/components/widget/data_viz/gviz/gviz-directive.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ explorer.components.widget.data_viz.gviz.gvizChart = function(
9393
// Create and attach to this element a gviz ChartWrapper
9494
let chartWrapper = chartWrapperService.create();
9595
chartWrapper.setContainerId(element[0].getElementsByClassName('pk-chart')[0]);
96+
scope.widgetConfig.state().chart.element = chartWrapper;
9697

9798
// We currently have animations enabled globally thanks to the
9899
// material design module, this has the side effect of animating

client/components/widget/widget-factory-service.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ goog.require('p3rf.perfkit.explorer.components.dashboard.DashboardInstance');
2929
goog.require('p3rf.perfkit.explorer.components.dashboard.DashboardModel');
3030
goog.require('p3rf.perfkit.explorer.components.dashboard.DashboardVersionService');
3131
goog.require('p3rf.perfkit.explorer.components.error.ErrorService');
32+
goog.require('p3rf.perfkit.explorer.models.ChartType');
3233
goog.require('p3rf.perfkit.explorer.models.ChartWidgetConfig');
3334
goog.require('p3rf.perfkit.explorer.models.WidgetConfig');
3435
goog.require('p3rf.perfkit.explorer.models.WidgetModel');
@@ -47,6 +48,7 @@ const DashboardModel = explorer.components.dashboard.DashboardModel;
4748
const DashboardVersionService =
4849
explorer.components.dashboard.DashboardVersionService;
4950
const ErrorService = explorer.components.error.ErrorService;
51+
const ChartType = explorer.models.ChartType;
5052
const WidgetConfig = explorer.models.WidgetConfig;
5153
const WidgetModel = explorer.models.WidgetModel;
5254
const WidgetState = explorer.models.WidgetState;
@@ -76,6 +78,12 @@ explorer.components.widget.WidgetFactoryService = function(
7678
*/
7779
this.widgetTypes = WidgetType;
7880

81+
/**
82+
* Exposes the chart types enum for directives.
83+
* @export {!Object.<string, string>}
84+
*/
85+
this.chartTypes = ChartType;
86+
7987
/**
8088
* Hash table of widgets.
8189
*

client/components/widget/widget-service.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ const WidgetConfig = explorer.models.WidgetConfig;
3939
*/
4040
explorer.components.widget.WidgetService = class {
4141
/** @ngInject */
42-
constructor(widgetFactoryService) {
42+
constructor(widgetFactoryService, chartWrapperService) {
4343
/** @export {string} */
4444
this.WIDGET_DELETE_WARNING = 'The widget will be deleted:\n\n';
4545

4646
this.widgetFactorySvc = widgetFactoryService;
47+
48+
this.chartWrapperSvc = chartWrapperService;
4749
};
4850

4951
/**
@@ -63,7 +65,54 @@ explorer.components.widget.WidgetService = class {
6365

6466
return this.WIDGET_DELETE_WARNING + widgetName;
6567
};
66-
68+
69+
/**
70+
* Copies an image of the current chart to the clipboard.
71+
*
72+
* @param {!WidgetConfig} widget
73+
*/
74+
copyAsImage(widget) {
75+
let chartWrapper = widget.state().chart.element;
76+
let chartObject = chartWrapper.getChart();
77+
78+
let imageBuffer = document.getElementById('pk-chart-image-buffer');
79+
let sel = null;
80+
imageBuffer.src = chartObject.getImageURI();
81+
82+
try {
83+
let range = document.createRange();
84+
range.selectNode(imageBuffer);
85+
86+
sel = window.getSelection();
87+
sel.removeAllRanges();
88+
sel.addRange(range);
89+
90+
document.execCommand('copy');
91+
} finally {
92+
sel.removeAllRanges();
93+
imageBuffer.src = '';
94+
}
95+
}
96+
97+
/**
98+
* Returns true if the widget is screenshottable, otherwise false.
99+
* @param {!WidgetConfig} widget
100+
* @return {boolean}
101+
* @export
102+
*/
103+
isCopyableAsImage(widget) {
104+
if (widget.model.type === this.widgetFactorySvc.widgetTypes.CHART) {
105+
let chartType = this.chartWrapperSvc.allChartsIndex[widget.model.chart.chartType];
106+
goog.asserts.assert(goog.isDefAndNotNull(chartType));
107+
108+
if (chartType.canScreenshot === true) {
109+
return true;
110+
}
111+
}
112+
113+
return false;
114+
}
115+
67116
/**
68117
* Returns true if the widget is scrollable, otherwise false.
69118
* @param {!WidgetConfig} widget

client/models/chart_widget_model.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ const ChartType = p3rf.perfkit.explorer.models.ChartType;
7474

7575
/** @constructor */
7676
p3rf.perfkit.explorer.models.ChartState = function() {
77+
/**
78+
* @type {?*}
79+
* @export
80+
*/
81+
this.element = null;
82+
7783
/**
7884
* @type {?number}
7985
* @export

0 commit comments

Comments
 (0)