diff --git a/packages/cubejs-client-core/dist/cubejs-client-core.js b/packages/cubejs-client-core/dist/cubejs-client-core.js index 05ab50a964cb1..8c2d24d064ac2 100644 --- a/packages/cubejs-client-core/dist/cubejs-client-core.js +++ b/packages/cubejs-client-core/dist/cubejs-client-core.js @@ -20,6 +20,9 @@ require('core-js/modules/es6.array.map'); var ramda = require('ramda'); var Moment = _interopDefault(require('moment')); var momentRange = _interopDefault(require('moment-range')); +require('core-js/modules/web.dom.iterable'); +require('core-js/modules/es6.array.iterator'); +require('core-js/modules/es6.object.keys'); require('core-js/modules/es6.array.is-array'); require('core-js/modules/es6.regexp.split'); require('core-js/modules/es6.function.name'); @@ -202,7 +205,7 @@ function () { var groupByXAxis = ramda.groupBy(function (_ref3) { var xValues = _ref3.xValues; return _this2.axisValuesString(xValues); - }); + }); // eslint-disable-next-line no-unused-vars var measureValue = function measureValue(row, measure, xValues) { return row[measure]; @@ -229,7 +232,8 @@ function () { }).reduce(function (a, b) { return Object.assign(a, b); }, {}); - }; + }; // eslint-disable-next-line no-unused-vars + measureValue = function measureValue(row, measure, xValues) { return row[measure] || 0; @@ -245,7 +249,8 @@ function () { }; }); }), ramda.unnest, groupByXAxis, ramda.toPairs)(this.loadResponse.data); - var allYValues = ramda.pipe(ramda.map(function (_ref6) { + var allYValues = ramda.pipe(ramda.map( // eslint-disable-next-line no-unused-vars + function (_ref6) { var _ref7 = _slicedToArray(_ref6, 2), xValuesString = _ref7[0], rows = _ref7[1]; @@ -254,7 +259,8 @@ function () { var row = _ref8.row; return _this2.axisValues(pivotConfig.y)(row); })); - }), ramda.unnest, ramda.uniq)(xGrouped); + }), ramda.unnest, ramda.uniq)(xGrouped); // eslint-disable-next-line no-unused-vars + return xGrouped.map(function (_ref9) { var _ref10 = _slicedToArray(_ref9, 2), xValuesString = _ref10[0], @@ -305,7 +311,7 @@ function () { yValuesArray = _ref14.yValuesArray; return _objectSpread({ category: _this3.axisValuesString(xValues, ', '), - //TODO deprecated + // TODO deprecated x: _this3.axisValuesString(xValues, ', ') }, yValuesArray.map(function (_ref15) { var _ref16 = _slicedToArray(_ref15, 2), @@ -318,6 +324,56 @@ function () { }, {})); }); } + }, { + key: "tablePivot", + value: function tablePivot(pivotConfig) { + var normalizedPivotConfig = this.normalizePivotConfig(pivotConfig); + + var valueToObject = function valueToObject(valuesArray, measureValue) { + return function (field, index) { + return _defineProperty({}, field === 'measures' ? valuesArray[index] : field, field === 'measures' ? measureValue : valuesArray[index]); + }; + }; + + return this.pivot(pivotConfig).map(function (_ref19) { + var xValues = _ref19.xValues, + yValuesArray = _ref19.yValuesArray; + return yValuesArray.map(function (_ref20) { + var _ref21 = _slicedToArray(_ref20, 2), + yValues = _ref21[0], + m = _ref21[1]; + + return normalizedPivotConfig.x.map(valueToObject(xValues, m)).concat(normalizedPivotConfig.y.map(valueToObject(yValues, m))).reduce(function (a, b) { + return Object.assign(a, b); + }, {}); + }); + }).reduce(function (a, b) { + return a.concat(b); + }); + } + }, { + key: "tableColumns", + value: function tableColumns(pivotConfig) { + var _this4 = this; + + var normalizedPivotConfig = this.normalizePivotConfig(pivotConfig); + + var column = function column(field) { + return field === 'measures' ? (_this4.query().measures || []).map(function (m) { + return { + key: m, + title: _this4.loadResponse.annotation.measures[m].title + }; + }) : [{ + key: field, + title: (_this4.loadResponse.annotation.dimensions[field] || _this4.loadResponse.annotation.timeDimensions[field]).title + }]; + }; + + return normalizedPivotConfig.x.map(column).concat(normalizedPivotConfig.y.map(column)).reduce(function (a, b) { + return a.concat(b); + }); + } }, { key: "totalRow", value: function totalRow() { @@ -326,21 +382,21 @@ function () { }, { key: "categories", value: function categories(pivotConfig) { - //TODO + // TODO return this.chartPivot(pivotConfig); } }, { key: "seriesNames", value: function seriesNames(pivotConfig) { - var _this4 = this; + var _this5 = this; pivotConfig = this.normalizePivotConfig(pivotConfig); return ramda.pipe(ramda.map(this.axisValues(pivotConfig.y)), ramda.unnest, ramda.uniq)(this.loadResponse.data).map(function (axisValues) { return { - title: _this4.axisValuesString(pivotConfig.y.find(function (d) { + title: _this5.axisValuesString(pivotConfig.y.find(function (d) { return d === 'measures'; - }) ? ramda.dropLast(1, axisValues).concat(_this4.loadResponse.annotation.measures[ResultSet.measureFromAxis(axisValues)].title) : axisValues, ', '), - key: _this4.axisValuesString(axisValues) + }) ? ramda.dropLast(1, axisValues).concat(_this5.loadResponse.annotation.measures[ResultSet.measureFromAxis(axisValues)].title) : axisValues, ', '), + key: _this5.axisValuesString(axisValues) }; }); } @@ -498,6 +554,23 @@ function () { return member; } + }, { + key: "defaultTimeDimensionNameFor", + value: function defaultTimeDimensionNameFor(memberName) { + var _this2 = this; + + var _memberName$split3 = memberName.split('.'), + _memberName$split4 = _slicedToArray(_memberName$split3, 1), + cube = _memberName$split4[0]; + + if (!this.cubesMap[cube]) { + return null; + } + + return Object.keys(this.cubesMap[cube].dimensions || {}).find(function (d) { + return _this2.cubesMap[cube].dimensions[d].type === 'time'; + }); + } }, { key: "filterOperatorsForMember", value: function filterOperatorsForMember(memberName, memberType) { diff --git a/packages/cubejs-client-core/dist/cubejs-client-core.umd.js b/packages/cubejs-client-core/dist/cubejs-client-core.umd.js index 489a5df0e6098..f074d33105814 100644 --- a/packages/cubejs-client-core/dist/cubejs-client-core.umd.js +++ b/packages/cubejs-client-core/dist/cubejs-client-core.umd.js @@ -14491,7 +14491,7 @@ var groupByXAxis = groupBy(function (_ref3) { var xValues = _ref3.xValues; return _this2.axisValuesString(xValues); - }); + }); // eslint-disable-next-line no-unused-vars var measureValue = function measureValue(row, measure, xValues) { return row[measure]; @@ -14518,7 +14518,8 @@ }).reduce(function (a, b) { return Object.assign(a, b); }, {}); - }; + }; // eslint-disable-next-line no-unused-vars + measureValue = function measureValue(row, measure, xValues) { return row[measure] || 0; @@ -14534,7 +14535,8 @@ }; }); }), unnest, groupByXAxis, toPairs)(this.loadResponse.data); - var allYValues = pipe(map(function (_ref6) { + var allYValues = pipe(map( // eslint-disable-next-line no-unused-vars + function (_ref6) { var _ref7 = _slicedToArray(_ref6, 2), xValuesString = _ref7[0], rows = _ref7[1]; @@ -14543,7 +14545,8 @@ var row = _ref8.row; return _this2.axisValues(pivotConfig.y)(row); })); - }), unnest, uniq)(xGrouped); + }), unnest, uniq)(xGrouped); // eslint-disable-next-line no-unused-vars + return xGrouped.map(function (_ref9) { var _ref10 = _slicedToArray(_ref9, 2), xValuesString = _ref10[0], @@ -14594,7 +14597,7 @@ yValuesArray = _ref14.yValuesArray; return _objectSpread({ category: _this3.axisValuesString(xValues, ', '), - //TODO deprecated + // TODO deprecated x: _this3.axisValuesString(xValues, ', ') }, yValuesArray.map(function (_ref15) { var _ref16 = _slicedToArray(_ref15, 2), @@ -14607,6 +14610,56 @@ }, {})); }); } + }, { + key: "tablePivot", + value: function tablePivot(pivotConfig) { + var normalizedPivotConfig = this.normalizePivotConfig(pivotConfig); + + var valueToObject = function valueToObject(valuesArray, measureValue) { + return function (field, index) { + return _defineProperty({}, field === 'measures' ? valuesArray[index] : field, field === 'measures' ? measureValue : valuesArray[index]); + }; + }; + + return this.pivot(pivotConfig).map(function (_ref19) { + var xValues = _ref19.xValues, + yValuesArray = _ref19.yValuesArray; + return yValuesArray.map(function (_ref20) { + var _ref21 = _slicedToArray(_ref20, 2), + yValues = _ref21[0], + m = _ref21[1]; + + return normalizedPivotConfig.x.map(valueToObject(xValues, m)).concat(normalizedPivotConfig.y.map(valueToObject(yValues, m))).reduce(function (a, b) { + return Object.assign(a, b); + }, {}); + }); + }).reduce(function (a, b) { + return a.concat(b); + }); + } + }, { + key: "tableColumns", + value: function tableColumns(pivotConfig) { + var _this4 = this; + + var normalizedPivotConfig = this.normalizePivotConfig(pivotConfig); + + var column = function column(field) { + return field === 'measures' ? (_this4.query().measures || []).map(function (m) { + return { + key: m, + title: _this4.loadResponse.annotation.measures[m].title + }; + }) : [{ + key: field, + title: (_this4.loadResponse.annotation.dimensions[field] || _this4.loadResponse.annotation.timeDimensions[field]).title + }]; + }; + + return normalizedPivotConfig.x.map(column).concat(normalizedPivotConfig.y.map(column)).reduce(function (a, b) { + return a.concat(b); + }); + } }, { key: "totalRow", value: function totalRow() { @@ -14615,21 +14668,21 @@ }, { key: "categories", value: function categories(pivotConfig) { - //TODO + // TODO return this.chartPivot(pivotConfig); } }, { key: "seriesNames", value: function seriesNames(pivotConfig) { - var _this4 = this; + var _this5 = this; pivotConfig = this.normalizePivotConfig(pivotConfig); return pipe(map(this.axisValues(pivotConfig.y)), unnest, uniq)(this.loadResponse.data).map(function (axisValues) { return { - title: _this4.axisValuesString(pivotConfig.y.find(function (d) { + title: _this5.axisValuesString(pivotConfig.y.find(function (d) { return d === 'measures'; - }) ? dropLast$1(1, axisValues).concat(_this4.loadResponse.annotation.measures[ResultSet.measureFromAxis(axisValues)].title) : axisValues, ', '), - key: _this4.axisValuesString(axisValues) + }) ? dropLast$1(1, axisValues).concat(_this5.loadResponse.annotation.measures[ResultSet.measureFromAxis(axisValues)].title) : axisValues, ', '), + key: _this5.axisValuesString(axisValues) }; }); } @@ -14787,6 +14840,23 @@ return member; } + }, { + key: "defaultTimeDimensionNameFor", + value: function defaultTimeDimensionNameFor(memberName) { + var _this2 = this; + + var _memberName$split3 = memberName.split('.'), + _memberName$split4 = _slicedToArray(_memberName$split3, 1), + cube = _memberName$split4[0]; + + if (!this.cubesMap[cube]) { + return null; + } + + return Object.keys(this.cubesMap[cube].dimensions || {}).find(function (d) { + return _this2.cubesMap[cube].dimensions[d].type === 'time'; + }); + } }, { key: "filterOperatorsForMember", value: function filterOperatorsForMember(memberName, memberType) { diff --git a/packages/cubejs-client-core/src/Meta.js b/packages/cubejs-client-core/src/Meta.js index 3367b8e8cecaa..696c2eeaea9e8 100644 --- a/packages/cubejs-client-core/src/Meta.js +++ b/packages/cubejs-client-core/src/Meta.js @@ -53,6 +53,15 @@ export default class Meta { return member; } + defaultTimeDimensionNameFor(memberName) { + const [cube] = memberName.split('.'); + if (!this.cubesMap[cube]) { + return null; + } + return Object.keys(this.cubesMap[cube].dimensions || {}) + .find(d => this.cubesMap[cube].dimensions[d].type === 'time'); + } + filterOperatorsForMember(memberName, memberType) { const member = this.resolveMember(memberName, memberType); return operators[member.type] || operators.string; diff --git a/packages/cubejs-client-core/src/ResultSet.js b/packages/cubejs-client-core/src/ResultSet.js index e153957f568f4..c0ad037771fc7 100644 --- a/packages/cubejs-client-core/src/ResultSet.js +++ b/packages/cubejs-client-core/src/ResultSet.js @@ -1,25 +1,22 @@ -import { groupBy, pipe, toPairs, uniq, filter, map, unnest, dropLast, equals, reduce, minBy, maxBy } from 'ramda'; +import { + groupBy, pipe, toPairs, uniq, filter, map, unnest, dropLast, equals, reduce, minBy, maxBy +} from 'ramda'; import Moment from 'moment'; import momentRange from 'moment-range'; const moment = momentRange.extendMoment(Moment); const TIME_SERIES = { - day: (range) => - Array.from(range.by('day')) - .map(d => d.format('YYYY-MM-DDT00:00:00.000')), - month: (range) => - Array.from(range.snapTo('month').by('month')) - .map(d => d.format('YYYY-MM-01T00:00:00.000')), - year: (range) => - Array.from(range.snapTo('year').by('year')) - .map(d => d.format('YYYY-01-01T00:00:00.000')), - hour: (range) => - Array.from(range.by('hour')) - .map(d => d.format('YYYY-MM-DDTHH:00:00.000')), - week: (range) => - Array.from(range.snapTo('isoweek').by('week')) - .map(d => d.startOf('isoweek').format('YYYY-MM-DDT00:00:00.000')) + day: (range) => Array.from(range.by('day')) + .map(d => d.format('YYYY-MM-DDT00:00:00.000')), + month: (range) => Array.from(range.snapTo('month').by('month')) + .map(d => d.format('YYYY-MM-01T00:00:00.000')), + year: (range) => Array.from(range.snapTo('year').by('year')) + .map(d => d.format('YYYY-01-01T00:00:00.000')), + hour: (range) => Array.from(range.by('hour')) + .map(d => d.format('YYYY-MM-DDTHH:00:00.000')), + week: (range) => Array.from(range.snapTo('isoweek').by('week')) + .map(d => d.startOf('isoweek').format('YYYY-MM-DDT00:00:00.000')) }; export default class ResultSet { @@ -35,11 +32,10 @@ export default class ResultSet { } axisValues(axis) { - const query = this.loadResponse.query; + const { query } = this.loadResponse; return row => { - const value = (measure) => - axis.filter(d => d !== 'measures') - .map(d => row[d] != null ? row[d] : null).concat(measure ? [measure] : []); + const value = (measure) => axis.filter(d => d !== 'measures') + .map(d => (row[d] != null ? row[d] : null)).concat(measure ? [measure] : []); if (axis.find(d => d === 'measures') && (query.measures || []).length) { return query.measures.map(value); } @@ -61,8 +57,8 @@ export default class ResultSet { } normalizePivotConfig(pivotConfig) { - const query = this.loadResponse.query; - let timeDimensions = (query.timeDimensions || []).filter(td => !!td.granularity); + const { query } = this.loadResponse; + const timeDimensions = (query.timeDimensions || []).filter(td => !!td.granularity); pivotConfig = pivotConfig || (timeDimensions.length ? { x: timeDimensions.map(td => td.dimension), y: query.dimensions || [] @@ -81,13 +77,13 @@ export default class ResultSet { static measureFromAxis(axisValues) { return axisValues[axisValues.length - 1]; - }; + } timeSeries(timeDimension) { if (!timeDimension.granularity) { return null; } - let dateRange = timeDimension.dateRange; + let { dateRange } = timeDimension; if (!dateRange) { const dates = pipe( map(row => row[timeDimension.dimension] && moment(row[timeDimension.dimension])), @@ -115,6 +111,7 @@ export default class ResultSet { pivotConfig = this.normalizePivotConfig(pivotConfig); let groupByXAxis = groupBy(({ xValues }) => this.axisValuesString(xValues)); + // eslint-disable-next-line no-unused-vars let measureValue = (row, measure, xValues) => row[measure]; if ( @@ -128,11 +125,15 @@ export default class ResultSet { const series = this.timeSeries(this.loadResponse.query.timeDimensions[0]); if (series) { groupByXAxis = (rows) => { - const byXValues = groupBy(({ xValues }) => moment(xValues[0]).format(moment.HTML5_FMT.DATETIME_LOCAL_MS), rows); + const byXValues = groupBy( + ({ xValues }) => moment(xValues[0]).format(moment.HTML5_FMT.DATETIME_LOCAL_MS), + rows + ); return series.map(d => ({ [d]: byXValues[d] || [{ xValues: [d], row: {} }] })) .reduce((a, b) => Object.assign(a, b), {}); }; + // eslint-disable-next-line no-unused-vars measureValue = (row, measure, xValues) => row[measure] || 0; } } @@ -146,14 +147,16 @@ export default class ResultSet { const allYValues = pipe( map( + // eslint-disable-next-line no-unused-vars ([xValuesString, rows]) => unnest(rows.map(({ row }) => this.axisValues(pivotConfig.y)(row))) ), unnest, uniq )(xGrouped); + // eslint-disable-next-line no-unused-vars return xGrouped.map(([xValuesString, rows]) => { - const xValues = rows[0].xValues; + const { xValues } = rows[0]; const yGrouped = pipe( map(({ row }) => this.axisValues(pivotConfig.y)(row).map(yValues => ({ yValues, row }))), unnest, @@ -162,10 +165,11 @@ export default class ResultSet { return { xValues, yValuesArray: unnest(allYValues.map(yValues => { - let measure = pivotConfig.x.find(d => d === 'measures') ? + const measure = pivotConfig.x.find(d => d === 'measures') ? ResultSet.measureFromAxis(xValues) : ResultSet.measureFromAxis(yValues); - return (yGrouped[this.axisValuesString(yValues)] || [{ row: {} }]).map(({ row }) => [yValues, measureValue(row, measure, xValues)]) + return (yGrouped[this.axisValuesString(yValues)] || + [{ row: {} }]).map(({ row }) => [yValues, measureValue(row, measure, xValues)]); })) }; }); @@ -177,7 +181,7 @@ export default class ResultSet { chartPivot(pivotConfig) { return this.pivot(pivotConfig).map(({ xValues, yValuesArray }) => ({ - category: this.axisValuesString(xValues, ', '), //TODO deprecated + category: this.axisValuesString(xValues, ', '), // TODO deprecated x: this.axisValuesString(xValues, ', '), ...( yValuesArray @@ -187,11 +191,47 @@ export default class ResultSet { })); } + tablePivot(pivotConfig) { + const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig); + const valueToObject = + (valuesArray, measureValue) => ( + (field, index) => ({ + [field === 'measures' ? valuesArray[index] : field]: field === 'measures' ? measureValue : valuesArray[index] + }) + ); + + return this.pivot(pivotConfig).map(({ xValues, yValuesArray }) => ( + yValuesArray.map(([yValues, m]) => ( + normalizedPivotConfig.x.map(valueToObject(xValues, m)) + .concat(normalizedPivotConfig.y.map(valueToObject(yValues, m))) + .reduce((a, b) => Object.assign(a, b), {}) + )) + )).reduce((a, b) => a.concat(b)); + } + + tableColumns(pivotConfig) { + const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig); + const column = field => ( + field === 'measures' ? + (this.query().measures || []).map(m => ({ key: m, title: this.loadResponse.annotation.measures[m].title })) : + [{ + key: field, + title: ( + this.loadResponse.annotation.dimensions[field] || + this.loadResponse.annotation.timeDimensions[field] + ).title + }] + ); + return normalizedPivotConfig.x.map(column) + .concat(normalizedPivotConfig.y.map(column)) + .reduce((a, b) => a.concat(b)); + } + totalRow() { return this.chartPivot()[0]; } - categories(pivotConfig) { //TODO + categories(pivotConfig) { // TODO return this.chartPivot(pivotConfig); } @@ -203,7 +243,7 @@ export default class ResultSet { .concat(this.loadResponse.annotation.measures[ResultSet.measureFromAxis(axisValues)].title) : axisValues, ', '), key: this.axisValuesString(axisValues) - })) + })); } query() { diff --git a/packages/cubejs-playground/src/DashboardSource.js b/packages/cubejs-playground/src/DashboardSource.js index 7e8111e279876..9d66258302ea9 100644 --- a/packages/cubejs-playground/src/DashboardSource.js +++ b/packages/cubejs-playground/src/DashboardSource.js @@ -90,7 +90,9 @@ class DashboardSource { this.definitions = []; traverse(this.appAst, { VariableDeclaration: (path) => { - this.definitions.push(...path.get('declarations')); + if (path.parent.type === 'Program') { + this.definitions.push(...path.get('declarations')); + } } }); } @@ -217,19 +219,20 @@ class DashboardSource { traverse(chartAst, { VariableDeclaration: (path) => { - if (path.get('declarations')[0].get('id').get('name').node === 'ChartRenderer') { - const chartRendererElement = path.get('declarations')[0].get('init').get('body'); - console.log(path.get('declarations')[0].get('init').get('body')); - this.dashboardElement.parentPath.pushContainer( - 'children', - t.JSXElement( - t.JSXOpeningElement(t.JSXIdentifier('DashboardItem'), []), - t.JSXClosingElement(t.JSXIdentifier('DashboardItem')), - [chartRendererElement.node] - ) - ); - } else { - definitions.push(path); + if (path.parent.type === 'Program') { + if (path.get('declarations')[0].get('id').get('name').node === 'ChartRenderer') { + const chartRendererElement = path.get('declarations')[0].get('init').get('body'); + this.dashboardElement.parentPath.pushContainer( + 'children', + t.JSXElement( + t.JSXOpeningElement(t.JSXIdentifier('DashboardItem'), []), + t.JSXClosingElement(t.JSXIdentifier('DashboardItem')), + [chartRendererElement.node] + ) + ); + } else { + definitions.push(path); + } } } }); diff --git a/packages/cubejs-playground/src/PlaygroundQueryBuilder.js b/packages/cubejs-playground/src/PlaygroundQueryBuilder.js index dc4242880643b..df805e037491b 100644 --- a/packages/cubejs-playground/src/PlaygroundQueryBuilder.js +++ b/packages/cubejs-playground/src/PlaygroundQueryBuilder.js @@ -325,8 +325,11 @@ TimeGroup.propTypes = { const ChartType = ({ chartType, updateChartType }) => { const chartTypes = [ { name: 'line', title: 'Line', icon: 'line-chart' }, + { name: 'area', title: 'Area', icon: 'area-chart' }, { name: 'bar', title: 'Bar', icon: 'bar-chart' }, - { name: 'pie', title: 'Pie', icon: 'pie-chart' } + { name: 'pie', title: 'Pie', icon: 'pie-chart' }, + { name: 'table', title: 'Table', icon: 'table' }, + { name: 'number', title: 'Number', icon: 'info-circle' } ]; const menu = ( diff --git a/packages/cubejs-playground/src/libraries/bizChart.js b/packages/cubejs-playground/src/libraries/bizChart.js index 780b1e8a7c21a..d7be271664370 100644 --- a/packages/cubejs-playground/src/libraries/bizChart.js +++ b/packages/cubejs-playground/src/libraries/bizChart.js @@ -3,18 +3,25 @@ import moment from 'moment'; const chartTypeToTemplate = { line: ` - - - {resultSet.seriesNames().map(s => ())} + + + - {resultSet.seriesNames().map(s => ())} + `, bar: ` - - - {resultSet.seriesNames().map(s => ())} + + + - {resultSet.seriesNames().map(s => ())} + + `, + area: ` + + + + + `, barStacked: ` @@ -31,14 +38,46 @@ const chartTypeToTemplate = { {resultSet.seriesNames().map(s => ())} - ` + `, + number: ` + + + {resultSet + .seriesNames() + .map(s => ( + + ))} + + + `, + table: ` + ({ ...c, dataIndex: c.key }))} + dataSource={resultSet.tablePivot()} + /> + ` }; export const sourceCodeTemplate = ({ chartType, renderFnName }) => ( `import { Chart, Axis, Tooltip, Geom, Coord, Legend } from 'bizcharts'; +import { Row, Col, Statistic, Table } from 'antd'; import moment from 'moment'; +const stackedChartData = (resultSet) => { + const data = resultSet.pivot().map( + ({ xValues, yValuesArray }) => + yValuesArray.map(([yValues, m]) => ({ + x: resultSet.axisValuesString(xValues, ', '), + color: resultSet.axisValuesString(yValues, ', '), + measure: m && Number.parseFloat(m) + })) + ).reduce((a, b) => a.concat(b)); + + return data; +} + const ${renderFnName} = ({ resultSet }) => (${chartTypeToTemplate[chartType]} );` ); diff --git a/packages/cubejs-react/dist/cubejs-react.js b/packages/cubejs-react/dist/cubejs-react.js index 81b10a8b2c448..44f7b9fde95e2 100644 --- a/packages/cubejs-react/dist/cubejs-react.js +++ b/packages/cubejs-react/dist/cubejs-react.js @@ -270,21 +270,22 @@ function (_React$Component) { var _componentDidMount = _asyncToGenerator( /*#__PURE__*/ _regeneratorRuntime.mark(function _callee() { - var meta; + var cubejsApi, meta; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: - _context.next = 2; - return this.props.cubejsApi.meta(); + cubejsApi = this.props.cubejsApi; + _context.next = 3; + return cubejsApi.meta(); - case 2: + case 3: meta = _context.sent; this.setState({ meta: meta }); - case 4: + case 5: case "end": return _context.stop(); } @@ -333,28 +334,35 @@ function (_React$Component) { var toQuery = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : getName; return { add: function add(member) { - return _this2.setState({ - query: _objectSpread({}, _this2.state.query, _defineProperty({}, memberType, (_this2.state.query[memberType] || []).concat(toQuery(member)))) - }); + var query = _this2.state.query; + + _this2.setState(_this2.applyQueryChangeHeuristics({ + query: _objectSpread({}, query, _defineProperty({}, memberType, (query[memberType] || []).concat(toQuery(member)))) + })); }, remove: function remove(member) { - var members = (_this2.state.query[memberType] || []).concat([]); + var query = _this2.state.query; + var members = (query[memberType] || []).concat([]); members.splice(member.index, 1); - return _this2.setState({ - query: _objectSpread({}, _this2.state.query, _defineProperty({}, memberType, members)) - }); + return _this2.setState(_this2.applyQueryChangeHeuristics({ + query: _objectSpread({}, query, _defineProperty({}, memberType, members)) + })); }, update: function update(member, updateWith) { - var members = (_this2.state.query[memberType] || []).concat([]); + var query = _this2.state.query; + var members = (query[memberType] || []).concat([]); members.splice(member.index, 1, toQuery(updateWith)); - return _this2.setState({ - query: _objectSpread({}, _this2.state.query, _defineProperty({}, memberType, members)) - }); + return _this2.setState(_this2.applyQueryChangeHeuristics({ + query: _objectSpread({}, query, _defineProperty({}, memberType, members)) + })); } }; }; var granularities = [{ + name: undefined, + title: 'w/o grouping' + }, { name: 'hour', title: 'Hour' }, { @@ -370,57 +378,61 @@ function (_React$Component) { name: 'year', title: 'Year' }]; + var _this$state = this.state, + meta = _this$state.meta, + query = _this$state.query, + chartType = _this$state.chartType; return _objectSpread({ - meta: this.state.meta, - query: this.state.query, + meta: meta, + query: query, validatedQuery: this.validatedQuery(), isQueryPresent: this.isQueryPresent(), - chartType: this.state.chartType, - measures: (this.state.meta && this.state.query.measures || []).map(function (m, i) { + chartType: chartType, + measures: (meta && query.measures || []).map(function (m, i) { return _objectSpread({ index: i - }, _this2.state.meta.resolveMember(m, 'measures')); + }, meta.resolveMember(m, 'measures')); }), - dimensions: (this.state.meta && this.state.query.dimensions || []).map(function (m, i) { + dimensions: (meta && query.dimensions || []).map(function (m, i) { return _objectSpread({ index: i - }, _this2.state.meta.resolveMember(m, 'dimensions')); + }, meta.resolveMember(m, 'dimensions')); }), - segments: (this.state.meta && this.state.query.segments || []).map(function (m, i) { + segments: (meta && query.segments || []).map(function (m, i) { return _objectSpread({ index: i - }, _this2.state.meta.resolveMember(m, 'segments')); + }, meta.resolveMember(m, 'segments')); }), - timeDimensions: (this.state.meta && this.state.query.timeDimensions || []).map(function (m, i) { + timeDimensions: (meta && query.timeDimensions || []).map(function (m, i) { return _objectSpread({}, m, { - dimension: _objectSpread({}, _this2.state.meta.resolveMember(m.dimension, 'dimensions'), { + dimension: _objectSpread({}, meta.resolveMember(m.dimension, 'dimensions'), { granularities: granularities }), index: i }); }), - filters: (this.state.meta && this.state.query.filters || []).map(function (m, i) { + filters: (meta && query.filters || []).map(function (m, i) { return _objectSpread({}, m, { - dimension: _this2.state.meta.resolveMember(m.dimension, ['dimensions', 'measures']), - operators: _this2.state.meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']), + dimension: meta.resolveMember(m.dimension, ['dimensions', 'measures']), + operators: meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']), index: i }); }), - availableMeasures: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'measures') || [], - availableDimensions: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'dimensions') || [], - availableTimeDimensions: (this.state.meta && this.state.meta.membersForQuery(this.state.query, 'dimensions') || []).filter(function (m) { + availableMeasures: meta && meta.membersForQuery(query, 'measures') || [], + availableDimensions: meta && meta.membersForQuery(query, 'dimensions') || [], + availableTimeDimensions: (meta && meta.membersForQuery(query, 'dimensions') || []).filter(function (m) { return m.type === 'time'; }), - availableSegments: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'segments') || [], + availableSegments: meta && meta.membersForQuery(query, 'segments') || [], updateMeasures: updateMethods('measures'), updateDimensions: updateMethods('dimensions'), updateSegments: updateMethods('segments'), updateTimeDimensions: updateMethods('timeDimensions', toTimeDimension), updateFilters: updateMethods('filters', toFilter), - updateChartType: function updateChartType(chartType) { - return _this2.setState({ - chartType: chartType - }); + updateChartType: function updateChartType(newChartType) { + return _this2.setState(_this2.applyQueryChangeHeuristics({ + chartType: newChartType + })); } }, queryRendererProps); } @@ -434,14 +446,134 @@ function (_React$Component) { }) }); } + }, { + key: "defaultHeuristics", + value: function defaultHeuristics(newState) { + var _this$state2 = this.state, + query = _this$state2.query, + sessionGranularity = _this$state2.sessionGranularity; + var defaultGranularity = sessionGranularity || 'day'; + + if (newState.query) { + var oldQuery = query; + var newQuery = newState.query; + var meta = this.state.meta; + + if ((oldQuery.timeDimensions || []).length === 1 && (newQuery.timeDimensions || []).length === 1 && newQuery.timeDimensions[0].granularity && oldQuery.timeDimensions[0].granularity !== newQuery.timeDimensions[0].granularity) { + newState = _objectSpread({}, newState, { + sessionGranularity: newQuery.timeDimensions[0].granularity + }); + } + + if ((oldQuery.measures || []).length === 0 && (newQuery.measures || []).length > 0 || (oldQuery.measures || []).length === 1 && (newQuery.measures || []).length === 1 && oldQuery.measures[0] !== newQuery.measures[0]) { + var defaultTimeDimension = meta.defaultTimeDimensionNameFor(newQuery.measures[0]); + newQuery = _objectSpread({}, newQuery, { + timeDimensions: defaultTimeDimension ? [{ + dimension: defaultTimeDimension, + granularity: defaultGranularity + }] : [] + }); + return _objectSpread({}, newState, { + query: newQuery, + chartType: defaultTimeDimension ? 'line' : 'number' + }); + } + + if ((oldQuery.dimensions || []).length === 0 && (newQuery.dimensions || []).length > 0) { + newQuery = _objectSpread({}, newQuery, { + timeDimensions: (newQuery.timeDimensions || []).map(function (td) { + return _objectSpread({}, td, { + granularity: undefined + }); + }) + }); + return _objectSpread({}, newState, { + query: newQuery, + chartType: 'table' + }); + } + + if ((oldQuery.dimensions || []).length > 0 && (newQuery.dimensions || []).length === 0) { + newQuery = _objectSpread({}, newQuery, { + timeDimensions: (newQuery.timeDimensions || []).map(function (td) { + return _objectSpread({}, td, { + granularity: td.granularity || defaultGranularity + }); + }) + }); + return _objectSpread({}, newState, { + query: newQuery, + chartType: (newQuery.timeDimensions || []).length ? 'line' : 'number' + }); + } + + if (((oldQuery.dimensions || []).length > 0 || (oldQuery.measures || []).length > 0) && (newQuery.dimensions || []).length === 0 && (newQuery.measures || []).length === 0) { + newQuery = _objectSpread({}, newQuery, { + timeDimensions: [], + filters: [] + }); + return _objectSpread({}, newState, { + query: newQuery, + sessionGranularity: null + }); + } + + return newState; + } + + if (newState.chartType) { + var newChartType = newState.chartType; + + if ((newChartType === 'line' || newChartType === 'area') && (query.timeDimensions || []).length === 1 && !query.timeDimensions[0].granularity) { + var _query$timeDimensions = _slicedToArray(query.timeDimensions, 1), + td = _query$timeDimensions[0]; + + return _objectSpread({}, newState, { + query: _objectSpread({}, query, { + timeDimensions: [_objectSpread({}, td, { + granularity: defaultGranularity + })] + }) + }); + } + + if ((newChartType === 'pie' || newChartType === 'table' || newChartType === 'number') && (query.timeDimensions || []).length === 1 && query.timeDimensions[0].granularity) { + var _query$timeDimensions2 = _slicedToArray(query.timeDimensions, 1), + _td = _query$timeDimensions2[0]; + + return _objectSpread({}, newState, { + query: _objectSpread({}, query, { + timeDimensions: [_objectSpread({}, _td, { + granularity: undefined + })] + }) + }); + } + } + + return newState; + } + }, { + key: "applyQueryChangeHeuristics", + value: function applyQueryChangeHeuristics(newState) { + var _this$props = this.props, + queryChangeHeuristics = _this$props.queryChangeHeuristics, + disableHeuristics = _this$props.disableHeuristics; + + if (disableHeuristics) { + return newState; + } + + return queryChangeHeuristics && queryChangeHeuristics(this.state, newState) || this.defaultHeuristics(newState); + } }, { key: "render", value: function render() { var _this3 = this; - var _this$props = this.props, - cubejsApi = _this$props.cubejsApi, - _render = _this$props.render; + var _this$props2 = this.props, + cubejsApi = _this$props2.cubejsApi, + _render = _this$props2.render; return React.createElement(QueryRenderer, { query: this.validatedQuery(), cubejsApi: cubejsApi, @@ -458,6 +590,19 @@ function (_React$Component) { return QueryBuilder; }(React.Component); +QueryBuilder.propTypes = { + render: PropTypes.func, + queryChangeHeuristics: PropTypes.func, + cubejsApi: PropTypes.object.isRequired, + disableHeuristics: PropTypes.bool, + query: PropTypes.object +}; +QueryBuilder.defaultProps = { + query: {}, + queryChangeHeuristics: null, + disableHeuristics: false, + render: null +}; exports.QueryRenderer = QueryRenderer; exports.QueryRendererWithTotals = QueryRendererWithTotals; diff --git a/packages/cubejs-react/dist/cubejs-react.umd.js b/packages/cubejs-react/dist/cubejs-react.umd.js index a8cbff620ae9c..d7986a35ac161 100644 --- a/packages/cubejs-react/dist/cubejs-react.umd.js +++ b/packages/cubejs-react/dist/cubejs-react.umd.js @@ -126,7 +126,7 @@ return store[key] || (store[key] = value !== undefined ? value : {}); })('versions', []).push({ version: _core.version, - mode: 'global', + mode: _library ? 'pure' : 'global', copyright: '© 2019 Denis Pushkarev (zloirock.ru)' }); }); @@ -1191,10 +1191,10 @@ return capability.promise; } }); - _export(_export.S + _export.F * (_library || !USE_NATIVE), PROMISE, { + _export(_export.S + _export.F * (!USE_NATIVE), PROMISE, { // 25.4.4.6 Promise.resolve(x) resolve: function resolve(x) { - return _promiseResolve(_library && this === Wrapper ? $Promise : this, x); + return _promiseResolve(this, x); } }); _export(_export.S + _export.F * !(USE_NATIVE && _iterDetect(function (iter) { @@ -7764,21 +7764,22 @@ var _componentDidMount = _asyncToGenerator( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() { - var meta; + var cubejsApi, meta; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: - _context.next = 2; - return this.props.cubejsApi.meta(); + cubejsApi = this.props.cubejsApi; + _context.next = 3; + return cubejsApi.meta(); - case 2: + case 3: meta = _context.sent; this.setState({ meta: meta }); - case 4: + case 5: case "end": return _context.stop(); } @@ -7827,28 +7828,35 @@ var toQuery = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : getName; return { add: function add(member) { - return _this2.setState({ - query: _objectSpread({}, _this2.state.query, _defineProperty({}, memberType, (_this2.state.query[memberType] || []).concat(toQuery(member)))) - }); + var query = _this2.state.query; + + _this2.setState(_this2.applyQueryChangeHeuristics({ + query: _objectSpread({}, query, _defineProperty({}, memberType, (query[memberType] || []).concat(toQuery(member)))) + })); }, remove: function remove(member) { - var members = (_this2.state.query[memberType] || []).concat([]); + var query = _this2.state.query; + var members = (query[memberType] || []).concat([]); members.splice(member.index, 1); - return _this2.setState({ - query: _objectSpread({}, _this2.state.query, _defineProperty({}, memberType, members)) - }); + return _this2.setState(_this2.applyQueryChangeHeuristics({ + query: _objectSpread({}, query, _defineProperty({}, memberType, members)) + })); }, update: function update(member, updateWith) { - var members = (_this2.state.query[memberType] || []).concat([]); + var query = _this2.state.query; + var members = (query[memberType] || []).concat([]); members.splice(member.index, 1, toQuery(updateWith)); - return _this2.setState({ - query: _objectSpread({}, _this2.state.query, _defineProperty({}, memberType, members)) - }); + return _this2.setState(_this2.applyQueryChangeHeuristics({ + query: _objectSpread({}, query, _defineProperty({}, memberType, members)) + })); } }; }; var granularities = [{ + name: undefined, + title: 'w/o grouping' + }, { name: 'hour', title: 'Hour' }, { @@ -7864,57 +7872,61 @@ name: 'year', title: 'Year' }]; + var _this$state = this.state, + meta = _this$state.meta, + query = _this$state.query, + chartType = _this$state.chartType; return _objectSpread({ - meta: this.state.meta, - query: this.state.query, + meta: meta, + query: query, validatedQuery: this.validatedQuery(), isQueryPresent: this.isQueryPresent(), - chartType: this.state.chartType, - measures: (this.state.meta && this.state.query.measures || []).map(function (m, i) { + chartType: chartType, + measures: (meta && query.measures || []).map(function (m, i) { return _objectSpread({ index: i - }, _this2.state.meta.resolveMember(m, 'measures')); + }, meta.resolveMember(m, 'measures')); }), - dimensions: (this.state.meta && this.state.query.dimensions || []).map(function (m, i) { + dimensions: (meta && query.dimensions || []).map(function (m, i) { return _objectSpread({ index: i - }, _this2.state.meta.resolveMember(m, 'dimensions')); + }, meta.resolveMember(m, 'dimensions')); }), - segments: (this.state.meta && this.state.query.segments || []).map(function (m, i) { + segments: (meta && query.segments || []).map(function (m, i) { return _objectSpread({ index: i - }, _this2.state.meta.resolveMember(m, 'segments')); + }, meta.resolveMember(m, 'segments')); }), - timeDimensions: (this.state.meta && this.state.query.timeDimensions || []).map(function (m, i) { + timeDimensions: (meta && query.timeDimensions || []).map(function (m, i) { return _objectSpread({}, m, { - dimension: _objectSpread({}, _this2.state.meta.resolveMember(m.dimension, 'dimensions'), { + dimension: _objectSpread({}, meta.resolveMember(m.dimension, 'dimensions'), { granularities: granularities }), index: i }); }), - filters: (this.state.meta && this.state.query.filters || []).map(function (m, i) { + filters: (meta && query.filters || []).map(function (m, i) { return _objectSpread({}, m, { - dimension: _this2.state.meta.resolveMember(m.dimension, ['dimensions', 'measures']), - operators: _this2.state.meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']), + dimension: meta.resolveMember(m.dimension, ['dimensions', 'measures']), + operators: meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']), index: i }); }), - availableMeasures: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'measures') || [], - availableDimensions: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'dimensions') || [], - availableTimeDimensions: (this.state.meta && this.state.meta.membersForQuery(this.state.query, 'dimensions') || []).filter(function (m) { + availableMeasures: meta && meta.membersForQuery(query, 'measures') || [], + availableDimensions: meta && meta.membersForQuery(query, 'dimensions') || [], + availableTimeDimensions: (meta && meta.membersForQuery(query, 'dimensions') || []).filter(function (m) { return m.type === 'time'; }), - availableSegments: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'segments') || [], + availableSegments: meta && meta.membersForQuery(query, 'segments') || [], updateMeasures: updateMethods('measures'), updateDimensions: updateMethods('dimensions'), updateSegments: updateMethods('segments'), updateTimeDimensions: updateMethods('timeDimensions', toTimeDimension), updateFilters: updateMethods('filters', toFilter), - updateChartType: function updateChartType(chartType) { - return _this2.setState({ - chartType: chartType - }); + updateChartType: function updateChartType(newChartType) { + return _this2.setState(_this2.applyQueryChangeHeuristics({ + chartType: newChartType + })); } }, queryRendererProps); } @@ -7928,14 +7940,134 @@ }) }); } + }, { + key: "defaultHeuristics", + value: function defaultHeuristics(newState) { + var _this$state2 = this.state, + query = _this$state2.query, + sessionGranularity = _this$state2.sessionGranularity; + var defaultGranularity = sessionGranularity || 'day'; + + if (newState.query) { + var oldQuery = query; + var newQuery = newState.query; + var meta = this.state.meta; + + if ((oldQuery.timeDimensions || []).length === 1 && (newQuery.timeDimensions || []).length === 1 && newQuery.timeDimensions[0].granularity && oldQuery.timeDimensions[0].granularity !== newQuery.timeDimensions[0].granularity) { + newState = _objectSpread({}, newState, { + sessionGranularity: newQuery.timeDimensions[0].granularity + }); + } + + if ((oldQuery.measures || []).length === 0 && (newQuery.measures || []).length > 0 || (oldQuery.measures || []).length === 1 && (newQuery.measures || []).length === 1 && oldQuery.measures[0] !== newQuery.measures[0]) { + var defaultTimeDimension = meta.defaultTimeDimensionNameFor(newQuery.measures[0]); + newQuery = _objectSpread({}, newQuery, { + timeDimensions: defaultTimeDimension ? [{ + dimension: defaultTimeDimension, + granularity: defaultGranularity + }] : [] + }); + return _objectSpread({}, newState, { + query: newQuery, + chartType: defaultTimeDimension ? 'line' : 'number' + }); + } + + if ((oldQuery.dimensions || []).length === 0 && (newQuery.dimensions || []).length > 0) { + newQuery = _objectSpread({}, newQuery, { + timeDimensions: (newQuery.timeDimensions || []).map(function (td) { + return _objectSpread({}, td, { + granularity: undefined + }); + }) + }); + return _objectSpread({}, newState, { + query: newQuery, + chartType: 'table' + }); + } + + if ((oldQuery.dimensions || []).length > 0 && (newQuery.dimensions || []).length === 0) { + newQuery = _objectSpread({}, newQuery, { + timeDimensions: (newQuery.timeDimensions || []).map(function (td) { + return _objectSpread({}, td, { + granularity: td.granularity || defaultGranularity + }); + }) + }); + return _objectSpread({}, newState, { + query: newQuery, + chartType: (newQuery.timeDimensions || []).length ? 'line' : 'number' + }); + } + + if (((oldQuery.dimensions || []).length > 0 || (oldQuery.measures || []).length > 0) && (newQuery.dimensions || []).length === 0 && (newQuery.measures || []).length === 0) { + newQuery = _objectSpread({}, newQuery, { + timeDimensions: [], + filters: [] + }); + return _objectSpread({}, newState, { + query: newQuery, + sessionGranularity: null + }); + } + + return newState; + } + + if (newState.chartType) { + var newChartType = newState.chartType; + + if ((newChartType === 'line' || newChartType === 'area') && (query.timeDimensions || []).length === 1 && !query.timeDimensions[0].granularity) { + var _query$timeDimensions = _slicedToArray(query.timeDimensions, 1), + td = _query$timeDimensions[0]; + + return _objectSpread({}, newState, { + query: _objectSpread({}, query, { + timeDimensions: [_objectSpread({}, td, { + granularity: defaultGranularity + })] + }) + }); + } + + if ((newChartType === 'pie' || newChartType === 'table' || newChartType === 'number') && (query.timeDimensions || []).length === 1 && query.timeDimensions[0].granularity) { + var _query$timeDimensions2 = _slicedToArray(query.timeDimensions, 1), + _td = _query$timeDimensions2[0]; + + return _objectSpread({}, newState, { + query: _objectSpread({}, query, { + timeDimensions: [_objectSpread({}, _td, { + granularity: undefined + })] + }) + }); + } + } + + return newState; + } + }, { + key: "applyQueryChangeHeuristics", + value: function applyQueryChangeHeuristics(newState) { + var _this$props = this.props, + queryChangeHeuristics = _this$props.queryChangeHeuristics, + disableHeuristics = _this$props.disableHeuristics; + + if (disableHeuristics) { + return newState; + } + + return queryChangeHeuristics && queryChangeHeuristics(this.state, newState) || this.defaultHeuristics(newState); + } }, { key: "render", value: function render() { var _this3 = this; - var _this$props = this.props, - cubejsApi = _this$props.cubejsApi, - _render = _this$props.render; + var _this$props2 = this.props, + cubejsApi = _this$props2.cubejsApi, + _render = _this$props2.render; return React.createElement(QueryRenderer, { query: this.validatedQuery(), cubejsApi: cubejsApi, @@ -7952,6 +8084,19 @@ return QueryBuilder; }(React.Component); + QueryBuilder.propTypes = { + render: PropTypes.func, + queryChangeHeuristics: PropTypes.func, + cubejsApi: PropTypes.object.isRequired, + disableHeuristics: PropTypes.bool, + query: PropTypes.object + }; + QueryBuilder.defaultProps = { + query: {}, + queryChangeHeuristics: null, + disableHeuristics: false, + render: null + }; exports.QueryRenderer = QueryRenderer; exports.QueryRendererWithTotals = QueryRendererWithTotals; diff --git a/packages/cubejs-react/src/QueryBuilder.jsx b/packages/cubejs-react/src/QueryBuilder.jsx index 5fd696015819e..479b1991d970e 100644 --- a/packages/cubejs-react/src/QueryBuilder.jsx +++ b/packages/cubejs-react/src/QueryBuilder.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import * as PropTypes from 'prop-types'; import QueryRenderer from './QueryRenderer.jsx'; export default class QueryBuilder extends React.Component { @@ -11,7 +12,8 @@ export default class QueryBuilder extends React.Component { } async componentDidMount() { - const meta = await this.props.cubejsApi.meta(); + const { cubejsApi } = this.props; + const meta = await cubejsApi.meta(); this.setState({ meta }); } @@ -35,35 +37,41 @@ export default class QueryBuilder extends React.Component { values: member.values }); const updateMethods = (memberType, toQuery = getName) => ({ - add: (member) => this.setState({ - query: { - ...this.state.query, - [memberType]: (this.state.query[memberType] || []).concat(toQuery(member)) - } - }), + add: (member) => { + const { query } = this.state; + this.setState(this.applyStateChangeHeuristics({ + query: { + ...query, + [memberType]: (query[memberType] || []).concat(toQuery(member)) + } + })); + }, remove: (member) => { - const members = (this.state.query[memberType] || []).concat([]); + const { query } = this.state; + const members = (query[memberType] || []).concat([]); members.splice(member.index, 1); - return this.setState({ + return this.setState(this.applyStateChangeHeuristics({ query: { - ...this.state.query, + ...query, [memberType]: members } - }); + })); }, update: (member, updateWith) => { - const members = (this.state.query[memberType] || []).concat([]); + const { query } = this.state; + const members = (query[memberType] || []).concat([]); members.splice(member.index, 1, toQuery(updateWith)); - return this.setState({ + return this.setState(this.applyStateChangeHeuristics({ query: { - ...this.state.query, + ...query, [memberType]: members } - }); + })); } }); const granularities = [ + { name: undefined, title: 'w/o grouping' }, { name: 'hour', title: 'Hour' }, { name: 'day', title: 'Day' }, { name: 'week', title: 'Week' }, @@ -71,44 +79,46 @@ export default class QueryBuilder extends React.Component { { name: 'year', title: 'Year' } ]; + const { meta, query, chartType } = this.state; + return { - meta: this.state.meta, - query: this.state.query, + meta, + query, validatedQuery: this.validatedQuery(), isQueryPresent: this.isQueryPresent(), - chartType: this.state.chartType, - measures: (this.state.meta && this.state.query.measures || []) - .map((m, i) => ({ index: i, ...this.state.meta.resolveMember(m, 'measures') })), - dimensions: (this.state.meta && this.state.query.dimensions || []) - .map((m, i) => ({ index: i, ...this.state.meta.resolveMember(m, 'dimensions') })), - segments: (this.state.meta && this.state.query.segments || []) - .map((m, i) => ({ index: i, ...this.state.meta.resolveMember(m, 'segments') })), - timeDimensions: (this.state.meta && this.state.query.timeDimensions || []) + chartType, + measures: (meta && query.measures || []) + .map((m, i) => ({ index: i, ...meta.resolveMember(m, 'measures') })), + dimensions: (meta && query.dimensions || []) + .map((m, i) => ({ index: i, ...meta.resolveMember(m, 'dimensions') })), + segments: (meta && query.segments || []) + .map((m, i) => ({ index: i, ...meta.resolveMember(m, 'segments') })), + timeDimensions: (meta && query.timeDimensions || []) .map((m, i) => ({ ...m, - dimension: { ...this.state.meta.resolveMember(m.dimension, 'dimensions'), granularities }, + dimension: { ...meta.resolveMember(m.dimension, 'dimensions'), granularities }, index: i })), - filters: (this.state.meta && this.state.query.filters || []) + filters: (meta && query.filters || []) .map((m, i) => ({ ...m, - dimension: this.state.meta.resolveMember(m.dimension, ['dimensions', 'measures']), - operators: this.state.meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']), + dimension: meta.resolveMember(m.dimension, ['dimensions', 'measures']), + operators: meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']), index: i })), - availableMeasures: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'measures') || [], - availableDimensions: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'dimensions') || [], + availableMeasures: meta && meta.membersForQuery(query, 'measures') || [], + availableDimensions: meta && meta.membersForQuery(query, 'dimensions') || [], availableTimeDimensions: ( - this.state.meta && this.state.meta.membersForQuery(this.state.query, 'dimensions') || [] + meta && meta.membersForQuery(query, 'dimensions') || [] ).filter(m => m.type === 'time'), - availableSegments: this.state.meta && this.state.meta.membersForQuery(this.state.query, 'segments') || [], + availableSegments: meta && meta.membersForQuery(query, 'segments') || [], updateMeasures: updateMethods('measures'), updateDimensions: updateMethods('dimensions'), updateSegments: updateMethods('segments'), updateTimeDimensions: updateMethods('timeDimensions', toTimeDimension), updateFilters: updateMethods('filters', toFilter), - updateChartType: (chartType) => this.setState({ chartType }), + updateChartType: (newChartType) => this.setState(this.applyStateChangeHeuristics({ chartType: newChartType })), ...queryRendererProps }; } @@ -121,6 +131,149 @@ export default class QueryBuilder extends React.Component { }; } + defaultHeuristics(newState) { + const { query, sessionGranularity } = this.state; + const defaultGranularity = sessionGranularity || 'day'; + if (newState.query) { + const oldQuery = query; + let newQuery = newState.query; + + const { meta } = this.state; + + if ( + (oldQuery.timeDimensions || []).length === 1 + && (newQuery.timeDimensions || []).length === 1 + && newQuery.timeDimensions[0].granularity + && oldQuery.timeDimensions[0].granularity !== newQuery.timeDimensions[0].granularity + ) { + newState = { + ...newState, + sessionGranularity: newQuery.timeDimensions[0].granularity + }; + } + + if ( + (oldQuery.measures || []).length === 0 && (newQuery.measures || []).length > 0 + || ( + (oldQuery.measures || []).length === 1 + && (newQuery.measures || []).length === 1 + && oldQuery.measures[0] !== newQuery.measures[0] + ) + ) { + const defaultTimeDimension = meta.defaultTimeDimensionNameFor(newQuery.measures[0]); + newQuery = { + ...newQuery, + timeDimensions: defaultTimeDimension ? [{ + dimension: defaultTimeDimension, + granularity: defaultGranularity + }] : [], + }; + return { + ...newState, + query: newQuery, + chartType: defaultTimeDimension ? 'line' : 'number' + }; + } + + if ( + (oldQuery.dimensions || []).length === 0 + && (newQuery.dimensions || []).length > 0 + ) { + newQuery = { + ...newQuery, + timeDimensions: (newQuery.timeDimensions || []).map(td => ({ ...td, granularity: undefined })), + }; + return { + ...newState, + query: newQuery, + chartType: 'table' + }; + } + + if ( + (oldQuery.dimensions || []).length > 0 + && (newQuery.dimensions || []).length === 0 + ) { + newQuery = { + ...newQuery, + timeDimensions: (newQuery.timeDimensions || []).map(td => ({ + ...td, granularity: td.granularity || defaultGranularity + })), + }; + return { + ...newState, + query: newQuery, + chartType: (newQuery.timeDimensions || []).length ? 'line' : 'number' + }; + } + + if ( + ( + (oldQuery.dimensions || []).length > 0 + || (oldQuery.measures || []).length > 0 + ) + && (newQuery.dimensions || []).length === 0 + && (newQuery.measures || []).length === 0 + ) { + newQuery = { + ...newQuery, + timeDimensions: [], + filters: [] + }; + return { + ...newState, + query: newQuery, + sessionGranularity: null + }; + } + return newState; + } + + if (newState.chartType) { + const newChartType = newState.chartType; + if ( + (newChartType === 'line' || newChartType === 'area') + && (query.timeDimensions || []).length === 1 + && !query.timeDimensions[0].granularity + ) { + const [td] = query.timeDimensions; + return { + ...newState, + query: { + ...query, + timeDimensions: [{ ...td, granularity: defaultGranularity }] + } + }; + } + + if ( + (newChartType === 'pie' || newChartType === 'table' || newChartType === 'number') + && (query.timeDimensions || []).length === 1 + && query.timeDimensions[0].granularity + ) { + const [td] = query.timeDimensions; + return { + ...newState, + query: { + ...query, + timeDimensions: [{ ...td, granularity: undefined }] + } + }; + } + } + + return newState; + } + + applyStateChangeHeuristics(newState) { + const { stateChangeHeuristics, disableHeuristics } = this.props; + if (disableHeuristics) { + return newState; + } + return stateChangeHeuristics && stateChangeHeuristics(this.state, newState) + || this.defaultHeuristics(newState); + } + render() { const { cubejsApi, render } = this.props; return ( @@ -137,3 +290,18 @@ export default class QueryBuilder extends React.Component { ); } } + +QueryBuilder.propTypes = { + render: PropTypes.func, + stateChangeHeuristics: PropTypes.func, + cubejsApi: PropTypes.object.isRequired, + disableHeuristics: PropTypes.bool, + query: PropTypes.object +}; + +QueryBuilder.defaultProps = { + query: {}, + stateChangeHeuristics: null, + disableHeuristics: false, + render: null +};