Skip to content

Commit

Permalink
Add post enumeration modification to generate.ts (#187)
Browse files Browse the repository at this point in the history
fix #176 
- add nominalColorScaleForHighCardinality config: fix #101
- add smallBandSizeForFacet config: fix #93
- add smallBandSizeForHighCardinality config: fix #93 
- create Stylize.ts to handle post enumeration modification
  • Loading branch information
FelixCodes authored and kanitw committed Aug 8, 2016
1 parent f91dc68 commit 2f68187
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 8 deletions.
11 changes: 9 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ export interface QueryConfig {
maxCardinalityForShape?: number;
typeMatchesSchemaType?: boolean;

// Effectiveness Preference
// STYLIZE
smallBandSizeForHighCardinalityOrFacet?: {maxCardinality: number, bandSize: number};
nominalColorScaleForHighCardinality?: {maxCardinality: number, palette: string};

// EFFECTIVENESS PREFERENCE
maxGoodCardinalityForColor?: number; // FIXME: revise
maxGoodCardinalityForFacet?: number; // FIXME: revise
}
Expand Down Expand Up @@ -156,8 +160,11 @@ export const DEFAULT_QUERY_CONFIG: QueryConfig = {
maxCardinalityForShape: 6,
typeMatchesSchemaType: true,

// Ranking Preference
// STYLIZE
smallBandSizeForHighCardinalityOrFacet: {maxCardinality: 10, bandSize: 12},
nominalColorScaleForHighCardinality: {maxCardinality: 10, palette: 'category20'},

// RANKING PREFERENCE
maxGoodCardinalityForFacet: 5, // FIXME: revise
maxGoodCardinalityForColor: 7, // FIXME: revise
};
5 changes: 5 additions & 0 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {QueryConfig, DEFAULT_QUERY_CONFIG} from './config';
import {SpecQueryModel} from './model';
import {SpecQuery} from './query/spec';
import {Schema} from './schema';
import {stylize} from './stylize';

export function generate(specQ: SpecQuery, schema: Schema, opt: QueryConfig = DEFAULT_QUERY_CONFIG) {
// 1. Build a SpecQueryModel, which also contains enumSpecIndex
Expand All @@ -22,5 +23,9 @@ export function generate(specQ: SpecQuery, schema: Schema, opt: QueryConfig = DE
}
});

if ((opt.nominalColorScaleForHighCardinality !== null) || (opt.smallBandSizeForHighCardinalityOrFacet !== null)) {
return stylize(answerSet, schema, opt);
}

return answerSet;
}
99 changes: 99 additions & 0 deletions src/stylize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {Channel} from 'vega-lite/src/channel';
import {ScaleType} from 'vega-lite/src/scale';
import {Type} from 'vega-lite/src/type';

import {DEFAULT_QUERY_CONFIG, QueryConfig} from './config';
import {SpecQueryModel} from './model';
import {EncodingQuery, ScaleQuery, scaleType} from './query/encoding';
import {Schema} from './schema';
import {contains, Dict} from './util';

export function stylize(answerSet: SpecQueryModel[], schema: Schema, opt: QueryConfig): SpecQueryModel[] {
answerSet = answerSet.map(function(specM) {
if (opt.smallBandSizeForHighCardinalityOrFacet) {
specM = smallBandSizeForHighCardinalityOrFacet(specM, schema);
}

if (opt.nominalColorScaleForHighCardinality) {
specM = nominalColorScaleForHighCardinality(specM, schema);
}
return specM;
});

return answerSet;
}

let encQIndex: Dict<EncodingQuery> = {};

export function smallBandSizeForHighCardinalityOrFacet(specM: SpecQueryModel, schema: Schema): SpecQueryModel {
[Channel.ROW, Channel.Y, Channel.COLUMN, Channel.X].forEach((channel) => {
encQIndex[channel] = specM.getEncodingQueryByChannel(channel);
});

const yEncQ = encQIndex[Channel.Y];
if (yEncQ !== undefined) {
if (encQIndex[Channel.ROW] ||
schema.cardinality(yEncQ) > DEFAULT_QUERY_CONFIG.smallBandSizeForHighCardinalityOrFacet.maxCardinality) {

// We check for undefined rather than
// yEncQ.scale = yEncQ.scale || {} to cover the case where
// yEncQ.scale has been set to false/null.
// This prevents us from incorrectly overriding scale and
// assigning a bandSize when scale is set to false.
if (yEncQ.scale === undefined) {
yEncQ.scale = {};
}

// We do not want to assign a bandSize if scale is set to false
// and we only apply this if the scale is (or can be) an ordinal scale.
if (yEncQ.scale && contains([ScaleType.ORDINAL, undefined], scaleType((yEncQ.scale as ScaleQuery).type, yEncQ.timeUnit, yEncQ.type))) {
if (!(yEncQ.scale as ScaleQuery).bandSize) {
(yEncQ.scale as ScaleQuery).bandSize = 12;
}
}
}
}

const xEncQ = encQIndex[Channel.X];
if (xEncQ !== undefined) {
if (encQIndex[Channel.COLUMN] ||
schema.cardinality(xEncQ) > DEFAULT_QUERY_CONFIG.smallBandSizeForHighCardinalityOrFacet.maxCardinality) {

// Just like y, we don't want to do this if scale is null/false
if (xEncQ.scale === undefined) {
xEncQ.scale = {};
}

// We do not want to assign a bandSize if scale is set to false
// and we only apply this if the scale is (or can be) an ordinal scale.
if (xEncQ.scale && contains([ScaleType.ORDINAL, undefined], scaleType((xEncQ.scale as ScaleQuery).type, xEncQ.timeUnit, xEncQ.type))) {
if (!(xEncQ.scale as ScaleQuery).bandSize) {
(xEncQ.scale as ScaleQuery).bandSize = 12;
}
}
}
}

return specM;
}

export function nominalColorScaleForHighCardinality(specM: SpecQueryModel, schema: Schema): SpecQueryModel {
encQIndex[Channel.COLOR] = specM.getEncodingQueryByChannel(Channel.COLOR);

const colorEncQ = encQIndex[Channel.COLOR];
if ((colorEncQ !== undefined) && (colorEncQ.type === Type.NOMINAL) &&
(schema.cardinality(colorEncQ) > DEFAULT_QUERY_CONFIG.nominalColorScaleForHighCardinality.maxCardinality)) {

if (colorEncQ.scale === undefined) {
colorEncQ.scale = {};
}

if (colorEncQ.scale) {
if (!(colorEncQ.scale as ScaleQuery).range) {
(colorEncQ.scale as ScaleQuery).range = DEFAULT_QUERY_CONFIG.nominalColorScaleForHighCardinality.palette;
}
}
}

return specM;
}
93 changes: 87 additions & 6 deletions test/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,26 +539,26 @@ describe('generate', function () {
describe('autoAddCount', () => {
describe('ordinal only', () => {
it('should output autoCount in the answer set', () => {
const query = {
const specQ = {
mark: Mark.POINT,
encodings: [
{ channel: Channel.X, field: 'O', type: Type.ORDINAL},
]
};
const answerSet = generate(query, schema, CONFIG_WITH_AUTO_ADD_COUNT);
const answerSet = generate(specQ, schema, CONFIG_WITH_AUTO_ADD_COUNT);
assert.equal(answerSet.length, 1);
assert.isTrue(answerSet[0].getEncodings()[1].autoCount);
});
});

describe('non-binned quantitative only', () => {
const query = {
const specQ = {
mark: Mark.POINT,
encodings: [
{ channel: Channel.X, field: 'Q', type: Type.QUANTITATIVE},
]
};
const answerSet = generate(query, schema, CONFIG_WITH_AUTO_ADD_COUNT);
const answerSet = generate(specQ, schema, CONFIG_WITH_AUTO_ADD_COUNT);

it('should output autoCount=false', () => {
assert.isFalse(answerSet[0].getEncodingQueryByIndex(1).autoCount);
Expand All @@ -570,7 +570,7 @@ describe('generate', function () {
});

describe('enumerate channel for a non-binned quantitative field', () => {
const query = {
const specQ = {
mark: Mark.POINT,
encodings: [
{
Expand All @@ -580,7 +580,7 @@ describe('generate', function () {
}
]
};
const answerSet = generate(query, schema, CONFIG_WITH_AUTO_ADD_COUNT);
const answerSet = generate(specQ, schema, CONFIG_WITH_AUTO_ADD_COUNT);

it('should not output point with only size for color', () => {
answerSet.forEach((model) => {
Expand All @@ -592,4 +592,85 @@ describe('generate', function () {
});
});
});

describe('stylizer', () => {
it('should generate answerSet when all stylizers are turned off', () => {
const specQ = {
mark: Mark.POINT,
encodings: [
{
channel: Channel.X,
field: 'A',
type: Type.QUANTITATIVE
}
]
};

const CONFIG_WITHOUT_HIGH_CARDINALITY_OR_FACET = extend(
{}, DEFAULT_QUERY_CONFIG, {nominalColorScaleForHighCardinality: null}, {smallBandSizeForHighCardinalityOrFacet: null});

const answerSet = generate(specQ, schema, CONFIG_WITHOUT_HIGH_CARDINALITY_OR_FACET);
assert.equal(answerSet.length, 1);
});

describe('nominalColorScaleForHighCardinality', () => {
it('should output range = category20', () => {
const specQ = {
mark: Mark.POINT,
encodings: [
{
channel: Channel.COLOR,
field: 'N20',
scale: {},
type: Type.NOMINAL
}
]
};

const answerSet = generate(specQ, schema, DEFAULT_QUERY_CONFIG);
assert.equal((answerSet[0].getEncodingQueryByIndex(0).scale as ScaleQuery).range, 'category20');
});
});

describe('smallBandSizeForHighCardinalityOrFacet', () => {
it('should output bandSize = 12', () => {
const specQ = {
mark: Mark.BAR,
encodings: [
{
channel: Channel.Y,
field: 'O_100',
scale: {},
type: Type.ORDINAL
}
]
};

const answerSet = generate(specQ, schema, DEFAULT_QUERY_CONFIG);
assert.equal((answerSet[0].getEncodingQueryByIndex(0).scale as ScaleQuery).bandSize, 12);
});

it('should output bandSize = 12', () => {
const specQ = {
mark: Mark.BAR,
encodings: [
{
channel: Channel.Y,
field: 'A',
scale: {},
type: Type.ORDINAL
},
{
channel: Channel.ROW,
field: 'A',
type: Type.ORDINAL
}
]
};

const answerSet = generate(specQ, schema, DEFAULT_QUERY_CONFIG);
assert.equal((answerSet[0].getEncodingQueryByIndex(0).scale as ScaleQuery).bandSize, 12);
});
});
});
});
Loading

0 comments on commit 2f68187

Please sign in to comment.