diff --git a/manage.py b/manage.py index fe0d961e..7385e7b9 100755 --- a/manage.py +++ b/manage.py @@ -65,4 +65,8 @@ def drop_tables(): manager.add_command("database", database_manager) if __name__ == '__main__': + channel = logging.StreamHandler() + logging.getLogger().addHandler(channel) + logging.getLogger().setLevel(settings.LOG_LEVEL) + manager.run() \ No newline at end of file diff --git a/migrations/create_visualizations.py b/migrations/create_visualizations.py index 3f743d75..9eb2a64d 100644 --- a/migrations/create_visualizations.py +++ b/migrations/create_visualizations.py @@ -4,7 +4,7 @@ from redash import db from redash import models if __name__ == '__main__': - default_options = {"series": {"type": "bar"}} + default_options = {"series": {"type": "column"}} db.connect_db() diff --git a/rd_ui/app/scripts/controllers.js b/rd_ui/app/scripts/controllers.js index 9ec09e6e..5bd29108 100644 --- a/rd_ui/app/scripts/controllers.js +++ b/rd_ui/app/scripts/controllers.js @@ -112,6 +112,9 @@ } else { // TODO: replace this with a safer method $location.path($location.path().replace(oldId, q.id)).replace(); + + // Reset visualizations tab to table after duplicating a query: + $location.hash('table'); } } }, function(httpResponse) { diff --git a/rd_ui/app/scripts/directives.js b/rd_ui/app/scripts/directives.js index a1b85e91..abc7be70 100644 --- a/rd_ui/app/scripts/directives.js +++ b/rd_ui/app/scripts/directives.js @@ -62,9 +62,10 @@ 'Cohort': Visualization.prototype.TYPES.COHORT }; scope.seriesTypes = { - 'Line': Visualization.prototype.SERIES_TYPES.LINE, - 'Bar': Visualization.prototype.SERIES_TYPES.BAR, - 'Area': Visualization.prototype.SERIES_TYPES.AREA + 'Line': 'line', + 'Column': 'column', + 'Area': 'area', + 'Scatter': 'scatter' }; if (!scope.vis) { @@ -77,7 +78,7 @@ 'query_id': q.id, 'type': Visualization.prototype.TYPES.CHART, 'name': '', - 'description': q.description, + 'description': q.description || '', 'options': newOptions() }; } @@ -93,7 +94,7 @@ // Chart return { 'series': { - 'type': Visualization.prototype.SERIES_TYPES.LINE + 'type': scope.seriesTypes[0] } }; } diff --git a/rd_ui/app/scripts/ng-highchart.js b/rd_ui/app/scripts/ng-highchart.js index 701a2313..4faecc67 100644 --- a/rd_ui/app/scripts/ng-highchart.js +++ b/rd_ui/app/scripts/ng-highchart.js @@ -1,19 +1,31 @@ -(function(){ +(function () { 'use strict'; var defaultOptions = { title: { "text": null }, + xAxis: { + type: 'datetime' + }, + yAxis: { + title: { + text: null + } + }, tooltip: { valueDecimals: 2, formatter: function () { + if (!this.points) { + this.points = [this.point]; + }; + if (moment.isMoment(this.x)) { var s = '' + moment(this.x).format("DD/MM/YY HH:mm") + '', pointsCount = this.points.length; $.each(this.points, function (i, point) { - s += '
' + point.series.name + ': ' + + s += '
' + point.series.name + ': ' + Highcharts.numberFormat(point.y); if (pointsCount > 1 && point.percentage) { @@ -23,7 +35,7 @@ } else { var s = "" + this.points[0].key + ""; $.each(this.points, function (i, point) { - s+= '
' + point.series.name + ': ' + + s += '
' + point.series.name + ': ' + Highcharts.numberFormat(point.y); }); } @@ -32,14 +44,6 @@ }, shared: true }, - xAxis: { - type: 'datetime' - }, - yAxis: { - title: { - text: null - } - }, exporting: { chartOptions: { title: { @@ -70,12 +74,50 @@ enabled: false }, plotOptions: { - "column": { - "stacking": "normal", - "pointPadding": 0, - "borderWidth": 1, - "groupPadding": 0, - "shadow": false + area: { + marker: { + enabled: false, + symbol: 'circle', + radius: 2, + states: { + hover: { + enabled: true + } + } + } + }, + column: { + stacking: "normal", + pointPadding: 0, + borderWidth: 1, + groupPadding: 0, + shadow: false + }, + line: { + marker: { + radius: 3, + }, + lineWidth: 1, + states: { + hover: { + lineWidth: 2 + } + } + }, + scatter: { + marker: { + radius: 5, + states: { + hover: { + enabled: true, + lineColor: 'rgb(100,100,100)' + } + } + }, + tooltip: { + headerFormat: '{series.name}
', + pointFormat: '{point.x}, {point.y}' + } } }, series: [] @@ -105,26 +147,34 @@ var chartOptions = $.extend(true, {}, defaultOptions, chartsDefaults); - // Update when options change - scope.$watch('options', function(newOptions) { - initChart(newOptions); - }, true); + // $timeout makes sure that this function invoked after the DOM ready. When draw/init + // invoked after the DOM is ready, we see first an empty HighCharts objects and later + // they get filled up. Which gives the feeling that the charts loading faster (otherwise + // we stare at an empty screen until the HighCharts object is ready). + $timeout(function(){ + // Update when options change + scope.$watch('options', function (newOptions) { + initChart(newOptions); + }, true); - //Update when charts data changes - scope.$watch(function () { - return (scope.series && scope.series.length) || 0; - }, function (length) { - if (!length || length == 0) { - scope.chart.showLoading(); - } else { - drawChart(); - }; - }, true); + //Update when charts data changes + scope.$watch(function () { + // TODO: this might be an issue in case the series change, but they stay + // with the same length + return (scope.series && scope.series.length) || 0; + }, function (length) { + if (!length || length == 0) { + scope.chart.showLoading(); + } else { + drawChart(); + }; + }, true); + }); function initChart(options) { if (scope.chart) { - scope.chart.destroy(); - } + scope.chart.destroy(); + }; $.extend(true, chartOptions, options); @@ -133,23 +183,25 @@ } function drawChart() { - while(scope.chart.series.length > 0) { - scope.chart.series[0].remove(true); - } + while (scope.chart.series.length > 0) { + scope.chart.series[0].remove(false); + }; - // todo series.type - - if (_.some(scope.series[0].data, function(p) { return angular.isString(p.x) })) { + if (_.some(scope.series[0].data, function (p) { + return angular.isString(p.x) + })) { scope.chart.xAxis[0].update({type: 'category'}); // We need to make sure that for each category, each series has a value. - var categories = _.union.apply(this, _.map(scope.series, function(s) { return _.pluck(s.data,'x')})); + var categories = _.union.apply(this, _.map(scope.series, function (s) { + return _.pluck(s.data, 'x') + })); - _.each(scope.series, function(s) { + _.each(scope.series, function (s) { // TODO: move this logic to Query#getChartData var yValues = _.groupBy(s.data, 'x'); - var newData = _.sortBy(_.map(categories, function(category) { + var newData = _.sortBy(_.map(categories, function (category) { return { name: category, y: yValues[category] && yValues[category][0].y @@ -164,11 +216,27 @@ scope.chart.counters.color = 0; - _.each(scope.series, function(s) { + _.each(scope.series, function (s) { // here we override the series with the visualization config - var _s = $.extend(true, {}, s, chartOptions['series']); - scope.chart.addSeries(_s); - }) + s = _.extend(s, chartOptions['series']); + + if (s.type == 'area') { + _.each(s.data, function (p) { + // This is an insane hack: somewhere deep in HighChart's code, + // when you stack areas, it tries to convert the string representation + // of point's x into a number. With the default implementation of toString + // it fails.... + + if (moment.isMoment(p.x)) { + p.x.toString = function () { + return String(this.toDate().getTime()); + }; + } + }); + }; + + scope.chart.addSeries(s, false); + }); scope.chart.redraw(); scope.chart.hideLoading(); diff --git a/rd_ui/app/scripts/services.js b/rd_ui/app/scripts/services.js index 092412b7..7216c2d2 100644 --- a/rd_ui/app/scripts/services.js +++ b/rd_ui/app/scripts/services.js @@ -291,11 +291,6 @@ 'CHART': 'CHART', 'COHORT': 'COHORT', 'TABLE': 'TABLE' - }, - SERIES_TYPES: { - 'LINE': 'line', - 'BAR': 'bar', - 'AREA': 'area' } }; diff --git a/redash/controllers.py b/redash/controllers.py index 15d77c95..f6261407 100644 --- a/redash/controllers.py +++ b/redash/controllers.py @@ -209,7 +209,7 @@ class QueryAPI(BaseResource): query = models.Query.get_by_id(query_id) - return query.to_dict(with_result=False) + return query.to_dict(with_result=False, with_visualizations=True) def get(self, query_id): q = models.Query.get(models.Query.id == query_id) diff --git a/redash/models.py b/redash/models.py index d9378d63..15c57b04 100644 --- a/redash/models.py +++ b/redash/models.py @@ -64,7 +64,7 @@ class Query(BaseModel): def create_default_visualizations(self): table_visualization = Visualization(query=self, name="Table", - description=self.description, + description='', type="TABLE", options="{}") table_visualization.save() @@ -185,7 +185,7 @@ class Visualization(BaseModel): type = peewee.CharField(max_length=100) query = peewee.ForeignKeyField(Query, related_name='visualizations') name = peewee.CharField(max_length=255) - description = peewee.CharField(max_length=4096) + description = peewee.CharField(max_length=4096, null=True) options = peewee.TextField() class Meta: