initial proof of concept
MODIFY
examples/legend/main.js
examples/legend/main.js
@@ -104,6 +104,12 @@ $(function () {
fillColor: 'magenta',
fillOpacity: 0.5
}
+ },
+ {
+ name: 'test1',
+ type: 'discrete',
+ style: {
+ }
}
]);

ADD
examples/legend2d/example.json
+{
+ "path": "legend2d",
+ "title": "2D legends",
+ "exampleCss": ["main.css"],
+ "exampleJs": ["main.js"],
+ "about": {
+ "text": "This example shows how to create a legend to describe feature styles on a map."
+ }
+}

ADD
examples/legend2d/index.pug
+extends ../common/templates/index.pug

ADD
examples/legend2d/main.css
examples/legend2d/main.css
ADD
examples/legend2d/main.js
+// Run after the DOM loads
+$(function () {
+ 'use strict';
+
+ // Create a map object
+ var map = geo.map({
+ node: '#map',
+ zoom: 3
+ });
+
+ // Add the default osm layer
+ // map.createLayer('osm');
+
+ // Create a ui layer
+ var ui = map.createLayer('ui');
+
+ // Create a legend widget
+ var legend = ui.createWidget('legend2d', {
+ position: {
+ right: 20,
+ top: 10
+ }
+ });
+
+ // Add rows to the legend
+ //
+ // The categories are displayed in the order passed as
+ // a row on the legend. The category objects should
+ // contain the following properties:
+ //
+ // * name: (string) The name/description
+ // * style: (object) The feature style object
+ // * type: (string) The feature type ('point', 'line', 'other')
+ //
+ // Points are displayed as circles, lines as lines, and everything else
+ // is displayed as a rounded rectangle. The style objects have the same
+ // properties as feature styles (fill is ignored for lines):
+ //
+ // * stroke: (bool) Turn on or off the stroke
+ // * strokeWidth: (number) The stroke width in pixels
+ // * strokeColor: (string|object) The stroke color
+ // * strokeOpacity: (number) The stroke opacity
+ // * fill: (bool) Turn on or off the fill
+ // * fillColor: (string|object) The fill color
+ // * fillOpacity: (number) The fill opacity
+ legend.categories([
+ {
+ name: 'test1',
+ type: 'discrete',
+ scale: {
+ type: 'ordinal',
+ domain: ['beijing', 'new york', 'london'],
+ range: ['red', 'green', 'blue']
+ }
+ },
+ {
+ name: 'test2',
+ type: 'discrete',
+ scale: {
+ type: 'quantize',
+ domain: [20000, 120000],
+ range: colorbrewer.YlGnBu["9"]
+ }
+ },
+ {
+ name: 'test3',
+ type: 'continuous',
+ scale: {
+ type: 'pow',
+ domain: [100, 10000],
+ range: ['red', 'blue']
+ }
+ },
+ {
+ name: 'test4',
+ type: 'continuous',
+ scale: {
+ type: 'log',
+ domain: [100, 10000],
+ range: ['purple', 'orange']
+ }
+ }
+ ]);
+
+});

ADD
examples/legend2d/thumb.jpg
examples/legend2d/thumb.jpg
Binary files /dev/null and b/examples/legend2d/thumb.jpg differ

MODIFY
src/ui/index.js
src/ui/index.js
module.exports = {
domWidget: require('./domWidget'),
legendWidget: require('./legendWidget'),
+ legend2dWidget: require('./legend2dWidget'),
sliderWidget: require('./sliderWidget'),
svgWidget: require('./svgWidget'),
uiLayer: require('./uiLayer'),

ADD
src/ui/legend2dWidget.js
+var domWidget = require('./domWidget');
+var inherit = require('../inherit');
+var registerWidget = require('../registry').registerWidget;
+require('./legend2dWidget.styl');
+
+var legend2dWidget = function (arg) {
+ 'use strict';
+ if (!(this instanceof legend2dWidget)) {
+ return new legend2dWidget(arg);
+ }
+
+ domWidget.call(this, arg);
+
+ var m_this = this,
+ m_default_canvas = 'div';
+ var m_categories = [];
+
+ /**
+ * Initializes DOM Widget.
+ * Sets the canvas for the widget, does parent/child relationship management,
+ * appends it to it's parent and handles any positioning logic.
+ */
+ this._init = function () {
+ if (arg.hasOwnProperty('parent')) {
+ arg.parent.addChild(m_this);
+ }
+
+ m_this._createCanvas();
+ m_this._appendChild();
+
+ m_this.canvas().addEventListener('mousedown', function (e) {
+ e.stopPropagation();
+ });
+
+ m_this.reposition();
+ };
+
+ /**
+ * Creates the widget canvas.
+ * This is just a simple DOM element (based on args.el, or defaults to a div)
+ */
+ this._createCanvas = function () {
+ m_this.canvas(document.createElement(arg.el || m_default_canvas));
+ };
+
+ this.draw = function () {
+ m_this._init();
+ var container = d3.select(m_this.canvas()).append('div');
+
+ var width = 700;
+
+ m_categories.forEach(function (category, index) {
+ var legendSvg = container
+ .append('div')
+ .style({
+ 'width': width + 'px',
+ 'height': '50px'
+ })
+ .append('svg')
+ .attr({
+ 'width': '100%',
+ 'height': '100%'
+ })
+
+ if (category.type === 'discrete') {
+ var steps;
+ var colorScale = d3.scale[category.scale.type]().domain(category.scale.domain).range(category.scale.range);
+ var axisScale;
+ if (category.scale.type === 'ordinal') {
+ steps = category.scale.domain;
+
+ axisScale = d3.scale.ordinal()
+ .domain(category.scale.domain)
+ .rangePoints([0, width]);
+ var axis = d3.svg.axis()
+ .scale(axisScale);
+ }
+ else if (category.scale.type === 'quantize') {
+ steps = category.scale.range.map(function (color) {
+ var range = colorScale.invertExtent(color);
+ // console.log(color, range);
+ return (range[1] - range[0]) / 2 + range[0];
+ });
+
+ var steps2 = category.scale.range.map(function (color) {
+ return colorScale.invertExtent(color)[0];
+ });
+
+ var ticks = steps2.slice();
+ ticks.push(colorScale.invertExtent(
+ category.scale.range[category.scale.range.length - 1])[1]
+ );
+
+ axisScale = d3.scale.ordinal()
+ .domain(ticks)
+ .rangePoints([0, width]);
+ var axis = d3.svg.axis()
+ .scale(axisScale)
+ .tickFormat(d3.format(".2s"));
+ }
+
+ legendSvg.selectAll('rect')
+ .data(steps)
+ .enter()
+ .append('rect')
+ .attr('width', width / steps.length)
+ .attr('height', '20px')
+ .attr('fill', function (d) {
+ // console.log(d, colorScale(d));
+ return colorScale(d);
+ })
+ .attr('transform', function (d, i) {
+ return 'translate(' + i * width / steps.length + ' ,0)';
+ })
+
+ }
+
+ else if (category.type === 'continuous') {
+ var axisScale = d3.scale[category.scale.type]().domain(category.scale.domain).range([0, width]);
+ if (category.scale.type === 'log') {
+ axisScale.base(Math.E);
+ }
+ if (category.scale.type === 'pow') {
+ axisScale.exponent(1.5);
+ }
+
+ var gradient = legendSvg
+ .append('defs')
+ .append('linearGradient')
+ .attr('id', 'gradient' + index);
+ gradient.append('stop')
+ .attr('offset', '0%')
+ .attr('stop-color', category.scale.range[0]);
+ gradient.append('stop')
+ .attr('offset', '100%')
+ .attr('stop-color', category.scale.range[1]);
+ legendSvg.append('rect')
+ .attr('fill', 'url(#gradient' + index + ')')
+ .attr('width', '100%')
+ .attr('height', '20px');
+
+ var axis = d3.svg.axis()
+ .scale(axisScale)
+ .tickFormat(d3.format(".2s"))
+ }
+
+ legendSvg.append('g')
+ .attr('class', 'x axis')
+ .attr('transform', 'translate(0, 20)')
+ .call(function (g) {
+ g.call(axis);
+ g.selectAll('path.domain, line')
+ .attr({
+ 'fill': 'none',
+ 'stroke': 'black'
+ });
+ });
+ });
+
+ }
+
+ this.categories = function (categories) {
+ m_categories = categories;
+ this.draw();
+ }
+
+ return this;
+};
+
+inherit(legend2dWidget, domWidget);
+
+registerWidget('dom', 'legend2d', legend2dWidget);
+module.exports = legend2dWidget;

MODIFY
src/ui/legendWidget.js
src/ui/legendWidget.js
@@ -183,6 +183,25 @@ var legendWidget = function (arg) {
return d.name;
});
+ var discreteCategories = m_categories.filter(function (category) {
+ return category.type == 'discrete';
+ });
+
+ var dis = m_group
+ .selectAll('g.discrete')
+ .data(discreteCategories)
+ .enter()
+ .append('g')
+ .attr('transform', function (d, i) {
+ return 'translate(0,' + scale.y(i) + ')';
+ });
+
+ dis.append('text')
+ .attr('x', '50px')
+ .attr('y', 0)
+ .attr('dy', '0.3em')
+ .text("abcdef")
+
m_this.reposition();
return m_this;

initial proof of concept