diff --git a/superset/assets/backendSync.json b/superset/assets/backendSync.json index 71e713032849..181261dd5a9e 100644 --- a/superset/assets/backendSync.json +++ b/superset/assets/backendSync.json @@ -1,175 +1,15 @@ { "controls": { "datasource": { - "type": "SelectControl", + "type": "DatasourceControl", "label": "Datasource", - "isLoading": true, - "clearable": false, "default": null, - "description": "" + "description": null }, "viz_type": { - "type": "SelectControl", + "type": "VizTypeControl", "label": "Visualization Type", - "clearable": false, "default": "table", - "choices": [ - [ - "dist_bar", - "Distribution - Bar Chart", - "/static/assets/images/viz_thumbnails/dist_bar.png" - ], - [ - "pie", - "Pie Chart", - "/static/assets/images/viz_thumbnails/pie.png" - ], - [ - "line", - "Time Series - Line Chart", - "/static/assets/images/viz_thumbnails/line.png" - ], - [ - "dual_line", - "Time Series - Dual Axis Line Chart", - "/static/assets/images/viz_thumbnails/dual_line.png" - ], - [ - "bar", - "Time Series - Bar Chart", - "/static/assets/images/viz_thumbnails/bar.png" - ], - [ - "compare", - "Time Series - Percent Change", - "/static/assets/images/viz_thumbnails/compare.png" - ], - [ - "area", - "Time Series - Stacked", - "/static/assets/images/viz_thumbnails/area.png" - ], - [ - "table", - "Table View", - "/static/assets/images/viz_thumbnails/table.png" - ], - [ - "markup", - "Markup", - "/static/assets/images/viz_thumbnails/markup.png" - ], - [ - "pivot_table", - "Pivot Table", - "/static/assets/images/viz_thumbnails/pivot_table.png" - ], - [ - "separator", - "Separator", - "/static/assets/images/viz_thumbnails/separator.png" - ], - [ - "word_cloud", - "Word Cloud", - "/static/assets/images/viz_thumbnails/word_cloud.png" - ], - [ - "treemap", - "Treemap", - "/static/assets/images/viz_thumbnails/treemap.png" - ], - [ - "cal_heatmap", - "Calendar Heatmap", - "/static/assets/images/viz_thumbnails/cal_heatmap.png" - ], - [ - "box_plot", - "Box Plot", - "/static/assets/images/viz_thumbnails/box_plot.png" - ], - [ - "bubble", - "Bubble Chart", - "/static/assets/images/viz_thumbnails/bubble.png" - ], - [ - "bullet", - "Bullet Chart", - "/static/assets/images/viz_thumbnails/bullet.png" - ], - [ - "big_number", - "Big Number with Trendline", - "/static/assets/images/viz_thumbnails/big_number.png" - ], - [ - "big_number_total", - "Big Number", - "/static/assets/images/viz_thumbnails/big_number_total.png" - ], - [ - "histogram", - "Histogram", - "/static/assets/images/viz_thumbnails/histogram.png" - ], - [ - "sunburst", - "Sunburst", - "/static/assets/images/viz_thumbnails/sunburst.png" - ], - [ - "sankey", - "Sankey", - "/static/assets/images/viz_thumbnails/sankey.png" - ], - [ - "directed_force", - "Directed Force Layout", - "/static/assets/images/viz_thumbnails/directed_force.png" - ], - [ - "country_map", - "Country Map", - "/static/assets/images/viz_thumbnails/country_map.png" - ], - [ - "world_map", - "World Map", - "/static/assets/images/viz_thumbnails/world_map.png" - ], - [ - "filter_box", - "Filter Box", - "/static/assets/images/viz_thumbnails/filter_box.png" - ], - [ - "iframe", - "iFrame", - "/static/assets/images/viz_thumbnails/iframe.png" - ], - [ - "para", - "Parallel Coordinates", - "/static/assets/images/viz_thumbnails/para.png" - ], - [ - "heatmap", - "Heatmap", - "/static/assets/images/viz_thumbnails/heatmap.png" - ], - [ - "horizon", - "Horizon", - "/static/assets/images/viz_thumbnails/horizon.png" - ], - [ - "mapbox", - "Mapbox", - "/static/assets/images/viz_thumbnails/mapbox.png" - ] - ], "description": "The type of visualization to display" }, "metrics": { @@ -179,8 +19,19 @@ "validators": [ null ], + "valueKey": "metric_name", "description": "One or many metrics to display" }, + "y_axis_bounds": { + "type": "BoundsControl", + "label": "Y Axis Bounds", + "renderTrigger": true, + "default": [ + null, + null + ], + "description": "Bounds for the Y axis. When left empty, the bounds are dynamically defined based on the min/max of the data. Note that this feature will only expand the axis range. It won't narrow the data's extent." + }, "order_by_cols": { "type": "SelectControl", "multi": true, @@ -192,14 +43,22 @@ "type": "SelectControl", "label": "Metric", "clearable": false, - "description": "Choose the metric" + "description": "Choose the metric", + "validators": [ + null + ], + "valueKey": "metric_name" }, "metric_2": { "type": "SelectControl", "label": "Right Axis Metric", - "choices": [], - "default": [], - "description": "Choose a metric for right axis" + "default": null, + "validators": [ + null + ], + "clearable": true, + "description": "Choose a metric for right axis", + "valueKey": "metric_name" }, "stacked_style": { "type": "SelectControl", @@ -221,8 +80,56 @@ "default": "stack", "description": "" }, - "linear_color_scheme": { + "sort_x_axis": { + "type": "SelectControl", + "label": "Sort X Axis", + "choices": [ + [ + "alpha_asc", + "Axis ascending" + ], + [ + "alpha_desc", + "Axis descending" + ], + [ + "value_asc", + "sum(value) ascending" + ], + [ + "value_desc", + "sum(value) descending" + ] + ], + "clearable": false, + "default": "alpha_asc" + }, + "sort_y_axis": { "type": "SelectControl", + "label": "Sort Y Axis", + "choices": [ + [ + "alpha_asc", + "Axis ascending" + ], + [ + "alpha_desc", + "Axis descending" + ], + [ + "value_asc", + "sum(value) ascending" + ], + [ + "value_desc", + "sum(value) descending" + ] + ], + "clearable": false, + "default": "alpha_asc" + }, + "linear_color_scheme": { + "type": "ColorSchemeControl", "label": "Linear Color Scheme", "choices": [ [ @@ -243,7 +150,31 @@ ] ], "default": "blue_white_yellow", - "description": "" + "clearable": false, + "description": "", + "renderTrigger": true, + "schemes": { + "blue_white_yellow": [ + "#00d1c1", + "white", + "#ffb400" + ], + "fire": [ + "white", + "yellow", + "red", + "black" + ], + "white_black": [ + "white", + "black" + ], + "black_white": [ + "black", + "white" + ] + }, + "isLinear": true }, "normalize_across": { "type": "SelectControl", @@ -288,6 +219,7 @@ "canvas_image_rendering": { "type": "SelectControl", "label": "Rendering", + "renderTrigger": true, "choices": [ [ "pixelated", @@ -723,6 +655,13 @@ "description": "Whether to include the time granularity as defined in the time section", "default": false }, + "show_perc": { + "type": "CheckboxControl", + "label": "Show percentage", + "renderTrigger": true, + "description": "Whether to include the percentage in the tooltip", + "default": true + }, "bar_stacked": { "type": "CheckboxControl", "label": "Stacked Bars", @@ -730,6 +669,13 @@ "default": false, "description": null }, + "pivot_margins": { + "type": "CheckboxControl", + "label": "Show totals", + "renderTrigger": false, + "default": true, + "description": "Display total row/column" + }, "show_markers": { "type": "CheckboxControl", "label": "Show Markers", @@ -785,29 +731,21 @@ }, "select_country": { "type": "SelectControl", - "label": "Country Name Type", + "label": "Country Name", "default": "France", "choices": [ - [ - "Algeria", - "Algeria" - ], [ "Belgium", "Belgium" ], [ - "Brasil", - "Brasil" + "Brazil", + "Brazil" ], [ "China", "China" ], - [ - "Germany", - "Germany" - ], [ "Egypt", "Egypt" @@ -816,6 +754,10 @@ "France", "France" ], + [ + "Germany", + "Germany" + ], [ "Italy", "Italy" @@ -825,8 +767,8 @@ "Morocco" ], [ - "Nederlanden", - "Nederlanden" + "Netherlands", + "Netherlands" ], [ "Russia", @@ -844,6 +786,10 @@ "Uk", "Uk" ], + [ + "Ukraine", + "Ukraine" + ], [ "Usa", "Usa" @@ -880,14 +826,18 @@ "multi": true, "label": "Group by", "default": [], - "description": "One or many controls to group by" + "includeTime": false, + "description": "One or many controls to group by", + "valueKey": "column_name" }, "columns": { "type": "SelectControl", "multi": true, "label": "Columns", "default": [], - "description": "One or many controls to pivot as columns" + "includeTime": false, + "description": "One or many controls to pivot as columns", + "valueKey": "column_name" }, "all_columns": { "type": "SelectControl", @@ -960,7 +910,46 @@ ] ], "default": "auto", - "description": "Bottom marging, in pixels, allowing for more room for axis labels" + "renderTrigger": true, + "description": "Bottom margin, in pixels, allowing for more room for axis labels" + }, + "left_margin": { + "type": "SelectControl", + "freeForm": true, + "label": "Left Margin", + "choices": [ + [ + "auto", + "auto" + ], + [ + 50, + "50" + ], + [ + 75, + "75" + ], + [ + 100, + "100" + ], + [ + 125, + "125" + ], + [ + 150, + "150" + ], + [ + 200, + "200" + ] + ], + "default": "auto", + "renderTrigger": true, + "description": "Left margin, in pixels, allowing for more room for axis labels" }, "granularity": { "type": "SelectControl", @@ -1263,77 +1252,16 @@ "description": "Pandas resample fill method" }, "since": { - "type": "SelectControl", + "type": "DateFilterControl", "freeForm": true, "label": "Since", - "default": "7 days ago", - "choices": [ - [ - "1 hour ago", - "1 hour ago" - ], - [ - "12 hours ago", - "12 hours ago" - ], - [ - "1 day ago", - "1 day ago" - ], - [ - "7 days ago", - "7 days ago" - ], - [ - "28 days ago", - "28 days ago" - ], - [ - "90 days ago", - "90 days ago" - ], - [ - "1 year ago", - "1 year ago" - ], - [ - "100 year ago", - "100 year ago" - ] - ], - "description": "Timestamp from filter. This supports free form typing and natural language as in `1 day ago`, `28 days` or `3 years`" + "default": "7 days ago" }, "until": { - "type": "SelectControl", + "type": "DateFilterControl", "freeForm": true, "label": "Until", - "default": "now", - "choices": [ - [ - "now", - "now" - ], - [ - "1 day ago", - "1 day ago" - ], - [ - "7 days ago", - "7 days ago" - ], - [ - "28 days ago", - "28 days ago" - ], - [ - "90 days ago", - "90 days ago" - ], - [ - "1 year ago", - "1 year ago" - ] - ] + "default": "now" }, "max_bubble_size": { "type": "SelectControl", @@ -1524,6 +1452,12 @@ "default": null, "description": "Metric used to define the top series" }, + "order_desc": { + "type": "CheckboxControl", + "label": "Sort Descending", + "default": true, + "description": "Whether to sort descending or ascending" + }, "rolling_type": { "type": "SelectControl", "label": "Rolling", @@ -1558,6 +1492,12 @@ "isInt": true, "description": "Defines the size of the rolling window function, relative to the time granularity selected" }, + "min_periods": { + "type": "TextControl", + "label": "Min Periods", + "isInt": true, + "description": "The minimum number of rolling periods required to show a value. For instance if you do a cumulative sum on 7 days you may want your \"Min Period\" to be 7, so that all data points shown are the total of 7 periods. This will hide the \"ramp up\" taking place over the first 7 periods" + }, "series": { "type": "SelectControl", "label": "Series", @@ -1568,24 +1508,39 @@ "type": "SelectControl", "label": "Entity", "default": null, - "description": "This define the element to be plotted on the chart" + "validators": [ + null + ], + "description": "This defines the element to be plotted on the chart" }, "x": { "type": "SelectControl", "label": "X Axis", + "description": "Metric assigned to the [X] axis", "default": null, - "description": "Metric assigned to the [X] axis" + "validators": [ + null + ], + "valueKey": "metric_name" }, "y": { "type": "SelectControl", "label": "Y Axis", "default": null, - "description": "Metric assigned to the [Y] axis" + "validators": [ + null + ], + "description": "Metric assigned to the [Y] axis", + "valueKey": "metric_name" }, "size": { "type": "SelectControl", "label": "Bubble Size", - "default": null + "default": null, + "validators": [ + null + ], + "valueKey": "metric_name" }, "url": { "type": "TextControl", @@ -1632,7 +1587,11 @@ "type": "SelectControl", "freeForm": true, "label": "Table Timestamp Format", - "default": "smart_date", + "default": "%Y-%m-%d %H:%M:%S", + "validators": [ + null + ], + "clearable": false, "choices": [ [ "smart_date", @@ -1746,7 +1705,41 @@ "x_axis_format": { "type": "SelectControl", "freeForm": true, - "label": "X axis format", + "label": "X Axis Format", + "renderTrigger": true, + "default": ".3s", + "choices": [ + [ + ".3s", + ".3s | 12.3k" + ], + [ + ".3%", + ".3% | 1234543.210%" + ], + [ + ".4r", + ".4r | 12350" + ], + [ + ".3f", + ".3f | 12345.432" + ], + [ + "+,", + "+, | +12,345.4321" + ], + [ + "$,.2f", + "$,.2f | $12,345.43" + ] + ], + "description": "D3 format syntax: https://github.com/d3/d3-format" + }, + "x_axis_time_format": { + "type": "SelectControl", + "freeForm": true, + "label": "X Axis Format", "renderTrigger": true, "default": "smart_date", "choices": [ @@ -1776,7 +1769,7 @@ "y_axis_format": { "type": "SelectControl", "freeForm": true, - "label": "Y axis format", + "label": "Y Axis Format", "renderTrigger": true, "default": ".3s", "choices": [ @@ -1810,7 +1803,7 @@ "y_axis_2_format": { "type": "SelectControl", "freeForm": true, - "label": "Right axis format", + "label": "Right Axis Format", "default": ".3s", "choices": [ [ @@ -1843,6 +1836,7 @@ "markup_type": { "type": "SelectControl", "label": "Markup Type", + "clearable": false, "choices": [ [ "markdown", @@ -1854,6 +1848,9 @@ ] ], "default": "markdown", + "validators": [ + null + ], "description": "Pick your favorite markup language" }, "rotation": { @@ -2046,6 +2043,13 @@ "default": true, "description": "Whether to display the min and max values of the X axis" }, + "y_axis_showminmax": { + "type": "CheckboxControl", + "label": "Y bounds", + "renderTrigger": true, + "default": true, + "description": "Whether to display the min and max values of the Y axis" + }, "rich_tooltip": { "type": "CheckboxControl", "label": "Rich Tooltip", @@ -2053,12 +2057,12 @@ "default": true, "description": "The rich tooltip shows a list of all series for that point in time" }, - "y_axis_zero": { + "insert_zeros": { "type": "CheckboxControl", - "label": "Y Axis Zero", - "default": false, + "label": "Insert Zeros", "renderTrigger": true, - "description": "Force the Y axis to start at 0 instead of the minimum value" + "default": false, + "description": "Insert zeros if there is no data for the selected granularity" }, "y_log_scale": { "type": "CheckboxControl", @@ -2078,12 +2082,14 @@ "type": "CheckboxControl", "label": "Donut", "default": false, + "renderTrigger": true, "description": "Do you want a donut or a pie?" }, "labels_outside": { "type": "CheckboxControl", "label": "Put labels outside", "default": true, + "renderTrigger": true, "description": "Put the labels outside the pie?" }, "contribution": { @@ -2369,6 +2375,235 @@ "label": "Cache Timeout (seconds)", "hidden": true, "description": "The number of seconds before expiring the cache" + }, + "order_by_entity": { + "type": "CheckboxControl", + "label": "Order by entity id", + "description": "Important! Select this if the table is not already sorted by entity id, else there is no guarantee that all events for each entity are returned.", + "default": true + }, + "min_leaf_node_event_count": { + "type": "SelectControl", + "freeForm": false, + "label": "Minimum leaf node event count", + "default": 1, + "choices": [ + [ + 1, + "1" + ], + [ + 2, + "2" + ], + [ + 3, + "3" + ], + [ + 4, + "4" + ], + [ + 5, + "5" + ], + [ + 6, + "6" + ], + [ + 7, + "7" + ], + [ + 8, + "8" + ], + [ + 9, + "9" + ], + [ + 10, + "10" + ] + ], + "description": "Leaf nodes that represent fewer than this number of events will be initially hidden in the visualization" + }, + "color_scheme": { + "type": "ColorSchemeControl", + "label": "Color Scheme", + "default": "bnbColors", + "renderTrigger": true, + "choices": [ + [ + "bnbColors", + "bnbColors" + ], + [ + "d3Category10", + "d3Category10" + ], + [ + "d3Category20", + "d3Category20" + ], + [ + "d3Category20b", + "d3Category20b" + ], + [ + "d3Category20c", + "d3Category20c" + ], + [ + "googleCategory10c", + "googleCategory10c" + ], + [ + "googleCategory20c", + "googleCategory20c" + ] + ], + "description": "The color scheme for rendering chart", + "schemes": { + "bnbColors": [ + "#ff5a5f", + "#7b0051", + "#007A87", + "#00d1c1", + "#8ce071", + "#ffb400", + "#b4a76c", + "#ff8083", + "#cc0086", + "#00a1b3", + "#00ffeb", + "#bbedab", + "#ffd266", + "#cbc29a", + "#ff3339", + "#ff1ab1", + "#005c66", + "#00b3a5", + "#55d12e", + "#b37e00", + "#988b4e" + ], + "d3Category10": [ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf" + ], + "d3Category20": [ + "#1f77b4", + "#aec7e8", + "#ff7f0e", + "#ffbb78", + "#2ca02c", + "#98df8a", + "#d62728", + "#ff9896", + "#9467bd", + "#c5b0d5", + "#8c564b", + "#c49c94", + "#e377c2", + "#f7b6d2", + "#7f7f7f", + "#c7c7c7", + "#bcbd22", + "#dbdb8d", + "#17becf", + "#9edae5" + ], + "d3Category20b": [ + "#393b79", + "#5254a3", + "#6b6ecf", + "#9c9ede", + "#637939", + "#8ca252", + "#b5cf6b", + "#cedb9c", + "#8c6d31", + "#bd9e39", + "#e7ba52", + "#e7cb94", + "#843c39", + "#ad494a", + "#d6616b", + "#e7969c", + "#7b4173", + "#a55194", + "#ce6dbd", + "#de9ed6" + ], + "d3Category20c": [ + "#3182bd", + "#6baed6", + "#9ecae1", + "#c6dbef", + "#e6550d", + "#fd8d3c", + "#fdae6b", + "#fdd0a2", + "#31a354", + "#74c476", + "#a1d99b", + "#c7e9c0", + "#756bb1", + "#9e9ac8", + "#bcbddc", + "#dadaeb", + "#636363", + "#969696", + "#bdbdbd", + "#d9d9d9" + ], + "googleCategory10c": [ + "#3366cc", + "#dc3912", + "#ff9900", + "#109618", + "#990099", + "#0099c6", + "#dd4477", + "#66aa00", + "#b82e2e", + "#316395" + ], + "googleCategory20c": [ + "#3366cc", + "#dc3912", + "#ff9900", + "#109618", + "#990099", + "#0099c6", + "#dd4477", + "#66aa00", + "#b82e2e", + "#316395", + "#994499", + "#22aa99", + "#aaaa11", + "#6633cc", + "#e67300", + "#8b0707", + "#651067", + "#329262", + "#5574a6", + "#3b3eac" + ] + } } } } \ No newline at end of file diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx index 894b5a4f6fd5..735fcdea03ce 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -1063,6 +1063,15 @@ export const controls = { 'point in time'), }, + insert_zeros: { + type: 'CheckboxControl', + label: 'Insert Zeros', + renderTrigger: true, + default: false, + description: 'Insert zeros if there is no data for the selected ' + + 'granularity', + }, + y_log_scale: { type: 'CheckboxControl', label: t('Y Log Scale'), diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js index 8298afce0d11..fb0e29d00a9d 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -159,6 +159,7 @@ export const visTypes = { ['show_brush', 'show_legend'], ['rich_tooltip', 'show_markers'], ['line_interpolation', 'contribution'], + ['insert_zeros'], ], }, { diff --git a/superset/assets/javascripts/modules/dates.js b/superset/assets/javascripts/modules/dates.js index 2c2815e8a1bd..b73ff0f837df 100644 --- a/superset/assets/javascripts/modules/dates.js +++ b/superset/assets/javascripts/modules/dates.js @@ -98,3 +98,44 @@ export const epochTimeXYearsAgo = function (y) { .utc() .valueOf(); }; + +/** + * Converts all kinds of "durations" or "granularities" to milliseconds + * from Epoch + * @param granularity in all kinds of formats: + * integer (milliseconds), SQL granularities, strings of integer, period granularities ISO8601 + * human readable ("1 hour", "5 years") + * @returns {number} milliseconds from Epoch, 0 if it couldn't parse the input. + */ +export function granularityToEpoch(granularity) { + let epoch = 0; + if (granularity) { + epoch = moment.duration(granularity).asMilliseconds(); + if (!epoch && typeof granularity === 'string') { + const gran = granularity.trim(); + if (gran.match(/^[0-9]*$/)) { // this covers strings containing only numbers + epoch = moment.duration(Number.parseInt(gran, 10)).asMilliseconds(); + } + if (!epoch && gran) { + // this covers human readable stuff like like "1 year", "5 seconds" + const t = gran.match(/^[0-9]*\s*/); + if (t && t.length && t[0].length) { + const num = t[0]; + const unit = gran.slice(num.length); + epoch = moment.duration(Number.parseInt(num, 10), unit).asMilliseconds(); + } + if (!epoch) { // this covers SQL like granularities ["month", "week", ...] + epoch = moment.duration(1, gran).asMilliseconds(); + } + if (!epoch) { + if (gran.toLowerCase() === 'fifteen_minute') { + epoch = moment.duration(15, 'minutes').asMilliseconds(); + } else if (gran.toLowerCase() === 'thirty_minute') { + epoch = moment.duration(30, 'minutes').asMilliseconds(); + } + } + } + } + } + return epoch; +} diff --git a/superset/assets/spec/javascripts/modules/dates_spec.js b/superset/assets/spec/javascripts/modules/dates_spec.js index b073921a437c..e93b26ce9e8b 100644 --- a/superset/assets/spec/javascripts/modules/dates_spec.js +++ b/superset/assets/spec/javascripts/modules/dates_spec.js @@ -1,5 +1,7 @@ import { it, describe } from 'mocha'; -import { expect } from 'chai'; +import { assert, expect } from 'chai'; +import moment from 'moment'; + import { tickMultiFormat, formatDate, @@ -8,6 +10,7 @@ import { epochTimeXHoursAgo, epochTimeXDaysAgo, epochTimeXYearsAgo, + granularityToEpoch, } from '../../../javascripts/modules/dates'; describe('tickMultiFormat', () => { @@ -76,3 +79,81 @@ describe('epochTimeXYearsAgo', () => { expect(epochTimeXYearsAgo(1)).to.be.a('number'); }); }); + + +const ONE_MILLISECOND = moment.duration(1).asMilliseconds(); +const ONE_SECOND = moment.duration(1, 'second').asMilliseconds(); +const TWO_SECONDS = moment.duration(2, 'second').asMilliseconds(); +const ONE_MINUTE = moment.duration(1, 'minute').asMilliseconds(); +const FIFTEEN_MINUTES = moment.duration(15, 'minute').asMilliseconds(); +const THIRTY_MINUTES = moment.duration(30, 'minute').asMilliseconds(); +const ONE_HOUR = moment.duration(1, 'hour').asMilliseconds(); +const ONE_DAY = moment.duration(1, 'day').asMilliseconds(); +const ONE_WEEK = moment.duration(7, 'days').asMilliseconds(); +const ONE_MONTH = moment.duration(1, 'month').asMilliseconds(); +const ONE_QUARTER = moment.duration(1, 'quarter').asMilliseconds(); +const ONE_YEAR = moment.duration(1, 'year').asMilliseconds(); +const FIVE_YEARS = moment.duration(5, 'year').asMilliseconds(); + +describe('granularityToEpoch', () => { + it('handles numbers', () => { + assert.equal(granularityToEpoch(1), ONE_MILLISECOND); + assert.equal(granularityToEpoch(1000), ONE_SECOND); + }); + it('handles human', () => { + assert.equal(granularityToEpoch('1 millisecond'), ONE_MILLISECOND); + assert.equal(granularityToEpoch('1 second'), ONE_SECOND); + assert.equal(granularityToEpoch('2 second'), TWO_SECONDS); + assert.equal(granularityToEpoch('2 seConds'), TWO_SECONDS); + assert.equal(granularityToEpoch('1 minute'), ONE_MINUTE); + assert.equal(granularityToEpoch('1 hour'), ONE_HOUR); + assert.equal(granularityToEpoch('1 day'), ONE_DAY); + assert.equal(granularityToEpoch('1 week'), ONE_WEEK); + assert.equal(granularityToEpoch('1 month'), ONE_MONTH); + assert.equal(granularityToEpoch('1 quarter'), ONE_QUARTER); + assert.equal(granularityToEpoch('1 year'), ONE_YEAR); + assert.equal(granularityToEpoch('5 years'), FIVE_YEARS); + }); + it('handles SQL', () => { + assert.equal(granularityToEpoch('second'), ONE_SECOND); + assert.equal(granularityToEpoch('minute'), ONE_MINUTE); + assert.equal(granularityToEpoch('hour'), ONE_HOUR); + assert.equal(granularityToEpoch('day'), ONE_DAY); + assert.equal(granularityToEpoch('week'), ONE_WEEK); + assert.equal(granularityToEpoch('month'), ONE_MONTH); + assert.equal(granularityToEpoch('quarter'), ONE_QUARTER); + assert.equal(granularityToEpoch('year'), ONE_YEAR); + }); + it('handles ISO 8601', () => { + assert.equal(granularityToEpoch('PT1S'), ONE_SECOND); + assert.equal(granularityToEpoch('PT2S'), TWO_SECONDS); + assert.equal(granularityToEpoch('PT1M'), ONE_MINUTE); + assert.equal(granularityToEpoch('PT1H'), ONE_HOUR); + assert.equal(granularityToEpoch('P1D'), ONE_DAY); + assert.equal(granularityToEpoch('P1W'), ONE_WEEK); + assert.equal(granularityToEpoch('P1M'), ONE_MONTH); + assert.equal(granularityToEpoch('P1Y'), ONE_YEAR); + assert.equal(granularityToEpoch('P5Y'), FIVE_YEARS); + }); + it('handles Druid', () => { + assert.equal(granularityToEpoch('all'), 0); + assert.equal(granularityToEpoch('none'), 0); + assert.equal(granularityToEpoch('second'), ONE_SECOND); + assert.equal(granularityToEpoch('minute'), ONE_MINUTE); + assert.equal(granularityToEpoch('fifteen_minute'), FIFTEEN_MINUTES); + assert.equal(granularityToEpoch('thirty_minute'), THIRTY_MINUTES); + assert.equal(granularityToEpoch('hour'), ONE_HOUR); + assert.equal(granularityToEpoch('day'), ONE_DAY); + assert.equal(granularityToEpoch('week'), ONE_WEEK); + assert.equal(granularityToEpoch('month'), ONE_MONTH); + assert.equal(granularityToEpoch('quarter'), ONE_QUARTER); + assert.equal(granularityToEpoch('year'), ONE_YEAR); + }); + it('handles bad input', () => { + assert.equal(granularityToEpoch(null), 0); + assert.equal(granularityToEpoch(undefined), 0); + assert.equal(granularityToEpoch('fsdf'), 0); + assert.equal(granularityToEpoch({}), 0); + assert.equal(granularityToEpoch([]), 0); + }); +}); diff --git a/superset/assets/visualizations/nvd3_vis.js b/superset/assets/visualizations/nvd3_vis.js index 831b99710e86..1b0f16172476 100644 --- a/superset/assets/visualizations/nvd3_vis.js +++ b/superset/assets/visualizations/nvd3_vis.js @@ -6,6 +6,7 @@ import nv from 'nvd3'; import { getColorFromScheme } from '../javascripts/modules/colors'; import { customizeToolTip, d3TimeFormatPreset, d3FormatPreset, tryNumify } from '../javascripts/modules/utils'; +import { granularityToEpoch } from '../javascripts/modules/dates'; // CSS import '../node_modules/nvd3/build/nv.d3.min.css'; @@ -18,6 +19,7 @@ const BREAKPOINTS = { small: 340, }; + const addTotalBarValues = function (svg, chart, data, stacked, axisFormat) { const format = d3.format(axisFormat || '.3s'); const countSeriesDisplayed = data.length; @@ -75,9 +77,54 @@ function getMaxLabelSize(container, axisClass) { function nvd3Vis(slice, payload) { let chart; - let colorKey = 'key'; + const fd = slice.formData; + let colorKey = fd.colorKey ? fd.colorKey : 'key'; const isExplore = $('#explore-container').length === 1; + // Make a local copy of the data + const data = payload.data.map(d => ({ + ...d, + values: d.values.slice(), + })); + + // This gets around that granularities are stored in different variables + // for duruid and SQL, the order here is important :( + const granularity = fd.time_grain_sqla || fd.granularity; + // insert zeros if there is no data for a given granularity + if (fd.insert_zeros) { + for (const d of data) { + if (d.values && d.values.length > 2) { + // We are trying to parse the granularity from the time series settings + // if that does not work we are guessing the expected granularity as + // the minimum delta X between two data points + const minDelta = granularityToEpoch(granularity) || + Math.min(...d.values.slice(1).map((v, i) => v.x - d.values[i].x)); + if (minDelta > 0) { + // This will generate a minimum number of zero data points, e.g.: + // your minDelta/granularity is 1 minute, but in a given time interval there was + // no data for 1 hour this will generate one data point with value 0 + // 1 Minute after the left bound of the interval and 1 Minute before + // the right bound of the interval + const zeros = d.values.slice(1).reduce((z, v, i) => { + const delta = Math.round(v.x - d.values[i].x); + if (delta > minDelta * 1.4) { + z.push({ y: 0, x: d.values[i].x + minDelta }); + } + if (delta > minDelta * 2.4) { + z.push({ y: 0, x: v.x - minDelta }); + } + return z; + }, []); + // Add the zero data points to your time series. + if (zeros.length) { + d.values.push(...zeros); + d.values = d.values.sort((a, b) => a.x - b.x); + } + } + } + } + } + slice.container.html(''); slice.clearError(); @@ -97,14 +144,14 @@ function nvd3Vis(slice, payload) { } let width = slice.width(); - const fd = slice.formData; + const barchartWidth = function () { let bars; if (fd.bar_stacked) { - bars = d3.max(payload.data, function (d) { return d.values.length; }); + bars = d3.max(data, function (d) { return d.values.length; }); } else { - bars = d3.sum(payload.data, function (d) { return d.values.length; }); + bars = d3.sum(data, function (d) { return d.values.length; }); } if (bars * minBarWidth > width) { return bars * minBarWidth; @@ -162,7 +209,7 @@ function nvd3Vis(slice, payload) { if (fd.show_bar_value) { setTimeout(function () { - addTotalBarValues(svg, chart, payload.data, stacked, fd.y_axis_format); + addTotalBarValues(svg, chart, data, stacked, fd.y_axis_format); }, animationTime); } break; @@ -179,13 +226,13 @@ function nvd3Vis(slice, payload) { stacked = fd.bar_stacked; chart.stacked(stacked); if (fd.order_bars) { - payload.data.forEach((d) => { + data.forEach((d) => { d.values.sort((a, b) => tryNumify(a.x) < tryNumify(b.x) ? -1 : 1); }); } if (fd.show_bar_value) { setTimeout(function () { - addTotalBarValues(svg, chart, payload.data, stacked, fd.y_axis_format); + addTotalBarValues(svg, chart, data, stacked, fd.y_axis_format); }, animationTime); } if (!reduceXTicks) { @@ -208,7 +255,7 @@ function nvd3Vis(slice, payload) { if (fd.pie_label_type === 'percent') { let total = 0; - payload.data.forEach((d) => { total += d.y; }); + data.forEach((d) => { total += d.y; }); chart.tooltip.valueFormatter(d => `${((d / total) * 100).toFixed()}%`); } @@ -248,7 +295,7 @@ function nvd3Vis(slice, payload) { return s; }); chart.pointRange([5, fd.max_bubble_size ** 2]); - chart.pointDomain([0, d3.max(payload.data, d => d3.max(d.values, v => v.size))]); + chart.pointDomain([0, d3.max(data, d => d3.max(d.values, v => v.size))]); break; case 'area': @@ -395,7 +442,7 @@ function nvd3Vis(slice, payload) { chart.showLegend(width > BREAKPOINTS.small); } svg - .datum(payload.data) + .datum(data) .transition().duration(500) .attr('height', height) .attr('width', width) @@ -471,7 +518,7 @@ function nvd3Vis(slice, payload) { // render chart svg - .datum(payload.data) + .datum(data) .transition().duration(500) .attr('height', height) .attr('width', width) @@ -489,7 +536,7 @@ function nvd3Vis(slice, payload) { // this will clear them before rendering the chart again. hideTooltips(); - nv.addGraph(drawGraph); + nv.addGraph(drawGraph()); } module.exports = nvd3Vis;