You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/06/22 00:54:34 UTC
[incubator-superset] 19/26: Fix dashboard position row data (#5131)
This is an automated email from the ASF dual-hosted git repository.
ccwilliams pushed a commit to branch dashboard-builder
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit 017e797e352d9d00b035f5b330fcac8fe84187e5
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Wed Jun 6 17:06:12 2018 -0700
Fix dashboard position row data (#5131)
* add slice_name to markdown
(cherry picked from commit 14b01f1)
* set min grid width be 1 column
* remove empty column
* check total columns count <= 12
* scan position data and fix rows
* fix dashboard url with default_filters
---
superset/assets/src/dashboard/util/constants.js | 2 +-
.../src/dashboard/util/dashboardLayoutConverter.js | 214 +++++++++++++++++++--
superset/models/core.py | 14 +-
3 files changed, 208 insertions(+), 22 deletions(-)
diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js
index ef2c8bb..bfe24cc 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -19,7 +19,7 @@ export const DASHBOARD_ROOT_DEPTH = 0;
export const GRID_BASE_UNIT = 8;
export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
export const GRID_COLUMN_COUNT = 12;
-export const GRID_MIN_COLUMN_COUNT = 2;
+export const GRID_MIN_COLUMN_COUNT = 1;
export const GRID_MIN_ROW_UNITS = 5;
export const GRID_MAX_ROW_UNITS = 100;
export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE;
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index cf7a493..c6c124b 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -13,7 +13,12 @@ import {
DASHBOARD_GRID_TYPE,
} from './componentTypes';
-import { DASHBOARD_GRID_ID } from './constants';
+import {
+ DASHBOARD_GRID_ID,
+ GRID_MIN_COLUMN_COUNT,
+ GRID_MIN_ROW_UNITS,
+ GRID_COLUMN_COUNT,
+} from './constants';
const MAX_RECURSIVE_LEVEL = 6;
const GRID_RATIO = 4;
@@ -72,19 +77,32 @@ function getColContainer() {
}
function getChartHolder(item) {
- const { size_x, size_y, slice_id, code } = item;
+ const { size_x, size_y, slice_id, code, slice_name } = item;
- const width = Math.max(1, Math.floor(size_x / GRID_RATIO));
- const height = Math.max(1, Math.round(size_y / GRID_RATIO));
+ const width = Math.max(
+ GRID_MIN_COLUMN_COUNT,
+ Math.round(size_x / GRID_RATIO),
+ );
+ const height = Math.max(
+ GRID_MIN_ROW_UNITS,
+ Math.round(size_y / GRID_RATIO * 100 / ROW_HEIGHT),
+ );
if (code !== undefined) {
+ let markdownContent = '';
+ if (slice_name) {
+ markdownContent = `##### **${slice_name}**\n`;
+ }
+ if (code) {
+ markdownContent += code;
+ }
return {
type: MARKDOWN_TYPE,
id: `DASHBOARD_MARKDOWN_TYPE-${generateId()}`,
children: [],
meta: {
width,
- height: Math.round(height * 100 / ROW_HEIGHT),
- code,
+ height,
+ code: markdownContent,
},
};
}
@@ -94,7 +112,7 @@ function getChartHolder(item) {
children: [],
meta: {
width,
- height: Math.round(height * 100 / ROW_HEIGHT),
+ height,
chartId: parseInt(slice_id, 10),
},
};
@@ -135,6 +153,50 @@ function hasOverlap(positions, xAxis = true) {
});
}
+function isWideLeafComponent(component) {
+ return (
+ [CHART_TYPE, MARKDOWN_TYPE].indexOf(component.type) > -1 &&
+ component.meta.width > GRID_MIN_COLUMN_COUNT
+ );
+}
+
+function canReduceColumnWidth(columnComponent, root) {
+ return (
+ columnComponent.type === COLUMN_TYPE &&
+ columnComponent.meta.width > GRID_MIN_COLUMN_COUNT &&
+ columnComponent.children.every(
+ childId =>
+ isWideLeafComponent(root[childId]) ||
+ (root[childId].type === ROW_TYPE &&
+ root[childId].children.every(id => isWideLeafComponent(root[id]))),
+ )
+ );
+}
+
+function reduceRowWidth(rowComponent, root) {
+ // find widest free chart and reduce width
+ const widestChartId = rowComponent.children
+ .filter(childId => isWideLeafComponent(root[childId]))
+ .reduce((prev, current) => {
+ if (root[prev].meta.width >= root[current].meta.width) {
+ return prev;
+ }
+ return current;
+ });
+
+ if (widestChartId) {
+ root[widestChartId].meta.width -= 1;
+ }
+ return getChildrenSum(rowComponent.children, 'width', root);
+}
+
+function reduceComponentWidth(component) {
+ if (isWideLeafComponent(component)) {
+ component.meta.width -= 1;
+ }
+ return component.meta.width;
+}
+
function doConvert(positions, level, parent, root) {
if (positions.length === 0) {
return;
@@ -235,7 +297,6 @@ function doConvert(positions, level, parent, root) {
// create a new column
const colContainer = getColContainer();
root[colContainer.id] = colContainer;
- rowContainer.children.push(colContainer.id);
if (!hasOverlap(lower, false)) {
lower.sort(sortByRowId).forEach(item => {
@@ -248,11 +309,15 @@ function doConvert(positions, level, parent, root) {
}
// add col meta
- colContainer.meta.width = getChildrenMax(
- colContainer.children,
- 'width',
- root,
- );
+ if (colContainer.children.length) {
+ rowContainer.children.push(colContainer.id);
+ // add col meta
+ colContainer.meta.width = getChildrenMax(
+ colContainer.children,
+ 'width',
+ root,
+ );
+ }
}
currentItems = upper.slice();
@@ -278,6 +343,50 @@ export function convertToLayout(positions) {
Object.values(root).forEach(item => {
if (ROW_TYPE === item.type) {
const meta = item.meta;
+ if (meta.width > GRID_COLUMN_COUNT) {
+ let currentWidth = meta.width;
+ while (
+ currentWidth > GRID_COLUMN_COUNT &&
+ item.children.filter(id => isWideLeafComponent(root[id])).length
+ ) {
+ currentWidth = reduceRowWidth(item, root);
+ }
+
+ // reduce column width
+ if (currentWidth > GRID_COLUMN_COUNT) {
+ // find column that: width > 2 and each row has at least 1 chart can reduce
+ // 2 loops: same column may reduce multiple times
+ let colIds;
+ do {
+ colIds = item.children.filter(colId =>
+ canReduceColumnWidth(root[colId], root),
+ );
+ let idx = 0;
+ while (idx < colIds.length && currentWidth > GRID_COLUMN_COUNT) {
+ const currentColumn = colIds[idx];
+ root[currentColumn].children.forEach(childId => {
+ if (root[childId].type === ROW_TYPE) {
+ root[childId].meta.width = reduceRowWidth(
+ root[childId],
+ root,
+ );
+ } else {
+ root[childId].meta.width = reduceComponentWidth(
+ root[childId],
+ );
+ }
+ });
+ root[currentColumn].meta.width = getChildrenMax(
+ root[currentColumn].children,
+ 'width',
+ root,
+ );
+ currentWidth = getChildrenSum(item.children, 'width', root);
+ idx += 1;
+ }
+ } while (colIds.length && currentWidth > GRID_COLUMN_COUNT);
+ }
+ }
delete meta.width;
}
});
@@ -286,13 +395,84 @@ export function convertToLayout(positions) {
return root;
}
+function mergePosition(position, bottomLine, maxColumn) {
+ const { col, size_x, size_y } = position;
+ const endColumn = col + size_x > maxColumn ? bottomLine.length : col + size_x;
+ const nextSectionStart =
+ bottomLine.slice(col).findIndex(value => value > bottomLine[col]) + 1;
+
+ const currentBottom =
+ nextSectionStart > 0 && nextSectionStart < size_x
+ ? Math.max.apply(null, bottomLine.slice(col, col + size_x + 1))
+ : bottomLine[col];
+ bottomLine.fill(currentBottom + size_y, col, endColumn);
+}
+
+// In original position data, a lot of position's row attribute are not correct, and same positions are
+// assigned to more than 1 chart. The convert function depends on row id, col id to split
+// the whole dashboard into nested rows and columns. Bad row id will lead to many empty spaces, or a few
+// charts are overlapped in the same row.
+// This function read positions by row first. Then based on previous col id, width and height attribute,
+// re-calculate next position's row id.
+function scanDashboardPositionsData(positions) {
+ const bottomLine = new Array(49).fill(0);
+ bottomLine[0] = Number.MAX_VALUE;
+ const maxColumn = Math.max.apply(
+ null,
+ positions.slice().map(position => position.col),
+ );
+
+ const positionsByRowId = {};
+ positions
+ .slice()
+ .sort(sortByRowId)
+ .forEach(position => {
+ const { row } = position;
+ if (positionsByRowId[row] === undefined) {
+ positionsByRowId[row] = [];
+ }
+ positionsByRowId[row].push(position);
+ });
+ const rawPositions = Object.values(positionsByRowId);
+ const updatedPositions = [];
+
+ while (rawPositions.length) {
+ const nextRow = rawPositions.shift();
+ let nextCol = 1;
+ while (nextRow.length) {
+ // special treatment for duplicated positions: display wider one first
+ const availableIndexByColumn = nextRow
+ .filter(position => position.col === nextCol)
+ .map((position, index) => index);
+ if (availableIndexByColumn.length) {
+ const idx =
+ availableIndexByColumn.length > 1
+ ? availableIndexByColumn.sort(
+ (idx1, idx2) => nextRow[idx2].size_x - nextRow[idx1].size_x,
+ )[0]
+ : availableIndexByColumn[0];
+
+ const nextPosition = nextRow.splice(idx, 1)[0];
+ mergePosition(nextPosition, bottomLine, maxColumn + 1);
+ nextPosition.row = bottomLine[nextPosition.col] - nextPosition.size_y;
+ updatedPositions.push(nextPosition);
+ nextCol += nextPosition.size_x;
+ } else {
+ nextCol = nextRow[0].col;
+ }
+ }
+ }
+
+ return updatedPositions;
+}
+
export default function(dashboard) {
const positions = [];
-
- // position data clean up. some dashboard didn't have position_json
let { position_json } = dashboard;
const positionDict = {};
if (Array.isArray(position_json)) {
+ // scan and fix positions data: extra spaces, dup rows, .etc
+ position_json = scanDashboardPositionsData(position_json);
position_json.forEach(position => {
positionDict[position.slice_id] = position;
});
@@ -300,12 +480,13 @@ export default function(dashboard) {
position_json = [];
}
+ // position data clean up. some dashboard didn't have position_json
const lastRowId = Math.max(
0,
Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
);
let newSliceCounter = 0;
- dashboard.slices.forEach(({ slice_id, form_data }) => {
+ dashboard.slices.forEach(({ slice_id, form_data, slice_name }) => {
let position = positionDict[slice_id];
if (!position) {
// append new slices to dashboard bottom, 3 slices per row
@@ -322,6 +503,7 @@ export default function(dashboard) {
position = {
...position,
code: form_data.code,
+ slice_name,
};
}
positions.push(position);
diff --git a/superset/models/core.py b/superset/models/core.py
index 3b663af..faf0a6f 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -347,11 +347,15 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
# add default_filters to the preselect_filters of dashboard
json_metadata = json.loads(self.json_metadata)
default_filters = json_metadata.get('default_filters')
- # make sure default_filters is not empty
- if default_filters and json.loads(default_filters):
- filters = parse.quote(default_filters.encode('utf8'))
- return '/superset/dashboard/{}/?preselect_filters={}'.format(
- self.slug or self.id, filters)
+ # make sure default_filters is not empty and is valid
+ if default_filters and default_filters != '{}':
+ try:
+ if json.loads(default_filters):
+ filters = parse.quote(default_filters.encode('utf8'))
+ return '/superset/dashboard/{}/?preselect_filters={}'.format(
+ self.slug or self.id, filters)
+ except Exception:
+ pass
return '/superset/dashboard/{}/'.format(self.slug or self.id)
@property