Repository: getredash/redash
Pull request: Custom JS code chart improvements
This PR introduces some fixes and changes to custom code chart visualization.

1. Fix: Currently, custom js code textarea does not load the saved code properly. This fixes the issue by setting the textarea contents only if the content is undefined.

2. Fix: Any update of the graph in editor causes appended trace. Trace gets added because the plotly graph is not initialized. Now data and calcdata will be initialized by setting them to [].

3. Change: Make textarea vertically resizable for readability and editability of code.

4. Change: Add toggle checkbox for auto-update graph. This is handy when you are dealing with big dataset which takes time to refresh.

This fixes #1628.

Problem statement:
Loading custom JS chart into editor fails.
### Issue Summary

Saved custom JS chart visualization does not load in editor.

### Steps to Reproduce

1. Select Custom Plotly chart and select xAxis and a yAxis.
 I chose `select ID, org_id from events LIMIT 10` from Redash metadata.

2. Paste the following to Custom Code.
 Line chart correctly renders.
```
Plotly.plot( element, [{
    x: x,
    y: ys }]);
```

3. Save

4. Click Edit.
 The error is raised and I see default template comments in Custom Code. Am I doing something wrong?
```
angular.js?3437:13920 SyntaxError: Unexpected end of input
    at refresh (eval at <anonymous> (app.d5b5662….js:1196), <anonymous>:501:92)
    at eval (eval at <anonymous> (app.d5b5662….js:1196), <anonymous>:527:9)
    at Scope.$digest (eval at <anonymous> (vendor.d6c0067….js:486), <anonymous>:17524:23)
    at ChildScope.$apply (eval at <anonymous> (vendor.d6c0067….js:486), <anonymous>:17790:24)
    at HTMLButtonElement.eval (eval at <anonymous> (vendor.d6c0067….js:486), <anonymous>:25890:23)
    at defaultHandlerWrapper (eval at <anonymous> (vendor.d6c0067….js:486), <anonymous>:3497:11)
    at HTMLButtonElement.eventHandler (eval at <anonymous> (vendor.d6c0067….js:486), <anonymous>:3485:9)
(anonymous) @ angular.js?3437:13920
(anonymous) @ angular.js?3437:10467
$digest @ angular.js?3437:17542
$apply @ angular.js?3437:17790
(anonymous) @ angular.js?3437:25890
defaultHandlerWrapper @ angular.js?3437:3497
eventHandler @ angular.js?3437:3485
angular.js?3437:13920 SyntaxError: Unexpected end of input
    at refresh
```


### Technical details:

* Redash Version: 1.0.0 pulled from HEAD on Feb. 22nd
* Browser/OS: Chrome
* How did you install Redash: Docker with dev setup

Hints from issue discussion:

Commit message: Fix: Custom code keeps appending trace per refresh

Patch to review:
diff --git a/client/app/assets/css/superflat_redash.css b/client/app/assets/css/superflat_redash.css
index b30dd11a64..b39b72bf74 100644
--- a/client/app/assets/css/superflat_redash.css
+++ b/client/app/assets/css/superflat_redash.css
@@ -1781,6 +1781,9 @@ fieldset[disabled] .form-control {
 textarea.form-control {
   height: auto;
 }
+textarea.v-resizable {
+  resize: vertical;
+}
 input[type="search"] {
   -webkit-appearance: none;
 }
diff --git a/client/app/visualizations/chart/chart-editor.html b/client/app/visualizations/chart/chart-editor.html
index 80b23a88d5..f29f35ac5f 100644
--- a/client/app/visualizations/chart/chart-editor.html
+++ b/client/app/visualizations/chart/chart-editor.html
@@ -134,17 +134,23 @@
 
   <div class="form-group" ng-if="options.globalSeriesType == 'custom'">
     <label class="control-label">Custom code</label>
-    <textarea ng-model="options.customCode" class="form-control" rows="10">
+    <textarea ng-model="options.customCode" class="form-control v-resizable" rows="10">
     </textarea>
   </div>
 
   <div class="checkbox" ng-if="options.globalSeriesType == 'custom'">
-      <label>
-        <input type="checkbox" ng-model="options.enableConsoleLogs">
-        <i class="input-helper"></i> Show errors in the console
-      </label>
-    </div>
+    <label>
+      <input type="checkbox" ng-model="options.enableConsoleLogs">
+      <i class="input-helper"></i> Show errors in the console
+    </label>
+  </div>
 
+  <div class="checkbox" ng-if="options.globalSeriesType == 'custom'">
+    <label>
+      <input type="checkbox" ng-model="options.autoRedraw">
+      <i class="input-helper"></i> Auto update graph
+    </label>
+  </div>
 
   <div ng-show="currentTab == 'xAxis'">
     <div class="form-group">
diff --git a/client/app/visualizations/chart/index.js b/client/app/visualizations/chart/index.js
index b9f93b9074..ea7d519ee0 100644
--- a/client/app/visualizations/chart/index.js
+++ b/client/app/visualizations/chart/index.js
@@ -87,10 +87,12 @@ function ChartEditor(ColorPalette, clientConfig) {
 
       scope.showSizeColumnPicker = () => some(scope.options.seriesOptions, options => options.type === 'bubble');
 
-      scope.options.customCode = `// Available variables are x, ys, element, and Plotly
+      if (scope.options.customCode === undefined) {
+        scope.options.customCode = `// Available variables are x, ys, element, and Plotly
 // Type console.log(x, ys); for more info about x and ys
 // To plot your graph call Plotly.plot(element, ...)
 // Plotly examples and docs: https://plot.ly/javascript/`;
+      }
 
       function refreshColumns() {
         scope.columns = scope.queryResult.getColumns();
diff --git a/client/app/visualizations/chart/plotly.js b/client/app/visualizations/chart/plotly.js
index e94e97cf4a..6049c14fee 100644
--- a/client/app/visualizations/chart/plotly.js
+++ b/client/app/visualizations/chart/plotly.js
@@ -499,6 +499,9 @@ const CustomPlotlyChart = (clientConfig) => {
         return;
       }
       const refresh = () => {
+        // Clear existing data
+        element[0].children[0].calcdata = [];
+        element[0].children[0].data = [];
         // eslint-disable-next-line no-eval
         const codeCall = eval(`codeCall = function(x, ys, element, Plotly){ ${scope.options.customCode} }`);
         codeCall(scope.x, scope.ys, element[0].children[0], Plotly);
@@ -514,9 +517,11 @@ const CustomPlotlyChart = (clientConfig) => {
           });
         });
       };
-      scope.$watch('options.customCode', () => {
+      scope.$watch('[options.customCode, options.autoRedraw]', () => {
         try {
-          refresh();
+          if (scope.options.autoRedraw) {
+            refresh();
+          }
         } catch (err) {
           if (scope.options.enableConsoleLogs) {
             // eslint-disable-next-line no-console


Code review comments:
diff_hunk: @@ -499,6 +499,9 @@ const CustomPlotlyChart = (clientConfig) => {
         return;
       }
       const refresh = () => {
+        // Clear existing data
+        element[0].children[0].calcdata = [];
+        element[0].children[0].data = [];
path: client/app/visualizations/chart/plotly.js
text: @user1:
Where is this used?

@author:
This piece is related to 

> 2. Fix: Any update of the graph in editor causes appended trace.

and used by

```
        const codeCall = eval(`codeCall = function(x, ys, element, Plotly){ ${scope.options.customCode} }`);
        codeCall(scope.x, scope.ys, element[0].children[0], Plotly);
```

The code above appends a trace to an already existing plot. So without clearing `data` from the already drawn plotly graph, every update in code editor will cause the trace in the graph to multiply. One-trace bar graph becomes two-trace bar graph, three-trace bar graph, and so on, each time you type in code editor.

Using Plotly.newPlot() might be another option to avoid appending, but haven't tried it yet.

@author:
Please see https://github.com/getredash/redash/pull/1873/commits/1a8078ab0345108ef91ca5a17835b3c6816f15ba .

This less hackish way fixes the problem too.