diff --git a/TODO.md b/TODO.md index 35e5c47bf2..f5b4dab466 100644 --- a/TODO.md +++ b/TODO.md @@ -20,6 +20,7 @@ List of TODO items for Panoramix An example of a layer might be "holidays" or "site outages", ... * **Worth doing? User defined groups:** People could define mappings in the UI of say "Countries I follow" and apply it to different datasets. For now, this is done by writing CASE-WHEN-type expression which is probably good enough. + ## Easy-ish fix * datasource in explore mode could be a dropdown * Create a set of slices and dashboard on top of the World Bank dataset that ship with load_examples diff --git a/panoramix/migrations/versions/43df8de3a5f4_dash_json.py b/panoramix/migrations/versions/43df8de3a5f4_dash_json.py new file mode 100644 index 0000000000..410fc380b7 --- /dev/null +++ b/panoramix/migrations/versions/43df8de3a5f4_dash_json.py @@ -0,0 +1,21 @@ +"""empty message + +Revision ID: 43df8de3a5f4 +Revises: 7dbf98566af7 +Create Date: 2016-01-18 23:43:16.073483 + +""" + +# revision identifiers, used by Alembic. +revision = '43df8de3a5f4' +down_revision = '7dbf98566af7' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + op.add_column('dashboards', sa.Column('json_metadata', sa.Text(), nullable=True)) + +def downgrade(): + op.drop_column('dashboards', 'json_metadata') diff --git a/panoramix/migrations/versions/7dbf98566af7_slice_description.py b/panoramix/migrations/versions/7dbf98566af7_slice_description.py new file mode 100644 index 0000000000..b29e70d3d4 --- /dev/null +++ b/panoramix/migrations/versions/7dbf98566af7_slice_description.py @@ -0,0 +1,21 @@ +"""empty message + +Revision ID: 7dbf98566af7 +Revises: 8e80a26a31db +Create Date: 2016-01-17 22:00:23.640788 + +""" + +# revision identifiers, used by Alembic. +revision = '7dbf98566af7' +down_revision = '8e80a26a31db' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + op.add_column('slices', sa.Column('description', sa.Text(), nullable=True)) + +def downgrade(): + op.drop_column('slices', 'description') diff --git a/panoramix/models.py b/panoramix/models.py index 63e3690ce4..2b46687208 100644 --- a/panoramix/models.py +++ b/panoramix/models.py @@ -66,6 +66,7 @@ class Slice(Model, AuditMixinNullable): datasource_name = Column(String(2000)) viz_type = Column(String(250)) params = Column(Text) + description = Column(Text) table = relationship( 'SqlaTable', foreign_keys=[table_id], backref='slices') @@ -88,6 +89,10 @@ class Slice(Model, AuditMixinNullable): form_data=d) return viz + @property + def description_markeddown(self): + return utils.markdown(self.description) + @property def datasource_id(self): return self.table_id or self.druid_datasource_id @@ -154,6 +159,7 @@ class Dashboard(Model, AuditMixinNullable): position_json = Column(Text) description = Column(Text) css = Column(Text) + json_metadata = Column(Text) slug = Column(String(255), unique=True) slices = relationship( 'Slice', secondary=dashboard_slices, backref='dashboards') @@ -165,6 +171,10 @@ class Dashboard(Model, AuditMixinNullable): def url(self): return "/panoramix/dashboard/{}/".format(self.slug or self.id) + @property + def metadata_dejson(self): + return json.loads(self.json_metadata) + def dashboard_link(self): return '{self.dashboard_title}'.format(self=self) diff --git a/panoramix/static/panoramix.css b/panoramix/static/panoramix.css index 2637204370..486d97c7fa 100644 --- a/panoramix/static/panoramix.css +++ b/panoramix/static/panoramix.css @@ -11,7 +11,18 @@ html>body{ margin-left: 365px; } +.slice_description{ + padding: 8px; + margin: 5px; + border: 1px solid #DDD; + background-color: #F8F8F8; + border-radius: 5px; + font-size: 12px; +} +.slice_info{ + cursor: pointer; +} .padded{ padding: 10px; @@ -23,7 +34,7 @@ html>body{ } .slice_container { - height: 100%; + //height: 100%; } .container-fluid { text-align: left; diff --git a/panoramix/static/panoramix.js b/panoramix/static/panoramix.js index df585fae7a..4d6c1a7a7a 100644 --- a/panoramix/static/panoramix.js +++ b/panoramix/static/panoramix.js @@ -106,7 +106,13 @@ var px = (function() { return token.width(); }, height: function(){ - return token.height() - 25; + var others = 0; + var widget = container.parents('.widget'); + var slice_description = widget.find('.slice_description'); + if (slice_description.is(":visible")) + others += widget.find('.slice_description').height() + 25; + others += widget.find('.slice_header').height(); + return widget.height() - others; }, render: function() { $('.btn-group.results span').attr('disabled','disabled'); @@ -416,9 +422,17 @@ var px = (function() { }).data('gridster'); $("div.gridster").css('visibility', 'visible'); $("#savedash").click(function() { + var expanded_slices = {}; + $.each($(".slice_info"), function(i, d){ + var widget = $(this).parents('.widget'); + var slice_description = widget.find('.slice_description'); + if(slice_description.is(":visible")) + expanded_slices[$(d).attr('slice_id')] = true; + }); var data = { positions: gridster.serialize(), - css: $("#dash_css").val() + css: $("#dash_css").val(), + expanded_slices: expanded_slices, }; $.ajax({ type: "POST", @@ -432,6 +446,13 @@ var px = (function() { var li = $(this).parents("li"); gridster.remove_widget(li); }); + $(".slice_info").click(function(){ + var widget = $(this).parents('.widget'); + var slice_description = widget.find('.slice_description'); + slice_description.slideToggle(500, function(){ + widget.find('.refresh').click(); + }); + }); $("table.slice_header").mouseover(function() { $(this).find("td.icons nobr").show(); }); diff --git a/panoramix/static/widgets/viz_nvd3.js b/panoramix/static/widgets/viz_nvd3.js index b9ebf14bd4..b2aaaeeb18 100644 --- a/panoramix/static/widgets/viz_nvd3.js +++ b/panoramix/static/widgets/viz_nvd3.js @@ -6,7 +6,7 @@ function viz_nvd3(slice) { $.getJSON(slice.jsonEndpoint(), function(payload) { var fd = payload.form_data; var viz_type = fd.viz_type; - var f = d3.format('.4s'); + var f = d3.format('.3s'); nv.addGraph(function() { if (viz_type === 'line') { if (fd.show_brush) { @@ -49,6 +49,7 @@ function viz_nvd3(slice) { } else if (viz_type === 'pie') { chart = nv.models.pieChart() chart.showLegend(fd.show_legend); + chart.valueFormat(f); if (fd.donut) { chart.donut(true); chart.donutLabelsOutside(true); @@ -99,8 +100,13 @@ function viz_nvd3(slice) { chart.showLegend(fd.show_legend); } - // make space for labels on right - //chart.height($(".chart").height() - 50).margin({"right": 50}); + var height = slice.height(); + if(chart.hasOwnProperty("x2Axis")) { + height += 30; + } + chart.height(height); + slice.container.css('height', height + 'px'); + if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) { chart.useInteractiveGuideline(true); } @@ -122,7 +128,9 @@ function viz_nvd3(slice) { else if (fd.x_axis_format !== undefined) { chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format)); } - if (fd.contribution || fd.num_period_compare) { + if (chart.yAxis !== undefined) + chart.yAxis.tickFormat(d3.format('.3s')); + if (fd.contribution || fd.num_period_compare || viz_type == 'compare') { chart.yAxis.tickFormat(d3.format('.3p')); if (chart.y2Axis != undefined) { chart.y2Axis.tickFormat(d3.format('.3p')); @@ -140,15 +148,9 @@ function viz_nvd3(slice) { d3.select(slice.selector).append("svg") .datum(payload.data) .transition().duration(500) + .attr('height', height) .call(chart); - // if it is a two axis chart, rescale it down just a little so it fits in the div. - if(chart.hasOwnProperty("x2Axis")) { - two_axis_chart = $(slice.selector + " > svg"); - w = two_axis_chart.width(); - h = two_axis_chart.height(); - two_axis_chart.get(0).setAttribute('viewBox', '0 0 '+w+' '+(h+30)); - } return chart; }); slice.done(payload); diff --git a/panoramix/static/widgets/viz_table.js b/panoramix/static/widgets/viz_table.js index 650b20ad51..e6b17cd6c2 100644 --- a/panoramix/static/widgets/viz_table.js +++ b/panoramix/static/widgets/viz_table.js @@ -79,13 +79,13 @@ px.registerViz('table', function(slice) { paging: false, searching: form_data.include_search, }); - slice.container.find('.tooltip').remove(); // Sorting table by main column if (form_data.metrics.length > 0) { var main_metric = form_data.metrics[0]; datatable.column(data.columns.indexOf(main_metric)).order( 'desc' ).draw(); } slice.done(json); + slice.container.parents('.widget').find('.tooltip').remove(); }).fail(function(xhr){ slice.error(xhr.responseText); }); diff --git a/panoramix/templates/panoramix/dashboard.html b/panoramix/templates/panoramix/dashboard.html index c0181a4524..bd196c89d3 100644 --- a/panoramix/templates/panoramix/dashboard.html +++ b/panoramix/templates/panoramix/dashboard.html @@ -91,25 +91,35 @@ body { - -
{{ slice.slice_name }}
+
+ + {{ slice.slice_name }} + {% if slice.description %} + + {% endif %} + +
- +
-
+
+ {{ slice.description_markeddown | safe }} +
+ +
loading -
+
{% endfor %} @@ -131,9 +141,6 @@ body { $('#filters').click( function(){ alert(dashboard.readFilters()); }); - $('a.bug').click( function(){ - console.log(dashboard.getSlice($(this).data('slice_id'))); - }); }); {% endblock %} diff --git a/panoramix/templates/panoramix/explore.html b/panoramix/templates/panoramix/explore.html index 76df90baef..06a03c585d 100644 --- a/panoramix/templates/panoramix/explore.html +++ b/panoramix/templates/panoramix/explore.html @@ -42,11 +42,16 @@ {{ form.get_field("viz_type")(class_="select2") }} - {{ viz.form_data.slice_name }} - - - - + {% if slice %} + {{ slice.slice_name }} + + {% if slice.description %} + + {% endif %} + + + + {% endif %}
@@ -160,7 +165,7 @@ {% include 'appbuilder/flash.html' %}
loading diff --git a/panoramix/templates/panoramix/viz.html b/panoramix/templates/panoramix/viz.html index b9d6eeee47..5a6b0bc721 100644 --- a/panoramix/templates/panoramix/viz.html +++ b/panoramix/templates/panoramix/viz.html @@ -1,6 +1,7 @@ {% if viz.form_data.get("json") == "true" %} {{ viz.get_json() }} {% else %} + {% if viz.request.args.get("standalone") == "true" %} {% extends 'panoramix/standalone.html' %} {% else %} @@ -22,4 +23,5 @@ {% endfor %} {% endblock %} + {% endif %} diff --git a/panoramix/views.py b/panoramix/views.py index e28d7d8b1b..1354729d2a 100644 --- a/panoramix/views.py +++ b/panoramix/views.py @@ -205,9 +205,12 @@ class SliceModelView(PanoramixModelView, DeleteMixin): 'slice_link', 'viz_type', 'datasource_type', 'datasource', 'created_by', 'changed_on_'] edit_columns = [ - 'slice_name', 'viz_type', 'druid_datasource', + 'slice_name', 'description', 'viz_type', 'druid_datasource', 'table', 'dashboards', 'params'] base_order = ('changed_on','desc') + description_columns = { + 'description': Markup("The content here can be displayed as widget headers in the dashboard view. Supports markdown"), + } appbuilder.add_view( @@ -222,7 +225,8 @@ class DashboardModelView(PanoramixModelView, DeleteMixin): datamodel = SQLAInterface(models.Dashboard) list_columns = ['dashboard_link', 'created_by', 'changed_by', 'changed_on_'] edit_columns = [ - 'dashboard_title', 'slug', 'slices', 'position_json', 'css'] + 'dashboard_title', 'slug', 'slices', 'position_json', 'css', + 'json_metadata'] add_columns = edit_columns base_order = ('changed_on','desc') description_columns = { @@ -434,6 +438,15 @@ class Panoramix(BaseView): payload, status=status, mimetype="application/csv") + + slice_id = request.args.get("slice_id") + slc = None + if slice_id: + slc = ( + db.session.query(models.Slice) + .filter_by(id=request.args.get("slice_id")) + .first() + ) if request.args.get("json") == "true": status = 200 if config.get("DEBUG"): @@ -451,9 +464,11 @@ class Panoramix(BaseView): mimetype="application/json") else: if config.get("DEBUG"): - resp = self.render_template("panoramix/viz.html", viz=obj) + resp = self.render_template( + "panoramix/viz.html", viz=obj, slice=slc) try: - resp = self.render_template("panoramix/viz.html", viz=obj) + resp = self.render_template( + "panoramix/viz.html", viz=obj, slice=slc) except Exception as e: if config.get("DEBUG"): raise(e) @@ -490,6 +505,9 @@ class Panoramix(BaseView): dash = session.query(Dash).filter_by(id=dashboard_id).first() dash.slices = [o for o in dash.slices if o.id in slice_ids] dash.position_json = json.dumps(data['positions'], indent=4) + dash.json_metadata = json.dumps({ + 'expanded_slices': data['expanded_slices'], + }, indent=4) dash.css = data['css'] session.merge(dash) session.commit()