From ddd2bbff9897415be968fb60e2afa6c695c9eec4 Mon Sep 17 00:00:00 2001 From: Jeffrey Heer Date: Fri, 4 May 2018 22:31:51 -0700 Subject: [PATCH] Add legend direction, discrete gradient, and redesigned symbol legend support. --- .../src/parsers/guides/guide-util.js | 44 +++++ .../guides/legend-gradient-discrete.js | 35 ++++ .../parsers/guides/legend-gradient-labels.js | 51 +++-- .../src/parsers/guides/legend-gradient.js | 33 +++- .../src/parsers/guides/legend-labels.js | 49 ----- .../parsers/guides/legend-symbol-groups.js | 151 +++++++++++++++ .../src/parsers/guides/legend-symbols.js | 54 ------ packages/vega-parser/src/parsers/legend.js | 177 ++++++++++-------- .../vega-parser/src/parsers/marks/roles.js | 1 + 9 files changed, 385 insertions(+), 210 deletions(-) create mode 100644 packages/vega-parser/src/parsers/guides/guide-util.js create mode 100644 packages/vega-parser/src/parsers/guides/legend-gradient-discrete.js delete mode 100644 packages/vega-parser/src/parsers/guides/legend-labels.js create mode 100644 packages/vega-parser/src/parsers/guides/legend-symbol-groups.js delete mode 100644 packages/vega-parser/src/parsers/guides/legend-symbols.js diff --git a/packages/vega-parser/src/parsers/guides/guide-util.js b/packages/vega-parser/src/parsers/guides/guide-util.js new file mode 100644 index 0000000000..33792dcb3f --- /dev/null +++ b/packages/vega-parser/src/parsers/guides/guide-util.js @@ -0,0 +1,44 @@ +import {Vertical} from './constants'; +import {value} from '../../util'; + +export function lookup(name, spec, config) { + return value(spec[name], config[name]); +} + +export function isVertical(spec, configVal) { + return value(spec.direction, configVal) === Vertical; +} + +export function gradientLength(spec, config) { + return value( + spec.gradientLength, + config.gradientLength || config.gradientWidth + ); +} + +export function gradientThickness(spec, config) { + return value( + spec.gradientThickness, + config.gradientThickness || config.gradientHeight + ); +} + +export function entryColumns(spec, config) { + return value( + spec.columns, + value(config.columns, +isVertical(spec, config.entryDirection)) + ); +} + +export function getEncoding(name, encode) { + var v = encode && ( + (encode.update && encode.update[name]) || + (encode.enter && encode.enter[name]) + ); + return v && v.signal ? v : v ? v.value : null; +} + +export function getStyle(name, scope, style) { + var s = scope.config.style[style]; + return s && s[name]; +} diff --git a/packages/vega-parser/src/parsers/guides/legend-gradient-discrete.js b/packages/vega-parser/src/parsers/guides/legend-gradient-discrete.js new file mode 100644 index 0000000000..5af0ae6365 --- /dev/null +++ b/packages/vega-parser/src/parsers/guides/legend-gradient-discrete.js @@ -0,0 +1,35 @@ +import {Value, Perc, Perc2} from './constants'; +import guideMark from './guide-mark'; +import {gradientLength, gradientThickness, isVertical, lookup} from './guide-util'; +import {RectMark} from '../marks/marktypes'; +import {LegendBandRole} from '../marks/roles'; +import {addEncode, encoder} from '../encode/encode-util'; + +export default function(spec, scale, config, userEncode, dataRef) { + var zero = {value: 0}, + vertical = isVertical(spec, config.gradientDirection), + thickness = gradientThickness(spec, config), + length = gradientLength(spec, config), + encode = {}, enter, update, u, v, uu, vv, adjust = ''; + + vertical + ? (u = 'y', uu = 'y2', v = 'x', vv = 'width', adjust = '1-') + : (u = 'x', uu = 'x2', v = 'y', vv = 'height'); + + encode.enter = enter = {opacity: zero}; + addEncode(enter, 'stroke', lookup('gradientStrokeColor', spec, config)); + addEncode(enter, 'strokeWidth', lookup('gradientStrokeWidth', spec, config)); + + encode.exit = {opacity: zero}; + encode.update = update = {opacity: {value: 1}}; + + enter.fill = update.fill = {scale: scale, field: Value}; + + enter[u] = update[u] = {signal: adjust + 'datum.' + Perc, mult: length}; + enter[v] = update[v] = {value: 0}; + + enter[uu] = update[uu] = {signal: adjust + 'datum.' + Perc2, mult: length}; + enter[vv] = update[vv] = encoder(thickness); + + return guideMark(RectMark, LegendBandRole, null, Value, dataRef, encode, userEncode); +} diff --git a/packages/vega-parser/src/parsers/guides/legend-gradient-labels.js b/packages/vega-parser/src/parsers/guides/legend-gradient-labels.js index ccbbd876c0..4af3c4faa0 100644 --- a/packages/vega-parser/src/parsers/guides/legend-gradient-labels.js +++ b/packages/vega-parser/src/parsers/guides/legend-gradient-labels.js @@ -1,25 +1,33 @@ -import {Perc, Label, GuideLabelStyle} from './constants'; +import {Index, Label, Perc, Value, GuideLabelStyle} from './constants'; import guideMark from './guide-mark'; +import {gradientLength, gradientThickness, isVertical, lookup} from './guide-util'; import {TextMark} from '../marks/marktypes'; import {LegendLabelRole} from '../marks/roles'; -import {addEncode} from '../encode/encode-util'; +import {addEncode, encoder} from '../encode/encode-util'; +import {value} from '../../util'; var alignExpr = 'datum.' + Perc + '<=0?"left"' + ':datum.' + Perc + '>=1?"right":"center"'; +var baselineExpr = 'datum.' + Perc + '<=0?"bottom"' + + ':datum.' + Perc + '>=1?"top":"middle"'; + export default function(spec, config, userEncode, dataRef) { var zero = {value: 0}, - encode = {}, enter, update; + vertical = isVertical(spec, config.gradientDirection), + thickness = encoder(gradientThickness(spec, config)), + length = gradientLength(spec, config), + overlap = lookup('labelOverlap', spec, config), + encode = {}, enter, update, u, v, adjust = ''; encode.enter = enter = { opacity: zero }; - addEncode(enter, 'fill', config.labelColor); - addEncode(enter, 'font', config.labelFont); - addEncode(enter, 'fontSize', config.labelFontSize); - addEncode(enter, 'fontWeight', config.labelFontWeight); - addEncode(enter, 'baseline', config.gradientLabelBaseline); - addEncode(enter, 'limit', config.gradientLabelLimit); + addEncode(enter, 'fill', lookup('labelColor', spec, config)); + addEncode(enter, 'font', lookup('labelFont', spec, config)); + addEncode(enter, 'fontSize', lookup('labelFontSize', spec, config)); + addEncode(enter, 'fontWeight', lookup('labelFontWeight', spec, config)); + addEncode(enter, 'limit', value(spec.labelLimit, config.gradientLabelLimit)); encode.exit = { opacity: zero @@ -30,17 +38,22 @@ export default function(spec, config, userEncode, dataRef) { text: {field: Label} }; - enter.x = update.x = { - field: Perc, - mult: config.gradientWidth - }; + if (vertical) { + enter.align = {value: 'left'}; + enter.baseline = update.baseline = {signal: baselineExpr}; + u = 'y'; v = 'x'; adjust = '1-'; + } else { + enter.align = update.align = {signal: alignExpr}; + enter.baseline = {value: 'top'}; + u = 'x'; v = 'y'; + } - enter.y = update.y = { - value: config.gradientHeight, - offset: config.gradientLabelOffset - }; + enter[u] = update[u] = {signal: adjust + 'datum.' + Perc, mult: length}; - enter.align = update.align = {signal: alignExpr}; + enter[v] = update[v] = thickness; + thickness.offset = value(spec.labelOffset, config.gradientLabelOffset) || 0; - return guideMark(TextMark, LegendLabelRole, GuideLabelStyle, Perc, dataRef, encode, userEncode); + spec = guideMark(TextMark, LegendLabelRole, GuideLabelStyle, Value, dataRef, encode, userEncode); + if (overlap) spec.overlap = {method: overlap, order: 'datum.' + Index}; + return spec; } diff --git a/packages/vega-parser/src/parsers/guides/legend-gradient.js b/packages/vega-parser/src/parsers/guides/legend-gradient.js index 1a6c2132d1..e4d945f499 100644 --- a/packages/vega-parser/src/parsers/guides/legend-gradient.js +++ b/packages/vega-parser/src/parsers/guides/legend-gradient.js @@ -1,21 +1,35 @@ import guideMark from './guide-mark'; +import {gradientLength, gradientThickness, isVertical, lookup} from './guide-util'; import {RectMark} from '../marks/marktypes'; import {LegendGradientRole} from '../marks/roles'; -import {addEncode} from '../encode/encode-util'; +import {addEncode, encoder} from '../encode/encode-util'; export default function(spec, scale, config, userEncode) { var zero = {value: 0}, - encode = {}, enter, update; + vertical = isVertical(spec, config.gradientDirection), + thickness = gradientThickness(spec, config), + length = gradientLength(spec, config), + encode = {}, enter, update, start, stop, width, height; + + if (vertical) { + start = [0, 1]; + stop = [0, 0]; + width = thickness; + height = length; + } else { + start = [0, 0]; + stop = [1, 0]; + width = length; + height = thickness; + } encode.enter = enter = { opacity: zero, x: zero, y: zero }; - addEncode(enter, 'width', config.gradientWidth); - addEncode(enter, 'height', config.gradientHeight); - addEncode(enter, 'stroke', config.gradientStrokeColor); - addEncode(enter, 'strokeWidth', config.gradientStrokeWidth); + addEncode(enter, 'stroke', lookup('gradientStrokeColor', spec, config)); + addEncode(enter, 'strokeWidth', lookup('gradientStrokeWidth', spec, config)); encode.exit = { opacity: zero @@ -24,11 +38,12 @@ export default function(spec, scale, config, userEncode) { encode.update = update = { x: zero, y: zero, - fill: {gradient: scale, start: [0,0], stop: [1,0]}, + fill: {gradient: scale, start: start, stop: stop}, opacity: {value: 1} }; - addEncode(update, 'width', config.gradientWidth); - addEncode(update, 'height', config.gradientHeight); + + enter.width = update.width = encoder(width); + enter.height = update.height = encoder(height); return guideMark(RectMark, LegendGradientRole, null, undefined, undefined, encode, userEncode); } diff --git a/packages/vega-parser/src/parsers/guides/legend-labels.js b/packages/vega-parser/src/parsers/guides/legend-labels.js deleted file mode 100644 index fa79ece88c..0000000000 --- a/packages/vega-parser/src/parsers/guides/legend-labels.js +++ /dev/null @@ -1,49 +0,0 @@ -import {Index, Label, Offset, Size, Total, Value, GuideLabelStyle} from './constants'; -import guideMark from './guide-mark'; -import {TextMark} from '../marks/marktypes'; -import {LegendLabelRole} from '../marks/roles'; -import {addEncode} from '../encode/encode-util'; - -export default function(spec, config, userEncode, dataRef) { - var zero = {value: 0}, - encode = {}, enter, update; - - encode.enter = enter = { - opacity: zero - }; - addEncode(enter, 'align', config.labelAlign); - addEncode(enter, 'baseline', config.labelBaseline); - addEncode(enter, 'fill', config.labelColor); - addEncode(enter, 'font', config.labelFont); - addEncode(enter, 'fontSize', config.labelFontSize); - addEncode(enter, 'fontWeight', config.labelFontWeight); - addEncode(enter, 'limit', config.labelLimit); - - encode.exit = { - opacity: zero - }; - - encode.update = update = { - opacity: {value: 1}, - text: {field: Label} - }; - - enter.x = update.x = { - field: Offset, - offset: config.labelOffset - }; - - enter.y = update.y = { - field: Size, - mult: 0.5, - offset: { - field: Total, - offset: { - field: {group: 'entryPadding'}, - mult: {field: Index} - } - } - }; - - return guideMark(TextMark, LegendLabelRole, GuideLabelStyle, Value, dataRef, encode, userEncode); -} diff --git a/packages/vega-parser/src/parsers/guides/legend-symbol-groups.js b/packages/vega-parser/src/parsers/guides/legend-symbol-groups.js new file mode 100644 index 0000000000..a55e2f8960 --- /dev/null +++ b/packages/vega-parser/src/parsers/guides/legend-symbol-groups.js @@ -0,0 +1,151 @@ +import { + Index, Label, Offset, Size, Value, + Skip, GuideLabelStyle, LegendScales +} from './constants'; +import guideGroup from './guide-group'; +import guideMark from './guide-mark'; +import {entryColumns, isVertical, lookup} from './guide-util'; +import {SymbolMark, TextMark} from '../marks/marktypes'; +import {ScopeRole, LegendSymbolRole, LegendLabelRole} from '../marks/roles'; +import {addEncode, encoder, extendEncode} from '../encode/encode-util'; + +var zero = {value: 0}; + +// userEncode is top-level, includes entries, symbols, labels +export default function(spec, config, userEncode, dataRef, columns) { + var entries = userEncode.entries, + interactive = !!(entries && entries.interactive), + name = entries ? entries.name : undefined, + height = lookup('clipHeight', spec, config), + symbolOffset = lookup('symbolOffset', spec, config), + valueRef = {data: 'value'}, + encode = {}, + xSignal = columns + '?' + 'datum.' + Offset + ':' + 'datum.' + Size, + yEncode = height ? encoder(height) : {field: Size}, + index = 'datum.' + Index, + ncols = 'max(1,' + columns + ')', + enter, update, labelOffset, symbols, labels, nrows, sort; + + // -- LEGEND SYMBOLS -- + encode = { + enter: enter = {opacity: zero}, + exit: {opacity: zero}, + update: update = {opacity: {value: 1}} + }; + + if (!spec.fill) { + addEncode(enter, 'fill', config.symbolBaseFillColor); + addEncode(enter, 'stroke', config.symbolBaseStrokeColor); + } + addEncode(enter, 'shape', lookup('symbolType', spec, config)); + addEncode(enter, 'size', lookup('symbolSize', spec, config)); + addEncode(enter, 'strokeWidth', lookup('symbolStrokeWidth', spec, config)); + addEncode(enter, 'fill', lookup('symbolFillColor', spec, config)); + addEncode(enter, 'stroke', lookup('symbolStrokeColor', spec, config)); + + enter.x = update.x = { + signal: xSignal, + mult: 0.5, + offset: symbolOffset + }; + + yEncode.mult = 0.5; + enter.y = update.y = yEncode; + + LegendScales.forEach(function(scale) { + if (spec[scale]) { + update[scale] = enter[scale] = {scale: spec[scale], field: Value}; + } + }); + + symbols = guideMark( + SymbolMark, LegendSymbolRole, null, + Value, valueRef, encode, userEncode.symbols + ); + if (height) symbols.clip = true; + + // -- LEGEND LABELS -- + encode = { + enter: enter = {opacity: zero}, + exit: {opacity: zero}, + update: update = { + opacity: {value: 1}, + text: {field: Label} + } + }; + + addEncode(enter, 'align', lookup('labelAlign', spec, config)); + addEncode(enter, 'baseline', lookup('labelBaseline', spec, config)); + addEncode(enter, 'fill', lookup('labelColor', spec, config)); + addEncode(enter, 'font', lookup('labelFont', spec, config)); + addEncode(enter, 'fontSize', lookup('labelFontSize', spec, config)); + addEncode(enter, 'fontWeight', lookup('labelFontWeight', spec, config)); + addEncode(enter, 'limit', lookup('labelLimit', spec, config)); + + labelOffset = encoder(symbolOffset); + labelOffset.offset = lookup('labelOffset', spec, config); + + enter.x = update.x = { + signal: xSignal, + offset: labelOffset + }; + + enter.y = update.y = yEncode; + + labels = guideMark( + TextMark, LegendLabelRole, GuideLabelStyle, + Value, valueRef, encode, userEncode.labels + ); + + // -- LEGEND ENTRY GROUPS -- + encode = { + enter: { + width: zero, + height: height ? encoder(height) : zero, + opacity: zero + }, + exit: {opacity: zero}, + update: update = { + opacity: {value: 1}, + row: {signal: null}, + column: {signal: null} + } + }; + + // annotate and sort groups to ensure correct ordering + if (isVertical(spec, config.entryDirection)) { + nrows = 'ceil(item.mark.items.length/' + ncols + ')'; + update.row.signal = index + '%' + nrows; + update.column.signal = 'floor(' + index + '/' + nrows + ')'; + sort = {field: ['row', index]}; + } else { + update.row.signal = 'floor(' + index + '/' + ncols + ')'; + update.column.signal = index + '%' + ncols; + sort = {field: index}; + } + // handle zero column case (implies infinite columns) + update.column.signal = columns + '?' + update.column.signal + ':' + index; + + // facet legend entries into sub-groups + dataRef = {facet: {data: dataRef, name: 'value', groupby: Index}}; + + spec = guideGroup( + ScopeRole, null, name, dataRef, interactive, + extendEncode(encode, entries, Skip), [symbols, labels] + ); + spec.sort = sort; + return spec; +} + +export function legendSymbolLayout(spec, config) { + // layout parameters for legend entries + return { + align: lookup('gridAlign', spec, config), + center: {row: true, column: false}, + columns: entryColumns(spec, config), + padding: { + row: lookup('rowPadding', spec, config), + column: lookup('columnPadding', spec, config) + } + }; +} diff --git a/packages/vega-parser/src/parsers/guides/legend-symbols.js b/packages/vega-parser/src/parsers/guides/legend-symbols.js deleted file mode 100644 index bba2a05d06..0000000000 --- a/packages/vega-parser/src/parsers/guides/legend-symbols.js +++ /dev/null @@ -1,54 +0,0 @@ -import {Index, Offset, Size, Total, Value, LegendScales} from './constants'; -import guideMark from './guide-mark'; -import {SymbolMark} from '../marks/marktypes'; -import {LegendSymbolRole} from '../marks/roles'; -import {addEncode} from '../encode/encode-util'; - -export default function(spec, config, userEncode, dataRef) { - var zero = {value: 0}, - encode = {}, enter, update; - - encode.enter = enter = { - opacity: zero - }; - addEncode(enter, 'shape', config.symbolType); - addEncode(enter, 'size', config.symbolSize); - addEncode(enter, 'strokeWidth', config.symbolStrokeWidth); - if (!spec.fill) { - addEncode(enter, 'fill', config.symbolFillColor); - addEncode(enter, 'stroke', config.symbolStrokeColor); - } - - encode.exit = { - opacity: zero - }; - - encode.update = update = { - opacity: {value: 1} - }; - - enter.x = update.x = { - field: Offset, - mult: 0.5 - }; - - enter.y = update.y = { - field: Size, - mult: 0.5, - offset: { - field: Total, - offset: { - field: {group: 'entryPadding'}, - mult: {field: Index} - } - } - }; - - LegendScales.forEach(function(scale) { - if (spec[scale]) { - update[scale] = enter[scale] = {scale: spec[scale], field: Value}; - } - }); - - return guideMark(SymbolMark, LegendSymbolRole, null, Value, dataRef, encode, userEncode); -} diff --git a/packages/vega-parser/src/parsers/legend.js b/packages/vega-parser/src/parsers/legend.js index 9110b1f4a9..a2c6029a50 100644 --- a/packages/vega-parser/src/parsers/legend.js +++ b/packages/vega-parser/src/parsers/legend.js @@ -1,109 +1,110 @@ +import { + GuideLabelStyle, Skip, + Symbols, Gradient, Discrete, LegendScales +} from './guides/constants'; import legendGradient from './guides/legend-gradient'; +import legendGradientDiscrete from './guides/legend-gradient-discrete'; import legendGradientLabels from './guides/legend-gradient-labels'; -import legendLabels from './guides/legend-labels'; -import legendSymbols from './guides/legend-symbols'; +import {default as legendSymbolGroups, legendSymbolLayout} from './guides/legend-symbol-groups'; import legendTitle from './guides/legend-title'; import guideGroup from './guides/guide-group'; +import {getEncoding, getStyle, gradientLength, lookup} from './guides/guide-util'; import parseExpression from './expression'; import parseMark from './mark'; +import {isContinuous, isDiscretizing} from './scale'; import {LegendRole, LegendEntryRole} from './marks/roles'; import {addEncode, encoder, extendEncode} from './encode/encode-util'; -import {GuideLabelStyle, GuideTitleStyle, Skip} from './guides/constants'; -import {ref, value} from '../util'; +import {ref, deref} from '../util'; import {Collect, LegendEntries} from '../transforms'; import {error} from 'vega-util'; export default function(spec, scope) { - var type = spec.type || 'symbol', - config = scope.config.legend, + var config = scope.config.legend, encode = spec.encode || {}, legendEncode = encode.legend || {}, name = legendEncode.name || undefined, interactive = legendEncode.interactive, style = legendEncode.style, - datum, dataRef, entryRef, group, title, - entryEncode, params, children; + entryEncode, entryLayout, params, children, + type, datum, dataRef, entryRef, group; // resolve 'canonical' scale name - var scale = spec.size || spec.shape || spec.fill || spec.stroke - || spec.strokeDash || spec.opacity; + var scale = LegendScales.reduce(function(a, b) { return a || spec[b]; }, 0); + if (!scale) error('Missing valid scale for legend.'); - if (!scale) { - error('Missing valid scale for legend.'); - } + // resolve legend type (symbol, gradient, or discrete gradient) + type = legendType(spec, scope.scaleType(scale)); // single-element data source for legend group datum = { - orient: value(spec.orient, config.orient), - title: spec.title != null + orient: lookup('orient', spec, config), + title: spec.title != null, + type: type }; dataRef = ref(scope.add(Collect(null, [datum]))); // encoding properties for legend group legendEncode = extendEncode({ - enter: legendEnter(config), + enter: legendEnter(spec, config), update: { - offset: encoder(value(spec.offset, config.offset)), - padding: encoder(value(spec.padding, config.padding)), - titlePadding: encoder(value(spec.titlePadding, config.titlePadding)) + offset: encoder(lookup('offset', spec, config)), + padding: encoder(lookup('padding', spec, config)), + titlePadding: encoder(lookup('titlePadding', spec, config)) } }, legendEncode, Skip); // encoding properties for legend entry sub-group - entryEncode = { - update: { - x: {field: {group: 'padding'}}, - y: {field: {group: 'padding'}}, - entryPadding: encoder(value(spec.entryPadding, config.entryPadding)) - } - }; - - if (type === 'gradient') { - // data source for gradient labels - entryRef = ref(scope.add(LegendEntries({ - type: 'gradient', - scale: scope.scaleRef(scale), - count: scope.objectProperty(spec.tickCount), - values: scope.objectProperty(spec.values), - formatSpecifier: scope.property(spec.format) - }))); - + entryEncode = {enter: {x: {value: 0}, y: {value: 0}}}; + + // data source for legend values + entryRef = ref(scope.add(LegendEntries(params = { + type: type, + scale: scope.scaleRef(scale), + count: scope.objectProperty(spec.tickCount), + values: scope.objectProperty(spec.values), + formatSpecifier: scope.property(spec.format) + }))); + + // continuous gradient legend + if (type === Gradient) { children = [ legendGradient(spec, scale, config, encode.gradient), legendGradientLabels(spec, config, encode.labels, entryRef) ]; + // adjust default tick count based on the gradient length + params.count = params.count || scope.signalRef( + 'max(2,2*floor(' + deref(gradientLength(spec, config)) + '/100))' + ); } - else { - // data source for legend entries - entryRef = ref(scope.add(LegendEntries(params = { - scale: scope.scaleRef(scale), - count: scope.objectProperty(spec.tickCount), - values: scope.objectProperty(spec.values), - formatSpecifier: scope.property(spec.format) - }))); - + // discrete gradient legend + else if (type === Discrete) { children = [ - legendSymbols(spec, config, encode.symbols, entryRef), - legendLabels(spec, config, encode.labels, entryRef) + legendGradientDiscrete(spec, scale, config, encode.gradient, entryRef), + legendGradientLabels(spec, config, encode.labels, entryRef) ]; + } - params.size = sizeExpression(spec, scope, children); + // symbol legend + else { + // determine legend symbol group layout + entryLayout = legendSymbolLayout(spec, config); + children = [ + legendSymbolGroups(spec, config, encode, entryRef, deref(entryLayout.columns)) + ]; + // pass symbol size information to legend entry generator + params.size = sizeExpression(spec, scope, children[0].marks); } // generate legend marks children = [ - guideGroup(LegendEntryRole, null, null, dataRef, interactive, entryEncode, children) + guideGroup(LegendEntryRole, null, null, dataRef, interactive, + entryEncode, children, entryLayout) ]; // include legend title if defined if (datum.title) { - title = legendTitle(spec, config, encode.title, dataRef); - entryEncode.update.y.offset = { - field: {group: 'titlePadding'}, - offset: get('fontSize', title.encode, scope, GuideTitleStyle) - }; - children.push(title); + children.push(legendTitle(spec, config, encode.title, dataRef)); } // build legend specification @@ -114,36 +115,54 @@ export default function(spec, scope) { return parseMark(group, scope); } -function sizeExpression(spec, scope, marks) { - var fontSize = get('fontSize', marks[1].encode, scope, GuideLabelStyle), - symbolSize = spec.size - ? 'scale("' + spec.size + '",datum)' - : deref(get('size', marks[0].encode, scope)), - expr = 'max(ceil(sqrt(' + symbolSize + ')),' + deref(fontSize) + ')'; +function legendType(spec, scaleType) { + var type = spec.type || Symbols; - return parseExpression(expr, scope); + if (!spec.type && scaleCount(spec) === 1 && (spec.fill || spec.stroke)) { + type = isContinuous(scaleType) ? Gradient + : isDiscretizing(scaleType) ? Discrete + : Symbols; + } + + return type !== Gradient ? type + : isDiscretizing(scaleType) ? Discrete + : Gradient; } -function legendEnter(config) { +function scaleCount(spec) { + return LegendScales.reduce(function(count, type) { + return count + (spec[type] ? 1 : 0); + }, 0); +} + +function legendEnter(s, c) { var enter = {}, - count = addEncode(enter, 'fill', config.fillColor) - + addEncode(enter, 'stroke', config.strokeColor) - + addEncode(enter, 'strokeWidth', config.strokeWidth) - + addEncode(enter, 'strokeDash', config.strokeDash) - + addEncode(enter, 'cornerRadius', config.cornerRadius); + count = addEncode(enter, 'fill', lookup('fillColor', s, c)) + + addEncode(enter, 'stroke', lookup('strokeColor', s, c)) + + addEncode(enter, 'strokeWidth', lookup('strokeWidth', s, c)) + + addEncode(enter, 'cornerRadius', lookup('cornerRadius', s, c)) + + addEncode(enter, 'strokeDash', c.strokeDash) return count ? enter : undefined; } -function deref(v) { - return v && v.signal || v; +function sizeExpression(spec, scope, marks) { + var fontSize, size, strokeWidth, expr; + + strokeWidth = getEncoding('strokeWidth', marks[0].encode); + + size = spec.size ? 'scale("' + spec.size + '",datum)' + : getEncoding('size', marks[0].encode, scope); + + fontSize = getFontSize(marks[1].encode, scope, GuideLabelStyle); + + expr = 'max(' + + 'ceil(sqrt(' + deref(size) + ')+' + deref(strokeWidth) + '),' + + deref(fontSize) + + ')'; + + return parseExpression(expr, scope); } -function get(name, encode, scope, style) { - var v = encode && ( - (encode.update && encode.update[name]) || - (encode.enter && encode.enter[name]) - ); - return v && v.signal ? v - : v ? +v.value - : ((v = scope.config.style[style]) && +v[name]); +function getFontSize(encode, scope, style) { + return getEncoding('fontSize', encode) || getStyle('fontSize', scope, style); } diff --git a/packages/vega-parser/src/parsers/marks/roles.js b/packages/vega-parser/src/parsers/marks/roles.js index 4ff5d5c3ac..b55451813f 100644 --- a/packages/vega-parser/src/parsers/marks/roles.js +++ b/packages/vega-parser/src/parsers/marks/roles.js @@ -10,6 +10,7 @@ export var AxisTickRole = 'axis-tick'; export var AxisTitleRole = 'axis-title'; export var LegendRole = 'legend'; +export var LegendBandRole = 'legend-band'; export var LegendEntryRole = 'legend-entry'; export var LegendGradientRole = 'legend-gradient'; export var LegendLabelRole = 'legend-label';