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: