Skip to content

Commit

Permalink
Add Global mode to CategoryWidget & PieWidget (#370)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergio Clebal authored Apr 19, 2022
1 parent b37a86a commit 4a6fbdb
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 226 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Not released

- Add Global mode to HistogramWidget [#371](https://github.com/CartoDB/carto-react/pull/371)
- Add Global mode to CategoryWidget & PieWidget [#370](https://github.com/CartoDB/carto-react/pull/370)
- Add Global mode to FormulaWidget [#368](https://github.com/CartoDB/carto-react/pull/368)

## 1.3
Expand Down
111 changes: 76 additions & 35 deletions packages/react-widgets/__tests__/models/CategoryModel.test.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,93 @@
import { getCategories } from '../../src/models/CategoryModel';
import { AggregationTypes } from '@carto/react-core';
import { Methods, executeTask } from '@carto/react-workers';
import { executeSQL } from '@carto/react-api/';

const RESULT = [
{ name: 'a', value: 2 },
{ name: 'b', value: 1 }
];

jest.mock('@carto/react-api', () => ({
executeSQL: jest
.fn()
.mockImplementation(() => new Promise((resolve) => resolve(RESULT)))
}));

jest.mock('@carto/react-workers', () => ({
executeTask: jest.fn(),
executeTask: jest
.fn()
.mockImplementation(() => new Promise((resolve) => resolve(RESULT))),
Methods: {
FEATURES_CATEGORY: 'featuresCategory'
}
}));

describe('getCategories', () => {
describe('should correctly handle viewport features', () => {
const categoriesParams = {
column: 'storetype',
operationColumn: 'revenue',
operation: AggregationTypes.COUNT,
filters: {},
dataSource: 'whatever-data-source'
};

test('correctly returns data', async () => {
executeTask.mockImplementation(() =>
Promise.resolve([
{ name: 'a', value: 2 },
{ name: 'b', value: 1 }
])
describe('local mode', () => {
test('should work correctly', async () => {
const props = {
source: {
id: '__test__',
type: 'query',
data: 'SELECT * FROM test',
credentials: {
apiVersion: 'v2'
}
},
operation: AggregationTypes.SUM,
column: 'column_1'
};

const data = await getCategories(props);

expect(data).toBe(RESULT);

expect(executeTask).toHaveBeenCalledWith(
props.source.id,
Methods.FEATURES_CATEGORY,
{
filters: props.source.filters,
filtersLogicalOperator: props.source.filtersLogicalOperator,
operation: props.operation,
joinOperation: props.joinOperation,
column: props.column,
operationColumn: props.column
}
);
const categories = await getCategories(categoriesParams);
expect(categories).toEqual([
{ name: 'a', value: 2 },
{ name: 'b', value: 1 }
]);
});
});

describe('global mode', () => {
test('should work correctly', async () => {
const props = {
source: {
id: '__test__',
type: 'table',
data: '__test__',
credentials: {
apiVersion: 'v3',
accessToken: '__test_token__'
},
connection: '__test_connection__'
},
operation: AggregationTypes.SUM,
column: 'column_1',
operationColumn: 'column_2',
global: true
};

const data = await getCategories(props);

expect(data).toBe(RESULT);

test('correctly called', async () => {
const {
column,
operationColumn,
operation,
filters,
dataSource
} = categoriesParams;
await getCategories(categoriesParams);
expect(executeTask).toHaveBeenCalledWith(dataSource, Methods.FEATURES_CATEGORY, {
column,
filters,
operation,
operationColumn
expect(executeSQL).toHaveBeenCalledWith({
credentials: props.source.credentials,
query: `SELECT COALESCE(column_1, 'null') as name, sum(column_2) as value FROM __test__ GROUP BY column_1`,
connection: props.source.connection,
opts: {
abortController: undefined
}
});
});
});
Expand Down
103 changes: 81 additions & 22 deletions packages/react-widgets/__tests__/models/HistogramModel.test.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,97 @@
import { getHistogram } from '../../src/models/HistogramModel';
import { AggregationTypes } from '@carto/react-core';
import { Methods, executeTask } from '@carto/react-workers';
import { executeSQL } from '@carto/react-api/';

const TICKS = [1, 2, 3];
const RESULT = [3, 1, 2, 0];

const MOCK_SQL_RESPONSE = Array(TICKS.length)
.fill(null)
// Only results [0,2] are used, because we're mocking the case
// when SQL doesn't have values for the last tick
.map((_, idx) => ({ tick: idx, value: RESULT[idx] }));

jest.mock('@carto/react-api', () => ({
executeSQL: jest
.fn()
.mockImplementation(() => new Promise((resolve) => resolve(MOCK_SQL_RESPONSE)))
}));

jest.mock('@carto/react-workers', () => ({
executeTask: jest.fn(),
executeTask: jest
.fn()
.mockImplementation(() => new Promise((resolve) => resolve(RESULT))),
Methods: {
FEATURES_HISTOGRAM: 'featuresHistogram'
}
}));

describe('getHistogram', () => {
describe('should correctly handle viewport features', () => {
const histogramParams = {
column: 'revenue',
operation: AggregationTypes.COUNT,
ticks: [0, 1, 2],
filters: {},
dataSource: 'whatever-data-source'
};

test('correctly returns data', async () => {
executeTask.mockImplementation(() => Promise.resolve([0, 1, 2, 1]));
const histogram = await getHistogram(histogramParams);
expect(histogram).toEqual([0, 1, 2, 1]);
describe('local mode', () => {
test('should work correctly', async () => {
const props = {
source: {
id: '__test__',
type: 'query',
data: 'SELECT * FROM test',
credentials: {
apiVersion: 'v2'
}
},
ticks: TICKS,
operation: AggregationTypes.COUNT,
column: 'column_1'
};

const data = await getHistogram(props);

expect(data).toBe(RESULT);

expect(executeTask).toHaveBeenCalledWith(
props.source.id,
Methods.FEATURES_HISTOGRAM,
{
filters: props.source.filters,
filtersLogicalOperator: props.source.filtersLogicalOperator,
operation: props.operation,
column: props.column,
ticks: props.ticks
}
);
});
});

describe('global mode', () => {
test('should work correctly', async () => {
const props = {
source: {
id: '__test__',
type: 'table',
data: '__test__',
credentials: {
apiVersion: 'v3',
accessToken: '__test_token__'
},
connection: '__test_connection__'
},
ticks: TICKS,
operation: AggregationTypes.COUNT,
column: 'column_1',
global: true
};

const data = await getHistogram(props);

expect(data).toEqual(RESULT);

test('correctly called', async () => {
const { column, operation, ticks, filters, dataSource } = histogramParams;
await getHistogram(histogramParams);
expect(executeTask).toHaveBeenCalledWith(dataSource, Methods.FEATURES_HISTOGRAM, {
column,
filters,
operation,
ticks
expect(executeSQL).toHaveBeenCalledWith({
credentials: props.source.credentials,
query: `SELECT tick, count(column_1) as value FROM (SELECT CASE WHEN column_1 < 1 THEN 0 WHEN column_1 < 2 THEN 1 WHEN column_1 < 3 THEN 2 ELSE 3 END as tick, column_1 FROM __test__) q GROUP BY tick`,
connection: props.source.connection,
opts: {
abortController: undefined
}
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/react-widgets/src/hooks/useWidgetFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function useWidgetFetch(
{ id, dataSource, params, global, onError }
) {
// State
const [data, setData] = useState(null);
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(false);

const isSourceReady = useSelector(
Expand Down
63 changes: 48 additions & 15 deletions packages/react-widgets/src/models/CategoryModel.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,55 @@
import { executeSQL } from '@carto/react-api/';
import { AggregationTypes } from '@carto/react-core/';
import { Methods, executeTask } from '@carto/react-workers';
import {
formatOperationColumn,
formatTableNameWithFilters,
wrapModelCall
} from './utils';

export const getCategories = async (props) => {
const {
column,
operationColumn,
operation,
joinOperation,
filters,
filtersLogicalOperator,
dataSource
} = props;

return executeTask(dataSource, Methods.FEATURES_CATEGORY, {
filters,
filtersLogicalOperator,
export function getCategories(props) {
return wrapModelCall(props, fromLocal, fromRemote);
}

// From local
function fromLocal(props) {
const { source, column, operationColumn, operation, joinOperation } = props;

return executeTask(source.id, Methods.FEATURES_CATEGORY, {
filters: source.filters,
filtersLogicalOperator: source.filtersLogicalOperator,
operation,
joinOperation,
column,
operationColumn: operationColumn || column
});
};
}

// From remote
function fromRemote(props) {
const { source, abortController } = props;
const { credentials, connection } = source;

const query = buildSqlQueryToGetCategories(props);

return executeSQL({
credentials,
query,
connection,
opts: { abortController }
});
}

function buildSqlQueryToGetCategories(props) {
const { column, operation, operationColumn, joinOperation } = props;

const selectValueClause = `${operation}(${
operation === AggregationTypes.COUNT
? '*'
: formatOperationColumn(operationColumn || column, joinOperation)
}) as value`;

return `SELECT COALESCE(${column}, 'null') as name, ${selectValueClause} FROM ${formatTableNameWithFilters(
props
)} GROUP BY ${column}`.trim();
}
53 changes: 47 additions & 6 deletions packages/react-widgets/src/models/HistogramModel.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,54 @@
import { executeSQL } from '@carto/react-api';
import { Methods, executeTask } from '@carto/react-workers';
import { formatTableNameWithFilters, wrapModelCall } from './utils';

export const getHistogram = async (props) => {
const { column, operation, ticks, filters, filtersLogicalOperator, dataSource } = props;
export function getHistogram(props) {
return wrapModelCall(props, fromLocal, fromRemote);
}

return executeTask(dataSource, Methods.FEATURES_HISTOGRAM, {
filters,
filtersLogicalOperator,
// From local
function fromLocal(props) {
const { source, column, operation, ticks } = props;

return executeTask(source.id, Methods.FEATURES_HISTOGRAM, {
filters: source.filters,
filtersLogicalOperator: source.filtersLogicalOperator,
operation,
column,
ticks
});
};
}

// From remote
async function fromRemote(props) {
const { source, ticks, abortController } = props;
const { credentials, connection } = source;

const query = buildSqlQueryToGetHistogram(props);

const data = await executeSQL({
credentials,
query,
connection,
opts: { abortController }
});

const result = Array(ticks.length + 1).fill(0);
data.forEach(({ tick, value }) => (result[tick] = value));

return result;
}

function buildSqlQueryToGetHistogram(props) {
const { column, operation, ticks } = props;

const caseTicks = ticks.map((t, index) => `WHEN ${column} < ${t} THEN ${index}`);
caseTicks.push(`ELSE ${ticks.length}`);

const selectValueClause = `${operation}(${column}) as value`;
const selectTickClause = `CASE ${caseTicks.join(' ')} END as tick`;

const subQuery = `SELECT ${selectTickClause}, ${column} FROM ${formatTableNameWithFilters(props)}`;

return `SELECT tick, ${selectValueClause} FROM (${subQuery}) q GROUP BY tick`;
}
Loading

0 comments on commit 4a6fbdb

Please sign in to comment.