diff --git a/rd_ui/app/index.html b/rd_ui/app/index.html
index 004da809..05145c8c 100644
--- a/rd_ui/app/index.html
+++ b/rd_ui/app/index.html
@@ -111,6 +111,10 @@
+
+
+
+
diff --git a/rd_ui/app/scripts/app.js b/rd_ui/app/scripts/app.js
index 04718e98..52ac7029 100644
--- a/rd_ui/app/scripts/app.js
+++ b/rd_ui/app/scripts/app.js
@@ -5,6 +5,7 @@ angular.module('redash', [
'redash.filters',
'redash.services',
'redash.renderers',
+ 'redash.visualization',
'ui.codemirror',
'highchart',
'angular-growl',
diff --git a/rd_ui/app/scripts/directives.js b/rd_ui/app/scripts/directives.js
index 6f171989..d9d3fa47 100644
--- a/rd_ui/app/scripts/directives.js
+++ b/rd_ui/app/scripts/directives.js
@@ -3,7 +3,7 @@
var directives = angular.module('redash.directives', []);
- directives.directive('rdTab', ['$location', function($location) {
+ directives.directive('rdTab', function() {
return {
restrict: 'E',
scope: {
@@ -19,9 +19,9 @@
});
}
}
- }]);
+ });
- directives.directive('rdTabs', ['$location', '$rootScope', function($location, $rootScope) {
+ directives.directive('rdTabs', ['$location', function($location) {
return {
restrict: 'E',
scope: {
@@ -46,130 +46,6 @@
}
}]);
- directives.directive('editVisulatizationForm', ['Visualization', 'growl', function(Visualization, growl) {
- return {
- restrict: 'E',
- templateUrl: '/views/edit_visualization.html',
- replace: true,
- scope: {
- query: '=',
- vis: '=?'
- },
- link: function(scope, element, attrs) {
- scope.advancedMode = false;
- scope.visTypes = {
- 'Chart': Visualization.prototype.TYPES.CHART,
- 'Cohort': Visualization.prototype.TYPES.COHORT
- };
- scope.seriesTypes = {
- 'Line': 'line',
- 'Column': 'column',
- 'Area': 'area',
- 'Scatter': 'scatter',
- 'Pie': 'pie'
- };
-
- scope.stackingOptions = {
- "None": "none",
- "Normal": "normal",
- "Percent": "percent"
- };
-
- scope.stacking = "none";
-
- if (!scope.vis) {
- // create new visualization
- // wait for query to load to populate with defaults
- var unwatch = scope.$watch('query', function(q) {
- if (q && q.id) {
- unwatch();
-
- if (!scope.vis) {
- scope.vis = {
- 'query_id': q.id,
- 'type': Visualization.prototype.TYPES.CHART
- };
- }
- }
- }, true);
- }
-
- function newOptions(chartType) {
- if (chartType === Visualization.prototype.TYPES.CHART) {
- return {
- 'series': {
- 'type': 'column',
- 'stacking': null
- }
- };
- };
-
- return {};
- }
-
- var chartOptionsUnwatch = null;
-
- scope.$watch('vis.type', function(type) {
- // if not edited by user, set name to match type
- if (type && scope.vis && !scope.visForm.name.$dirty) {
- // poor man's titlecase
- scope.vis.name = scope.vis.type[0] + scope.vis.type.slice(1).toLowerCase();
- }
-
- if (type && type == Visualization.prototype.TYPES.CHART) {
- if (scope.vis.options.series.stacking === null) {
- scope.stacking = "none";
- } else if (scope.vis.options.series.stacking === undefined) {
- scope.stacking = "normal";
- } else {
- scope.stacking = scope.vis.options.series.stacking ;
- }
-
- chartOptionsUnwatch = scope.$watch("stacking", function(stacking) {
- if (stacking == "none") {
- scope.vis.options.series.stacking = null;
- } else {
- scope.vis.options.series.stacking = stacking;
- }
- });
- } else {
- if (chartOptionsUnwatch) {
- chartOptionsUnwatch();
- chartOptionsUnwatch = null;
- }
- }
- });
-
- scope.toggleAdvancedMode = function() {
- scope.advancedMode = !scope.advancedMode;
- };
-
- scope.typeChanged = function() {
- scope.vis.options = newOptions(scope.vis.type);
- };
-
- scope.submit = function() {
- Visualization.save(scope.vis, function success(result) {
- growl.addSuccessMessage("Visualization saved");
-
- scope.vis = result;
-
- var visIds = _.pluck(scope.query.visualizations, 'id');
- var index = visIds.indexOf(result.id);
-
- if (index > -1) {
- scope.query.visualizations[index] = result;
- } else {
- scope.query.visualizations.push(result);
- }
- }, function error() {
- growl.addErrorMessage("Visualization could not be saved");
- });
- };
- }
- }
- }]);
-
directives.directive('editDashboardForm', ['$http', '$location', '$timeout', 'Dashboard', function($http, $location, $timeout, Dashboard) {
return {
restrict: 'E',
@@ -281,6 +157,7 @@
$scope.widgetSize = 1;
$scope.queryId = null;
$scope.selectedVis = null;
+ $scope.query = null;
}
diff --git a/rd_ui/app/scripts/query_fiddle/renderers.js b/rd_ui/app/scripts/query_fiddle/renderers.js
index e747ac18..1eb965e1 100644
--- a/rd_ui/app/scripts/query_fiddle/renderers.js
+++ b/rd_ui/app/scripts/query_fiddle/renderers.js
@@ -1,134 +1,5 @@
var renderers = angular.module('redash.renderers', []);
-renderers.directive('visualizationRenderer', function() {
- return {
- restrict: 'E',
- scope: {
- visualization: '=',
- queryResult: '='
- },
- template: '
' +
- ' ' +
- ' ' +
- ' ' +
- '
',
- replace: false
- }
-});
-
-renderers.directive('chartRenderer', function () {
- return {
- restrict: 'E',
- scope: {
- queryResult: '=',
- options: '=?'
- },
- template: " ",
- replace: false,
- controller: ['$scope', function ($scope) {
- $scope.chartSeries = [];
- $scope.chartOptions = {};
-
- $scope.$watch('options', function(chartOptions) {
- if (chartOptions) {
- $scope.chartOptions = chartOptions;
- }
- });
- $scope.$watch('queryResult && queryResult.getData()', function (data) {
- if (!data || $scope.queryResult.getData() == null) {
- $scope.chartSeries.splice(0, $scope.chartSeries.length);
- } else {
- $scope.chartSeries.splice(0, $scope.chartSeries.length);
-
- _.each($scope.queryResult.getChartData(), function (s) {
- $scope.chartSeries.push(_.extend(s, {'stacking': 'normal'}));
- });
- }
- });
- }]
- }
-})
-
-renderers.directive('gridRenderer', function () {
- return {
- restrict: 'E',
- scope: {
- queryResult: '=',
- itemsPerPage: '='
- },
- templateUrl: "/views/grid_renderer.html",
- replace: false,
- controller: ['$scope', function ($scope) {
- $scope.gridColumns = [];
- $scope.gridData = [];
- $scope.gridConfig = {
- isPaginationEnabled: true,
- itemsByPage: $scope.itemsPerPage || 15,
- maxSize: 8
- };
-
- $scope.$watch('queryResult && queryResult.getData()', function (data) {
- if (!data) {
- return;
- }
-
- if ($scope.queryResult.getData() == null) {
- $scope.gridColumns = [];
- $scope.gridData = [];
- $scope.filters = [];
- } else {
-
-
- $scope.filters = $scope.queryResult.getFilters();
-
- var gridData = _.map($scope.queryResult.getData(), function (row) {
- var newRow = {};
- _.each(row, function (val, key) {
- newRow[$scope.queryResult.getColumnCleanName(key)] = val;
- })
- return newRow;
- });
-
- $scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) {
- var columnDefinition = {
- 'label': $scope.queryResult.getColumnFriendlyNames()[i],
- 'map': col
- };
-
- if (gridData.length > 0) {
- var exampleData = gridData[0][col];
- if (angular.isNumber(exampleData)) {
- columnDefinition['formatFunction'] = 'number';
- columnDefinition['formatParameter'] = 2;
- } else if (moment.isMoment(exampleData)) {
- columnDefinition['formatFunction'] = function(value) {
- return value.format("DD/MM/YY HH:mm");
- }
- }
- }
-
- return columnDefinition;
- });
-
- $scope.gridData = _.clone(gridData);
-
- $scope.$watch('filters', function (filters) {
- $scope.gridData = _.filter(gridData, function (row) {
- return _.reduce(filters, function (memo, filter) {
- if (filter.current == 'All') {
- return memo && true;
- }
-
- return (memo && row[$scope.queryResult.getColumnCleanName(filter.name)] == filter.current);
- }, true);
- });
- }, true);
- }
- });
- }]
- }
-})
-
renderers.directive('pivotTableRenderer', function () {
return {
restrict: 'E',
@@ -152,52 +23,4 @@ renderers.directive('pivotTableRenderer', function () {
});
}
}
-})
-
-renderers.directive('cohortRenderer', function() {
- return {
- restrict: 'E',
- scope: {
- queryResult: '='
- },
- template: "",
- replace: false,
- link: function($scope, element, attrs) {
- $scope.$watch('queryResult && queryResult.getData()', function (data) {
- if (!data) {
- return;
- }
-
- if ($scope.queryResult.getData() == null) {
-
- } else {
- var sortedData = _.sortBy($scope.queryResult.getData(), "date");
- var grouped = _.groupBy(sortedData, "date");
- var data = _.map(grouped, function(values, date) {
- var row = [values[0].total];
- _.each(values, function(value) { row.push(value.value); });
- return row;
- });
-
- var initialDate = moment(sortedData[0].date).toDate(),
- container = angular.element(element)[0];
-
- Cornelius.draw({
- initialDate: initialDate,
- container: container,
- cohort: data,
- title: null,
- timeInterval: 'daily',
- labels: {
- time: 'Activation Day',
- people: 'Users'
- },
- formatHeaderLabel: function (i) {
- return "Day " + (i - 1);
- }
- });
- }
- });
- }
- }
-})
+});
\ No newline at end of file
diff --git a/rd_ui/app/scripts/services.js b/rd_ui/app/scripts/services.js
index 7216c2d2..cf7efc58 100644
--- a/rd_ui/app/scripts/services.js
+++ b/rd_ui/app/scripts/services.js
@@ -283,23 +283,7 @@
return Query;
};
- var Visualization = function($resource) {
- var Visualization = $resource('/api/visualizations/:id', {id: '@id'});
-
- Visualization.prototype = {
- TYPES: {
- 'CHART': 'CHART',
- 'COHORT': 'COHORT',
- 'TABLE': 'TABLE'
- }
- };
-
- return Visualization;
- };
-
angular.module('redash.services', [])
.factory('QueryResult', ['$resource', '$timeout', QueryResult])
.factory('Query', ['$resource', 'QueryResult', Query])
- .factory('Visualization', ['$resource', Visualization])
-
})();
diff --git a/rd_ui/app/scripts/visualizations/base.js b/rd_ui/app/scripts/visualizations/base.js
new file mode 100644
index 00000000..7d9e5071
--- /dev/null
+++ b/rd_ui/app/scripts/visualizations/base.js
@@ -0,0 +1,149 @@
+(function () {
+ var VisualizationProvider = function() {
+ this.visualizations = {};
+ this.visualizationTypes = {};
+ var defaultConfig = {
+ defaultOptions: {},
+ skipTypes: false,
+ editorTemplate: null
+ }
+
+ this.registerVisualization = function(config) {
+ var visualization = _.extend({}, defaultConfig, config);
+
+ // TODO: this is prone to errors; better refactor.
+ if (_.isEmpty(this.visualizations)) {
+ this.defaultVisualization = visualization;
+ }
+
+ this.visualizations[config.type] = visualization;
+
+ if (!config.skipTypes) {
+ this.visualizationTypes[config.name] = config.type;
+ };
+ };
+
+ this.getSwitchTemplate = function(property) {
+ var pattern = /(<[a-zA-Z0-9-]*?)( |>)/
+
+ var mergedTemplates = _.reduce(this.visualizations, function(templates, visualization) {
+ if (visualization[property]) {
+ var ngSwitch = '$1 ng-switch-when="' + visualization.type + '" $2';
+ var template = visualization[property].replace(pattern, ngSwitch);
+
+ return templates + "\n" + template;
+ }
+
+ return templates;
+ }, "");
+
+ mergedTemplates = ''+ mergedTemplates + "
";
+
+ return mergedTemplates;
+ }
+
+ this.$get = ['$resource', function($resource) {
+ var Visualization = $resource('/api/visualizations/:id', {id: '@id'});
+ Visualization.visualizations = this.visualizations;
+ Visualization.visualizationTypes = this.visualizationTypes;
+ Visualization.renderVisualizationsTemplate = this.getSwitchTemplate('renderTemplate');
+ Visualization.editorTemplate = this.getSwitchTemplate('editorTemplate');
+ Visualization.defaultVisualization = this.defaultVisualization;
+
+ return Visualization;
+ }];
+ };
+
+ var VisualizationRenderer = function(Visualization) {
+ return {
+ restrict: 'E',
+ scope: {
+ visualization: '=',
+ queryResult: '='
+ },
+ // TODO: using switch here (and in the options editor) might introduce errors and bad
+ // performance wise. It's better to eventually show the correct template based on the
+ // visualization type and not make the browser render all of them.
+ template: Visualization.renderVisualizationsTemplate,
+ replace: false
+ }
+ };
+
+ var VisualizationOptionsEditor = function(Visualization) {
+ return {
+ restrict: 'E',
+ template: Visualization.editorTemplate,
+ replace: false
+ }
+ };
+
+ var EditVisualizationForm = function(Visualization, growl) {
+ return {
+ restrict: 'E',
+ templateUrl: '/views/visualizations/edit_visualization.html',
+ replace: true,
+ scope: {
+ query: '=',
+ queryResult: '=',
+ visualization: '=?'
+ },
+ link: function (scope, element, attrs) {
+ scope.visTypes = Visualization.visualizationTypes;
+
+ scope.newVisualization = function(q) {
+ return {
+ 'query_id': q.id,
+ 'type': Visualization.defaultVisualization.type,
+ 'name': Visualization.defaultVisualization.name,
+ 'description': q.description || '',
+ 'options': Visualization.defaultVisualization.defaultOptions
+ };
+ }
+
+ if (!scope.visualization) {
+ // create new visualization
+ // wait for query to load to populate with defaults
+ var unwatch = scope.$watch('query', function (q) {
+ if (q && q.id) {
+ unwatch();
+
+ scope.visualization = scope.newVisualization(q);
+ }
+ }, true);
+ }
+
+ scope.$watch('visualization.type', function (type) {
+ // if not edited by user, set name to match type
+ if (type && scope.visualization && !scope.visForm.name.$dirty) {
+ // poor man's titlecase
+ scope.visualization.name = scope.visualization.type[0] + scope.visualization.type.slice(1).toLowerCase();
+ }
+ });
+
+ scope.submit = function () {
+ Visualization.save(scope.visualization, function success(result) {
+ growl.addSuccessMessage("Visualization saved");
+
+ scope.visualization = scope.newVisualization(scope.query);
+
+ var visIds = _.pluck(scope.query.visualizations, 'id');
+ var index = visIds.indexOf(result.id);
+ if (index > -1) {
+ scope.query.visualizations[index] = result;
+ } else {
+ scope.query.visualizations.push(result);
+ }
+ }, function error() {
+ growl.addErrorMessage("Visualization could not be saved");
+ });
+ };
+ }
+ }
+ };
+
+ angular.module('redash.visualization', [])
+ .provider('Visualization', VisualizationProvider)
+ .directive('visualizationRenderer', ['Visualization', VisualizationRenderer])
+ .directive('visualizationOptionsEditor', ['Visualization', VisualizationOptionsEditor])
+ .directive('editVisulatizationForm', ['Visualization', 'growl', EditVisualizationForm])
+})();
diff --git a/rd_ui/app/scripts/visualizations/chart.js b/rd_ui/app/scripts/visualizations/chart.js
new file mode 100644
index 00000000..60ef8940
--- /dev/null
+++ b/rd_ui/app/scripts/visualizations/chart.js
@@ -0,0 +1,106 @@
+(function () {
+ var chartVisualization = angular.module('redash.visualization');
+
+ chartVisualization.config(['VisualizationProvider', function(VisualizationProvider) {
+ var renderTemplate = ' ';
+ var editTemplate = ' ';
+ var defaultOptions = {
+ 'series': {
+ 'type': 'column',
+ 'stacking': null
+ }
+ };
+
+ VisualizationProvider.registerVisualization({
+ type: 'CHART',
+ name: 'Chart',
+ renderTemplate: renderTemplate,
+ editorTemplate: editTemplate,
+ defaultOptions: defaultOptions
+ });
+ }]);
+
+ chartVisualization.directive('chartRenderer', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ queryResult: '=',
+ options: '=?'
+ },
+ template: " ",
+ replace: false,
+ controller: ['$scope', function ($scope) {
+ $scope.chartSeries = [];
+ $scope.chartOptions = {};
+
+ $scope.$watch('options', function(chartOptions) {
+ if (chartOptions) {
+ $scope.chartOptions = chartOptions;
+ }
+ });
+ $scope.$watch('queryResult && queryResult.getData()', function (data) {
+ if (!data || $scope.queryResult.getData() == null) {
+ $scope.chartSeries.splice(0, $scope.chartSeries.length);
+ } else {
+ $scope.chartSeries.splice(0, $scope.chartSeries.length);
+
+ _.each($scope.queryResult.getChartData(), function (s) {
+ $scope.chartSeries.push(_.extend(s, {'stacking': 'normal'}));
+ });
+ }
+ });
+ }]
+ }
+ });
+
+ chartVisualization.directive('chartEditor', function () {
+ return {
+ restrict: 'E',
+ templateUrl: '/views/visualizations/chart_editor.html',
+ link: function (scope, element, attrs) {
+ scope.seriesTypes = {
+ 'Line': 'line',
+ 'Column': 'column',
+ 'Area': 'area',
+ 'Scatter': 'scatter',
+ 'Pie': 'pie'
+ };
+
+ scope.stackingOptions = {
+ "None": "none",
+ "Normal": "normal",
+ "Percent": "percent"
+ };
+
+ scope.stacking = "none";
+
+ var chartOptionsUnwatch = null;
+
+ scope.$watch('visualization', function (visualization) {
+ if (visualization && visualization.type == 'CHART') {
+ if (scope.visualization.options.series.stacking === null) {
+ scope.stacking = "none";
+ } else if (scope.visualization.options.series.stacking === undefined) {
+ scope.stacking = "normal";
+ } else {
+ scope.stacking = scope.visualization.options.series.stacking;
+ }
+
+ chartOptionsUnwatch = scope.$watch("stacking", function (stacking) {
+ if (stacking == "none") {
+ scope.visualization.options.series.stacking = null;
+ } else {
+ scope.visualization.options.series.stacking = stacking;
+ }
+ });
+ } else {
+ if (chartOptionsUnwatch) {
+ chartOptionsUnwatch();
+ chartOptionsUnwatch = null;
+ }
+ }
+ });
+ }
+ }
+ });
+}());
\ No newline at end of file
diff --git a/rd_ui/app/scripts/visualizations/cohort.js b/rd_ui/app/scripts/visualizations/cohort.js
new file mode 100644
index 00000000..4226c4a3
--- /dev/null
+++ b/rd_ui/app/scripts/visualizations/cohort.js
@@ -0,0 +1,60 @@
+(function () {
+ var cohortVisualization = angular.module('redash.visualization');
+
+ cohortVisualization.config(['VisualizationProvider', function(VisualizationProvider) {
+ VisualizationProvider.registerVisualization({
+ type: 'COHORT',
+ name: 'Cohort',
+ renderTemplate: ' '
+ });
+ }]);
+
+ cohortVisualization.directive('cohortRenderer', function() {
+ return {
+ restrict: 'E',
+ scope: {
+ queryResult: '='
+ },
+ template: "",
+ replace: false,
+ link: function($scope, element, attrs) {
+ $scope.$watch('queryResult && queryResult.getData()', function (data) {
+ if (!data) {
+ return;
+ }
+
+ if ($scope.queryResult.getData() == null) {
+
+ } else {
+ var sortedData = _.sortBy($scope.queryResult.getData(), "date");
+ var grouped = _.groupBy(sortedData, "date");
+ var data = _.map(grouped, function(values, date) {
+ var row = [values[0].total];
+ _.each(values, function(value) { row.push(value.value); });
+ return row;
+ });
+
+ var initialDate = moment(sortedData[0].date).toDate(),
+ container = angular.element(element)[0];
+
+ Cornelius.draw({
+ initialDate: initialDate,
+ container: container,
+ cohort: data,
+ title: null,
+ timeInterval: 'daily',
+ labels: {
+ time: 'Activation Day',
+ people: 'Users'
+ },
+ formatHeaderLabel: function (i) {
+ return "Day " + (i - 1);
+ }
+ });
+ }
+ });
+ }
+ }
+ });
+
+}());
\ No newline at end of file
diff --git a/rd_ui/app/scripts/visualizations/table.js b/rd_ui/app/scripts/visualizations/table.js
new file mode 100644
index 00000000..98ee06d9
--- /dev/null
+++ b/rd_ui/app/scripts/visualizations/table.js
@@ -0,0 +1,92 @@
+(function () {
+ var tableVisualization = angular.module('redash.visualization');
+
+ tableVisualization.config(['VisualizationProvider', function(VisualizationProvider) {
+ VisualizationProvider.registerVisualization({
+ type: 'TABLE',
+ name: 'Table',
+ renderTemplate: ' ',
+ skipTypes: true
+ });
+ }]);
+
+ tableVisualization.directive('gridRenderer', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ queryResult: '=',
+ itemsPerPage: '='
+ },
+ templateUrl: "/views/grid_renderer.html",
+ replace: false,
+ controller: ['$scope', function ($scope) {
+ $scope.gridColumns = [];
+ $scope.gridData = [];
+ $scope.gridConfig = {
+ isPaginationEnabled: true,
+ itemsByPage: $scope.itemsPerPage || 15,
+ maxSize: 8
+ };
+
+ $scope.$watch('queryResult && queryResult.getData()', function (data) {
+ if (!data) {
+ return;
+ }
+
+ if ($scope.queryResult.getData() == null) {
+ $scope.gridColumns = [];
+ $scope.gridData = [];
+ $scope.filters = [];
+ } else {
+
+
+ $scope.filters = $scope.queryResult.getFilters();
+
+ var gridData = _.map($scope.queryResult.getData(), function (row) {
+ var newRow = {};
+ _.each(row, function (val, key) {
+ newRow[$scope.queryResult.getColumnCleanName(key)] = val;
+ })
+ return newRow;
+ });
+
+ $scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) {
+ var columnDefinition = {
+ 'label': $scope.queryResult.getColumnFriendlyNames()[i],
+ 'map': col
+ };
+
+ if (gridData.length > 0) {
+ var exampleData = gridData[0][col];
+ if (angular.isNumber(exampleData)) {
+ columnDefinition['formatFunction'] = 'number';
+ columnDefinition['formatParameter'] = 2;
+ } else if (moment.isMoment(exampleData)) {
+ columnDefinition['formatFunction'] = function(value) {
+ return value.format("DD/MM/YY HH:mm");
+ }
+ }
+ }
+
+ return columnDefinition;
+ });
+
+ $scope.gridData = _.clone(gridData);
+
+ $scope.$watch('filters', function (filters) {
+ $scope.gridData = _.filter(gridData, function (row) {
+ return _.reduce(filters, function (memo, filter) {
+ if (filter.current == 'All') {
+ return memo && true;
+ }
+
+ return (memo && row[$scope.queryResult.getColumnCleanName(filter.name)] == filter.current);
+ }, true);
+ });
+ }, true);
+ }
+ });
+ }]
+ }
+ })
+}());
\ No newline at end of file
diff --git a/rd_ui/app/views/edit_visualization.html b/rd_ui/app/views/edit_visualization.html
deleted file mode 100644
index 2521902f..00000000
--- a/rd_ui/app/views/edit_visualization.html
+++ /dev/null
@@ -1,24 +0,0 @@
-
diff --git a/rd_ui/app/views/queryfiddle.html b/rd_ui/app/views/queryfiddle.html
index 3a51ea07..99b3cf3e 100644
--- a/rd_ui/app/views/queryfiddle.html
+++ b/rd_ui/app/views/queryfiddle.html
@@ -73,7 +73,7 @@
@@ -90,7 +90,7 @@
-
+
diff --git a/rd_ui/app/views/visualizations/chart_editor.html b/rd_ui/app/views/visualizations/chart_editor.html
new file mode 100644
index 00000000..2c36235a
--- /dev/null
+++ b/rd_ui/app/views/visualizations/chart_editor.html
@@ -0,0 +1,7 @@
+
+ Chart Type
+
+
+ Stacking
+
+
\ No newline at end of file
diff --git a/rd_ui/app/views/visualizations/edit_visualization.html b/rd_ui/app/views/visualizations/edit_visualization.html
new file mode 100644
index 00000000..0250bc4b
--- /dev/null
+++ b/rd_ui/app/views/visualizations/edit_visualization.html
@@ -0,0 +1,18 @@
+