diff --git a/superset/assets/javascripts/explore/components/Control.jsx b/superset/assets/javascripts/explore/components/Control.jsx
index b0dce3549fd95..6022b9f690fc8 100644
--- a/superset/assets/javascripts/explore/components/Control.jsx
+++ b/superset/assets/javascripts/explore/components/Control.jsx
@@ -10,6 +10,7 @@ import SelectControl from './controls/SelectControl';
import TextAreaControl from './controls/TextAreaControl';
import TextControl from './controls/TextControl';
import VizTypeControl from './controls/VizTypeControl';
+import ColorSchemeControl from './controls/ColorSchemeControl';
const controlMap = {
BoundsControl,
@@ -21,6 +22,7 @@ const controlMap = {
TextAreaControl,
TextControl,
VizTypeControl,
+ ColorSchemeControl,
};
const controlTypes = Object.keys(controlMap);
diff --git a/superset/assets/javascripts/explore/components/controls/ColorSchemeControl.jsx b/superset/assets/javascripts/explore/components/controls/ColorSchemeControl.jsx
new file mode 100644
index 0000000000000..db51792cfc696
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/ColorSchemeControl.jsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Creatable } from 'react-select';
+import ControlHeader from '../ControlHeader';
+
+import { colorScalerFactory } from '../../../modules/colors';
+
+const propTypes = {
+ description: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ onChange: PropTypes.func,
+ value: PropTypes.string,
+ default: PropTypes.string,
+ choices: PropTypes.arrayOf(React.PropTypes.array).isRequired,
+ schemes: PropTypes.object.isRequired,
+ isLinear: PropTypes.bool,
+};
+
+const defaultProps = {
+ choices: [],
+ schemes: {},
+ onChange: () => {},
+};
+
+export default class ColorSchemeControl extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ scheme: this.props.value,
+ };
+
+ this.onChange = this.onChange.bind(this);
+ this.renderOption = this.renderOption.bind(this);
+ }
+
+ onChange(option) {
+ const optionValue = option ? option.value : null;
+ this.props.onChange(optionValue);
+ this.setState({ scheme: optionValue });
+ }
+
+ renderOption(key) {
+ const currentScheme = key.value ?
+ this.props.schemes[key.value] :
+ this.props.schemes[defaultProps.value];
+
+ let colors = currentScheme;
+ if (this.props.isLinear) {
+ const colorScaler = colorScalerFactory(currentScheme);
+ colors = [...Array(20).keys()].map(d => (colorScaler(d / 20)));
+ }
+
+ const list = colors.map((color, i) => (
+
+ ));
+ return ();
+ }
+
+ render() {
+ const selectProps = {
+ multi: false,
+ name: `select-${this.props.name}`,
+ placeholder: `Select (${this.props.choices.length})`,
+ default: this.props.default,
+ options: this.props.choices.map(choice => ({ value: choice[0], label: choice[1] })),
+ value: this.props.value,
+ autosize: false,
+ clearable: false,
+ onChange: this.onChange,
+ optionRenderer: this.renderOption,
+ valueRenderer: this.renderOption,
+ };
+ return (
+
+
+
+
+ );
+ }
+}
+
+ColorSchemeControl.propTypes = propTypes;
+ColorSchemeControl.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/main.css b/superset/assets/javascripts/explore/main.css
index 388be8f9fa784..5baea683d1996 100644
--- a/superset/assets/javascripts/explore/main.css
+++ b/superset/assets/javascripts/explore/main.css
@@ -60,3 +60,16 @@
cursor: not-allowed;
border: 1px solid #aaa;
}
+
+.color-scheme-container {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ align-items: center;
+}
+.color-scheme-container li {
+ flex-basis: 9px;
+ height: 10px;
+ margin: 9px 1px;
+}
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index 3d33873c3c53d..febb3efa20889 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import { formatSelectOptionsForRange, formatSelectOptions } from '../../modules/utils';
import * as v from '../validators';
+import { ALL_COLOR_SCHEMES, spectrums } from '../../modules/colors';
import MetricOption from '../../components/MetricOption';
import ColumnOption from '../../components/ColumnOption';
@@ -155,7 +156,7 @@ export const controls = {
},
linear_color_scheme: {
- type: 'SelectControl',
+ type: 'ColorSchemeControl',
label: 'Linear Color Scheme',
choices: [
['fire', 'fire'],
@@ -165,6 +166,9 @@ export const controls = {
],
default: 'blue_white_yellow',
description: '',
+ renderTrigger: true,
+ schemes: spectrums,
+ isLinear: true,
},
normalize_across: {
@@ -1307,5 +1311,15 @@ export const controls = {
description: 'Leaf nodes that represent fewer than this number of events will be initially ' +
'hidden in the visualization',
},
+
+ color_scheme: {
+ type: 'ColorSchemeControl',
+ label: 'Color Scheme',
+ default: 'bnbColors',
+ renderTrigger: true,
+ choices: Object.keys(ALL_COLOR_SCHEMES).map(s => ([s, s])),
+ description: 'The color scheme for rendering chart',
+ schemes: ALL_COLOR_SCHEMES,
+ },
};
export default controls;
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index 7e04237a36dc8..81496fa8cabd8 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -18,6 +18,12 @@ export const sections = {
['slice_id', 'cache_timeout'],
],
},
+ colorScheme: {
+ label: 'Color Scheme',
+ controlSetRows: [
+ ['color_scheme'],
+ ],
+ },
sqlaTimeSeries: {
label: 'Time',
description: 'Time related form attributes',
@@ -83,6 +89,7 @@ export const visTypes = {
{
label: 'Chart Options',
controlSetRows: [
+ ['color_scheme'],
['metrics'],
['groupby'],
['columns'],
@@ -119,6 +126,7 @@ export const visTypes = {
['pie_label_type'],
['donut', 'show_legend'],
['labels_outside'],
+ ['color_scheme'],
],
},
],
@@ -133,6 +141,7 @@ export const visTypes = {
{
label: 'Chart Options',
controlSetRows: [
+ ['color_scheme'],
['show_brush', 'show_legend'],
['rich_tooltip', null],
['show_markers', 'x_axis_showminmax'],
@@ -164,6 +173,7 @@ export const visTypes = {
{
label: 'Chart Options',
controlSetRows: [
+ ['color_scheme'],
['x_axis_format'],
],
},
@@ -204,6 +214,7 @@ export const visTypes = {
{
label: 'Chart Options',
controlSetRows: [
+ ['color_scheme'],
['show_brush', 'show_legend', 'show_bar_value'],
['rich_tooltip', 'contribution'],
['line_interpolation', 'bar_stacked'],
@@ -237,6 +248,7 @@ export const visTypes = {
{
label: 'Chart Options',
controlSetRows: [
+ ['color_scheme'],
['x_axis_format', 'y_axis_format'],
],
},
@@ -260,6 +272,7 @@ export const visTypes = {
controlSetRows: [
['show_brush', 'show_legend'],
['line_interpolation', 'stacked_style'],
+ ['color_scheme'],
['rich_tooltip', 'contribution'],
['show_controls', null],
],
@@ -383,6 +396,7 @@ export const visTypes = {
['series', 'metric', 'limit'],
['size_from', 'size_to'],
['rotation'],
+ ['color_scheme'],
],
},
],
@@ -401,6 +415,7 @@ export const visTypes = {
{
label: 'Chart Options',
controlSetRows: [
+ ['color_scheme'],
['treemap_ratio'],
['number_format'],
],
@@ -436,6 +451,7 @@ export const visTypes = {
{
label: 'Chart Options',
controlSetRows: [
+ ['color_scheme'],
['whisker_options'],
],
},
@@ -455,6 +471,7 @@ export const visTypes = {
{
label: 'Chart Options',
controlSetRows: [
+ ['color_scheme'],
['show_legend', null],
],
},
@@ -553,6 +570,7 @@ export const visTypes = {
{
label: 'Histogram Options',
controlSetRows: [
+ ['color_scheme'],
['link_length'],
],
},
@@ -579,6 +597,7 @@ export const visTypes = {
['groupby'],
['metric', 'secondary_metric'],
['row_limit'],
+ ['color_scheme'],
],
},
],
@@ -609,6 +628,7 @@ export const visTypes = {
['groupby'],
['metric'],
['row_limit'],
+ ['color_scheme'],
],
},
],
@@ -655,6 +675,7 @@ export const visTypes = {
['groupby', 'columns'],
['metric'],
['row_limit', 'y_axis_format'],
+ ['color_scheme'],
],
},
],
diff --git a/superset/assets/javascripts/modules/colors.js b/superset/assets/javascripts/modules/colors.js
index 56c88afd2a764..9897e4c228e06 100644
--- a/superset/assets/javascripts/modules/colors.js
+++ b/superset/assets/javascripts/modules/colors.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import d3 from 'd3';
// Color related utility functions go in this object
-export const bnbColors = [
+const bnbColors = [
'#ff5a5f', // rausch
'#7b0051', // hackb
'#007A87', // kazan
@@ -25,8 +25,55 @@ export const bnbColors = [
'#b37e00',
'#988b4e',
];
+const d3Category10 = d3.scale.category10().range();
+const d3Category20 = d3.scale.category20().range();
+const d3Category20b = d3.scale.category20b().range();
+const d3Category20c = d3.scale.category20c().range();
+const googleCategory10c = [
+ '#3366cc',
+ '#dc3912',
+ '#ff9900',
+ '#109618',
+ '#990099',
+ '#0099c6',
+ '#dd4477',
+ '#66aa00',
+ '#b82e2e',
+ '#316395',
+];
+const googleCategory20c = [
+ '#3366cc',
+ '#dc3912',
+ '#ff9900',
+ '#109618',
+ '#990099',
+ '#0099c6',
+ '#dd4477',
+ '#66aa00',
+ '#b82e2e',
+ '#316395',
+ '#994499',
+ '#22aa99',
+ '#aaaa11',
+ '#6633cc',
+ '#e67300',
+ '#8b0707',
+ '#651067',
+ '#329262',
+ '#5574a6',
+ '#3b3eac',
+];
+export const ALL_COLOR_SCHEMES = {
+ bnbColors,
+ d3Category10,
+ d3Category20,
+ d3Category20b,
+ d3Category20c,
+ googleCategory10c,
+ googleCategory20c,
+};
-const spectrums = {
+export const spectrums = {
blue_white_yellow: [
'#00d1c1',
'white',
@@ -48,21 +95,25 @@ const spectrums = {
],
};
-export const category21 = (function () {
+export const getColorFromScheme = (function () {
// Color factory
const seen = {};
- return function (s) {
+ return function (s, scheme) {
if (!s) {
return;
}
+ const selectedScheme = scheme ? ALL_COLOR_SCHEMES[scheme] : ALL_COLOR_SCHEMES.bnbColors;
let stringifyS = String(s);
// next line is for superset series that should have the same color
stringifyS = stringifyS.replace('---', '');
- if (seen[stringifyS] === undefined) {
- seen[stringifyS] = Object.keys(seen).length;
+ if (seen[selectedScheme] === undefined) {
+ seen[selectedScheme] = {};
+ }
+ if (seen[selectedScheme][stringifyS] === undefined) {
+ seen[selectedScheme][stringifyS] = Object.keys(seen[selectedScheme]).length;
}
/* eslint consistent-return: 0 */
- return bnbColors[seen[stringifyS] % bnbColors.length];
+ return selectedScheme[seen[selectedScheme][stringifyS] % selectedScheme.length];
};
}());
diff --git a/superset/assets/spec/javascripts/modules/colors_spec.jsx b/superset/assets/spec/javascripts/modules/colors_spec.jsx
new file mode 100644
index 0000000000000..558547101a06e
--- /dev/null
+++ b/superset/assets/spec/javascripts/modules/colors_spec.jsx
@@ -0,0 +1,22 @@
+import { it, describe } from 'mocha';
+import { expect } from 'chai';
+
+import { ALL_COLOR_SCHEMES, getColorFromScheme } from '../../../javascripts/modules/colors';
+
+describe('colors', () => {
+ it('default to bnbColors', () => {
+ const color1 = getColorFromScheme('CA');
+ expect(color1).to.equal(ALL_COLOR_SCHEMES.bnbColors[0]);
+ });
+ it('series with same scheme should have the same color', () => {
+ const color1 = getColorFromScheme('CA', 'bnbColors');
+ const color2 = getColorFromScheme('CA', 'googleCategory20c');
+ const color3 = getColorFromScheme('CA', 'bnbColors');
+ const color4 = getColorFromScheme('NY', 'bnbColors');
+
+ expect(color1).to.equal(ALL_COLOR_SCHEMES.bnbColors[0]);
+ expect(color2).to.equal(ALL_COLOR_SCHEMES.googleCategory20c[0]);
+ expect(color1).to.equal(color3);
+ expect(color4).to.equal(ALL_COLOR_SCHEMES.bnbColors[1]);
+ });
+});
diff --git a/superset/assets/visualizations/chord.jsx b/superset/assets/visualizations/chord.jsx
index c2b3c3498e7c4..dbb551ca36e4d 100644
--- a/superset/assets/visualizations/chord.jsx
+++ b/superset/assets/visualizations/chord.jsx
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
import d3 from 'd3';
-import { category21 } from '../javascripts/modules/colors';
+import { getColorFromScheme } from '../javascripts/modules/colors';
import './chord.css';
function chordViz(slice, json) {
@@ -60,7 +60,7 @@ function chordViz(slice, json) {
const groupPath = group.append('path')
.attr('id', (d, i) => 'group' + i)
.attr('d', arc)
- .style('fill', (d, i) => category21(nodes[i]));
+ .style('fill', (d, i) => getColorFromScheme(nodes[i], slice.formData.color_scheme));
// Add a text label.
const groupText = group.append('text')
@@ -84,7 +84,7 @@ function chordViz(slice, json) {
.on('mouseover', (d) => {
chord.classed('fade', p => p !== d);
})
- .style('fill', d => category21(nodes[d.source.index]))
+ .style('fill', d => getColorFromScheme(nodes[d.source.index], slice.formData.color_scheme))
.attr('d', path);
// Add an elaborate mouseover title for each chord.
diff --git a/superset/assets/visualizations/histogram.js b/superset/assets/visualizations/histogram.js
index 16af9c44ce513..b5bbf0951946b 100644
--- a/superset/assets/visualizations/histogram.js
+++ b/superset/assets/visualizations/histogram.js
@@ -1,5 +1,5 @@
import d3 from 'd3';
-import { category21 } from '../javascripts/modules/colors';
+import { getColorFromScheme } from '../javascripts/modules/colors';
require('./histogram.css');
@@ -76,7 +76,7 @@ function histogram(slice, payload) {
.attr('x', d => x(d.x))
.attr('y', d => y(d.y))
.attr('height', d => y.range()[0] - y(d.y))
- .style('fill', d => category21(d.length))
+ .style('fill', d => getColorFromScheme(d.length, slice.formData.color_scheme))
.order();
// Find maximum length to position the ticks on top of the bar correctly
diff --git a/superset/assets/visualizations/nvd3_vis.js b/superset/assets/visualizations/nvd3_vis.js
index 21342942cfd86..f9c03c638eaab 100644
--- a/superset/assets/visualizations/nvd3_vis.js
+++ b/superset/assets/visualizations/nvd3_vis.js
@@ -4,7 +4,7 @@ import throttle from 'lodash.throttle';
import d3 from 'd3';
import nv from 'nvd3';
-import { category21 } from '../javascripts/modules/colors';
+import { getColorFromScheme } from '../javascripts/modules/colors';
import { customizeToolTip, d3TimeFormatPreset, d3FormatPreset, tryNumify } from '../javascripts/modules/utils';
// CSS
@@ -337,7 +337,7 @@ function nvd3Vis(slice, payload) {
}
if (vizType !== 'bullet') {
- chart.color(d => category21(d[colorKey]));
+ chart.color(d => getColorFromScheme(d[colorKey], fd.color_scheme));
}
if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
chart.useInteractiveGuideline(true);
diff --git a/superset/assets/visualizations/sankey.js b/superset/assets/visualizations/sankey.js
index 80e8980b8eaaf..2dcba6af34117 100644
--- a/superset/assets/visualizations/sankey.js
+++ b/superset/assets/visualizations/sankey.js
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
import d3 from 'd3';
-import { category21 } from '../javascripts/modules/colors';
+import { getColorFromScheme } from '../javascripts/modules/colors';
import './sankey.css';
d3.sankey = require('d3-sankey').sankey;
@@ -138,7 +138,7 @@ function sankeyVis(slice, payload) {
.attr('width', sankey.nodeWidth())
.style('fill', function (d) {
const name = d.name || 'N/A';
- d.color = category21(name.replace(/ .*/, ''));
+ d.color = getColorFromScheme(name.replace(/ .*/, ''), slice.formData.color_scheme);
return d.color;
})
.style('stroke', function (d) {
diff --git a/superset/assets/visualizations/sunburst.js b/superset/assets/visualizations/sunburst.js
index 34143c0be34e1..41859e3a2d87c 100644
--- a/superset/assets/visualizations/sunburst.js
+++ b/superset/assets/visualizations/sunburst.js
@@ -1,6 +1,6 @@
/* eslint-disable no-underscore-dangle, no-param-reassign */
import d3 from 'd3';
-import { category21 } from '../javascripts/modules/colors';
+import { getColorFromScheme } from '../javascripts/modules/colors';
import { wrapSvgText } from '../javascripts/modules/utils';
require('./sunburst.css');
@@ -110,7 +110,9 @@ function sunburstVis(slice, payload) {
entering.append('svg:polygon')
.attr('points', breadcrumbPoints)
.style('fill', function (d) {
- return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1);
+ return colorByCategory ?
+ getColorFromScheme(d.name, slice.formData.color_scheme) :
+ colorScale(d.m2 / d.m1);
});
entering.append('svg:text')
@@ -119,7 +121,9 @@ function sunburstVis(slice, payload) {
.attr('dy', '0.35em')
.style('fill', function (d) {
// Make text white or black based on the lightness of the background
- const col = d3.hsl(colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1));
+ const col = d3.hsl(colorByCategory ?
+ getColorFromScheme(d.name, slice.formData.color_scheme) :
+ colorScale(d.m2 / d.m1));
return col.l < 0.5 ? 'white' : 'black';
})
.attr('class', 'step-label')
@@ -360,7 +364,9 @@ function sunburstVis(slice, payload) {
})
.attr('d', arc)
.attr('fill-rule', 'evenodd')
- .style('fill', d => colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1))
+ .style('fill', d => colorByCategory ?
+ getColorFromScheme(d.name, fd.color_scheme) :
+ colorScale(d.m2 / d.m1))
.style('opacity', 1)
.on('mouseenter', mouseenter);
diff --git a/superset/assets/visualizations/treemap.js b/superset/assets/visualizations/treemap.js
index 2a5a9c3b3cf45..072582a97730c 100644
--- a/superset/assets/visualizations/treemap.js
+++ b/superset/assets/visualizations/treemap.js
@@ -1,6 +1,6 @@
/* eslint-disable no-shadow, no-param-reassign, no-underscore-dangle, no-use-before-define */
import d3 from 'd3';
-import { category21 } from '../javascripts/modules/colors';
+import { getColorFromScheme } from '../javascripts/modules/colors';
require('./treemap.css');
@@ -185,7 +185,7 @@ function treemap(slice, payload) {
.text(function (d) { return formatNumber(d.value); });
t.call(text);
g.selectAll('rect')
- .style('fill', function (d) { return category21(d.name); });
+ .style('fill', function (d) { return getColorFromScheme(d.name, formData.color_scheme); });
return g;
};
diff --git a/superset/assets/visualizations/word_cloud.js b/superset/assets/visualizations/word_cloud.js
index 5d445c7db7a6d..5ee0e0b3954d3 100644
--- a/superset/assets/visualizations/word_cloud.js
+++ b/superset/assets/visualizations/word_cloud.js
@@ -1,7 +1,7 @@
/* eslint-disable no-use-before-define */
import d3 from 'd3';
import cloudLayout from 'd3-cloud';
-import { category21 } from '../javascripts/modules/colors';
+import { getColorFromScheme } from '../javascripts/modules/colors';
function wordCloudChart(slice, payload) {
const chart = d3.select(slice.selector);
@@ -42,7 +42,7 @@ function wordCloudChart(slice, payload) {
.append('text')
.style('font-size', d => d.size + 'px')
.style('font-family', 'Impact')
- .style('fill', d => category21(d.text))
+ .style('fill', d => getColorFromScheme(d.text, fd.color_scheme))
.attr('text-anchor', 'middle')
.attr('transform', d => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
.text(d => d.text);