Skip to content
This repository has been archived by the owner on Apr 23, 2024. It is now read-only.

Commit

Permalink
feat: color by dimension
Browse files Browse the repository at this point in the history
  • Loading branch information
miralemd committed Sep 6, 2019
1 parent 0f688b6 commit 9be3004
Show file tree
Hide file tree
Showing 23 changed files with 1,151 additions and 68 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"max-len": 0,
"no-plusplus": 0,
"no-bitwise" : 0,
"no-param-reassign": 0,
"no-unused-expressions": 0,
"import/no-extraneous-dependencies": [2, { "devDependencies": true }]
},
Expand Down
72 changes: 57 additions & 15 deletions src/__tests__/pic-definition.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,43 @@ describe('pic-definition', () => {
const context = {
permissions: [],
};
const picassoColoring = {
color: () => 'fill',
datumProps: () => ({ colorProp: 'c' }),
scales: () => ({ colorScale: 's' }),
palettes: () => ['p'],
legend: () => ({ components: [], interactions: [] }),
};
describe('components', () => {
it('should contain axis', () => {
const [{ default: def }] = mock({
axis: () => ['x', 'y'],
});
const c = def({ context }).components;
const c = def({ context, picassoColoring }).components;
expect(c).to.eql(['x', 'y']);
});

it('should contain cell rects and labels', () => {
const [{ default: def }] = mock({
cells: () => ['rects', 'labels'],
});
const c = def({ context }).components;
const c = def({ context, picassoColoring }).components;
expect(c).to.eql(['rects', 'labels']);
});

it('should contain span rects and labels', () => {
const [{ default: def }] = mock({
axis: () => ['sr', 'slabels'],
});
const c = def({ context }).components;
const c = def({ context, picassoColoring }).components;
expect(c).to.eql(['sr', 'slabels']);
});

it('should contain tooltip when permission is passive', () => {
const [{ default: def }] = mock({
tooltip: () => ['t'],
});
const c = def({ context: { permissions: ['passive'] } }).components;
const c = def({ context: { permissions: ['passive'] }, picassoColoring }).components;
expect(c).to.eql(['t']);
});

Expand All @@ -58,7 +65,7 @@ describe('pic-definition', () => {
disclaimer: () => ['d'],
cells: () => ['c'],
});
const c = def({ context, restricted: { type: 'disrupt' } });
const c = def({ context, restricted: { type: 'disrupt' }, picassoColoring });
expect(c).to.eql({
components: ['d'],
});
Expand All @@ -68,25 +75,43 @@ describe('pic-definition', () => {
const [{ default: def }] = mock({
disclaimer: () => ['d'],
});
const c = def({ context, restricted: {} }).components;
const c = def({ context, restricted: {}, picassoColoring }).components;
expect(c).to.eql(['d']);
});

it('should contain legend component', () => {
const [{ default: def }] = mock();
const c = def({
context,
restricted: {},
picassoColoring: {
...picassoColoring, legend: () => ({ components: ['leg'], interactions: [] }),
},
}).components;
expect(c).to.eql(['leg']);
});
});

it('should contain scales', () => {
const [{ default: def }] = mock({
scales: () => ({ x: 'data' }),
});
const s = def({ context }).scales;
expect(s).to.eql({ x: 'data' });
const s = def({ context, picassoColoring }).scales;
expect(s).to.eql({ x: 'data', colorScale: 's' });
});

it('should contain palettes', () => {
const [{ default: def }] = mock();
const s = def({ context, picassoColoring }).palettes;
expect(s).to.eql(['p']);
});

describe('collections', () => {
it('should contain stacked first dimension', () => {
const [{ default: def }] = mock({
stack: opts => opts,
});
const [first] = def({ context }).collections;
const [first] = def({ context, picassoColoring }).collections;
expect(first).to.containSubset({
key: 'span',
field: 'qDimensionInfo/0',
Expand All @@ -102,23 +127,40 @@ describe('pic-definition', () => {
const [{ default: def }] = mock({
stack: opts => opts,
});
const [, second] = def({ context }).collections;
const [, second] = def({ context, picassoColoring }).collections;
expect(second).to.eql({
key: 'stacked',
key: 'cells',
field: 'qDimensionInfo/1',
props: {
colorProp: 'c',
},
});
});
});

it('should not contain any events when passive permission is missing', () => {
it('should not contain any events when passive and interact permission is missing', () => {
const [{ default: def }] = mock();
const s = def({ context }).interactions;
expect(Object.keys(s[0].events)).to.eql([]);
const s = def({ context, picassoColoring }).interactions;
expect(s).to.eql([]);
});

it('should contain interactive mousemove and mouseleave when passive', () => {
const [{ default: def }] = mock();
const s = def({ context: { permissions: ['passive'] } }).interactions;
const s = def({ context: { permissions: ['passive'] }, picassoColoring }).interactions;
expect(Object.keys(s[0].events)).to.eql(['mousemove', 'mouseleave']);
});

it('should contain legend interactions', () => {
const [{ default: def }] = mock();
const c = def({
context: {
permissions: [''],
},
restricted: {},
picassoColoring: {
...picassoColoring, legend: () => ({ components: [], interactions: ['legint'] }),
},
}).interactions;
expect(c).to.eql(['legint']);
});
});
99 changes: 99 additions & 0 deletions src/coloring/byDimension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
removeRole,
addRole,
} from '../roles/roles';

const RX = /\/(qDimensions|qMeasures)\/(\d+)/;
const RXA = /\/(qDimensions|qMeasures)\/(\d+)\/(qAttributeDimensions|qAttributeExpressions)\/(\d+)/;

/**
* @param {object} properties
* @param {object} byDimensionConfig
* @param {'index'|'expression'|'libraryId'} byDimensionConfig.type
* @param {string|integer} byDimensionConfig.typeValue
* @param {boolean} byDimensionConfig.persistent
* @param {string} byDimensionConfig.scheme
* @param {string|QAE.StringExpression} byDimensionConfig.label
*/
export function setByDimension(properties, byDimensionConfig) {
// remove existing
removeRole(properties.qHyperCubeDef, 'color');

const dimensions = properties.qHyperCubeDef.qDimensions;

const defaultTargetPath = `/qDimensions/${dimensions.length - 1}`;

const config = byDimensionConfig || {
type: 'index',
typeValue: dimensions.length - 1,
};

let targetPath;
if (config.type === 'index') {
targetPath = `/qDimensions/${config.typeValue}`;
} else {
targetPath = `${defaultTargetPath}/qAttributeDimensions/0`;
}

const m = RXA.exec(targetPath);
if (m) {
const def = config.type === 'libraryId' ? {
qLibraryId: config.typeValue,
libraryId: config.typeValue, // add custom property since qLibraryId is not returned in attr dimension/measure in layout
} : {
qDef: config.typeValue,
};
properties.qHyperCubeDef[m[1]][+m[2]][m[3]].splice(+m[4], 0, {
...def,
qAttribute: true,
qSortBy: { qSortByAscii: 1 },
roles: [{ role: 'color' }],
});
} else {
// add role only
const mx = RX.exec(targetPath);
if (mx) {
addRole(properties.qHyperCubeDef[mx[1]][+mx[2]], 'color');
} else {
console.error('hmm?');
}
}

properties.color.byDimension = {
persistent: false,
scheme: '',
...(properties.color.byDimension || {}),
...(config || {}),
};
}

export function getByDimensionSettings({
layout,
theme,
definition,
fieldPath,
}) {
if (definition.qError || (definition.qSize && definition.qSize.qcy === 0)) {
return {
invalid: true,
...theme.dataColors(),
};
}
const pals = theme.palettes('qualitative');
const c = (layout.color && layout.color.byDimension) || {};
return {
mode: 'field',
field: fieldPath,
fieldType: 'dimension',
persistent: c.persistent,

type: 'categorical',

// references values in a theme
palette: pals.filter(p => p.key === c.scheme)[0] || pals[0],
...theme.dataColors(),

// for tooltips and legend
label: c.type === 'expression' ? c.label || definition.qFallbackTitle : definition.qFallbackTitle,
};
}
File renamed without changes.
86 changes: 86 additions & 0 deletions src/coloring/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
removeRole,
findFields,
} from '../roles/roles';

import {
setByDimension,
getByDimensionSettings,
} from './byDimension';

const DIMENSION_RX = /(qDimensions|qDimensionInfo)\/\d+(\/(qAttributeDimensions|qAttrDimInfo)\/\d+)?$/;
const MEASURE_RX = /(qMeasures|qMeasureInfo)\/\d+(\/(qAttributeExpressions|qAttrExprInfo)\/\d+)?$/;

function getFieldType(path) {
if (DIMENSION_RX.test(path)) {
return 'dimension';
}
if (MEASURE_RX.test(path)) {
return 'measure';
}
return undefined;
}

function setByAuto(properties) {
properties.color.mode = 'auto';
}

export default function coloring({
properties,
layout,
theme,
}) {
return {
colorBy({
mode,
modeConfig,
}) {
// reset
removeRole(properties.qHyperCubeDef, 'color');

if (!properties.color) {
properties.color = {};
}

if (mode === 'auto') {
setByAuto(properties);
delete properties.color.byDimension;
} else if (mode === 'byDimension') {
setByDimension(properties, modeConfig);
}
},
getSettings() {
const hc = layout ? layout.qHyperCube : properties.qHyperCubeDef;
const colorProps = layout ? layout.color : properties.color;

const colorByField = findFields(f => f.roles && f.roles.filter(r => r.role === 'color').length > 0, hc)[0];
let fieldPath = colorByField ? colorByField.path.replace(/^\//, '') : 'qDimensionInfo/1';
let definition = colorByField ? colorByField.definition : null;

if (!colorByField || colorProps.mode === 'auto') {
fieldPath = 'qDimensionInfo/1';
definition = layout ? layout.qHyperCube.qDimensionInfo[1] : properties.qHyperCubeDef.qDimensions[0];
}

const fieldType = getFieldType(fieldPath);

if (fieldType === 'dimension') {
return getByDimensionSettings({
layout,
theme,
definition,
fieldPath,
});
}

return {
invalid: true,
};
},
getLegendSettings() {
return {
dock: 'right',
};
},
};
}

0 comments on commit 9be3004

Please sign in to comment.