Improve categorical color management (#5815)

* Create new classes for handling categorical colors

* verify to pass existing unit tests

* separate logic for forcing color and getting color

* replace getColorFromScheme with CategoricalColorManager

* organize static functions

* migrate to new function

* Remove ALL_COLOR_SCHEMES

* move sequential colors to another file

* extract categorical colors to separate file

* move airbnb and lyft colors to separate files

* fix missing toFunction()

* Rewrite to support local and global force items, plus namespacing.

* fix references

* revert nvd3

* update namespace api

* Update the visualizations

* update usage with static functions

* update unit test

* add unit test

* rename default namespace

* add unit test for color namespace

* add unit test for namespace

* start unit test for colorschememanager

* add unit tests for color scheme manager

* check returns for chaining

* complete unit test for the new classes

* fix color tests

* update unit tests

* update unit tests

* move color scheme registration to common

* update unit test

* rename sharedForcedColors to parentForcedColors

* remove import
This commit is contained in:
Krist Wongsuphasawat
2018-09-12 14:10:26 -07:00
committed by Chris Williams
parent bec0b4cc37
commit f482a6cf99
26 changed files with 1186 additions and 595 deletions

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import d3 from 'd3';
import PropTypes from 'prop-types';
import { getColorFromScheme } from '../modules/colors';
import { getScale } from '../modules/CategoricalColorNamespace';
import './chord.css';
const propTypes = {
@@ -31,6 +31,7 @@ function chordVis(element, props) {
const div = d3.select(element);
const { nodes, matrix } = data;
const f = d3.format(numberFormat);
const colorFn = getScale(colorScheme).toFunction();
const outerRadius = Math.min(width, height) / 2 - 10;
const innerRadius = outerRadius - 24;
@@ -78,7 +79,7 @@ function chordVis(element, props) {
const groupPath = group.append('path')
.attr('id', (d, i) => 'group' + i)
.attr('d', arc)
.style('fill', (d, i) => getColorFromScheme(nodes[i], colorScheme));
.style('fill', (d, i) => colorFn(nodes[i]));
// Add a text label.
const groupText = group.append('text')
@@ -102,7 +103,7 @@ function chordVis(element, props) {
.on('mouseover', (d) => {
chord.classed('fade', p => p !== d);
})
.style('fill', d => getColorFromScheme(nodes[d.source.index], colorScheme))
.style('fill', d => colorFn(nodes[d.source.index]))
.attr('d', path);
// Add an elaborate mouseover title for each chord.

View File

@@ -6,19 +6,21 @@ import PropTypes from 'prop-types';
import AnimatableDeckGLContainer from './AnimatableDeckGLContainer';
import Legend from '../Legend';
import { getColorFromScheme, hexToRGB } from '../../modules/colors';
import { getScale } from '../../modules/CategoricalColorNamespace';
import { hexToRGB } from '../../modules/colors';
import { getPlaySliderParams } from '../../modules/time';
import sandboxedEval from '../../modules/sandbox';
function getCategories(fd, data) {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
const colorFn = getScale(fd.color_scheme).toFunction();
const categories = {};
data.forEach((d) => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color;
if (fd.dimension) {
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
color = hexToRGB(colorFn(d.cat_color), c.a * 255);
} else {
color = fixedColor;
}
@@ -98,10 +100,11 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
}
addColor(data, fd) {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const colorFn = getScale(fd.color_scheme).toFunction();
return data.map((d) => {
let color;
if (fd.dimension) {
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
color = hexToRGB(colorFn(d.cat_color), c.a * 255);
return { ...d, color };
}
return d;

View File

@@ -2,8 +2,8 @@
import d3 from 'd3';
import PropTypes from 'prop-types';
import { hierarchy } from 'd3-hierarchy';
import { getScale } from '../modules/CategoricalColorNamespace';
import { d3TimeFormatPreset } from '../modules/utils';
import { getColorFromScheme } from '../modules/colors';
import './partition.css';
// Compute dx, dy, x, y for each node and
@@ -97,6 +97,7 @@ function Icicle(element, props) {
const hasTime = ['adv_anal', 'time_series'].indexOf(chartType) >= 0;
const format = d3.format(numberFormat);
const timeFormat = d3TimeFormatPreset(dateTimeFormat);
const colorFn = getScale(colorScheme).toFunction();
div.selectAll('*').remove();
const tooltip = div
@@ -363,7 +364,7 @@ function Icicle(element, props) {
// Apply color scheme
g.selectAll('rect')
.style('fill', (d) => {
d.color = getColorFromScheme(d.name, colorScheme);
d.color = colorFn(d.name);
return d.color;
});
}

View File

@@ -2,8 +2,8 @@
import d3 from 'd3';
import PropTypes from 'prop-types';
import nv from 'nvd3';
import { getScale } from '../modules/CategoricalColorNamespace';
import { d3TimeFormatPreset } from '../modules/utils';
import { getColorFromScheme } from '../modules/colors';
import './rose.css';
const propTypes = {
@@ -62,6 +62,7 @@ function Rose(element, props) {
const numGroups = datum[times[0]].length;
const format = d3.format(numberFormat);
const timeFormat = d3TimeFormatPreset(dateTimeFormat);
const colorFn = getScale(colorScheme).toFunction();
d3.select('.nvtooltip').remove();
div.selectAll('*').remove();
@@ -70,7 +71,6 @@ function Rose(element, props) {
const legend = nv.models.legend();
const tooltip = nv.models.tooltip();
const state = { disabled: datum[times[0]].map(() => false) };
const color = name => getColorFromScheme(name, colorScheme);
const svg = div
.append('svg')
@@ -101,9 +101,9 @@ function Rose(element, props) {
.map(v => ({
key: v.name,
value: v.value,
color: color(v.name),
color: colorFn(v.name),
highlight: v.id === d.arcId,
})) : [{ key: d.name, value: d.val, color: color(d.name) }];
})) : [{ key: d.name, value: d.val, color: colorFn(d.name) }];
return {
key: 'Date',
value: d.time,
@@ -113,7 +113,7 @@ function Rose(element, props) {
legend
.width(width)
.color(d => getColorFromScheme(d.key, colorScheme));
.color(d => colorFn(d.key));
legendWrap
.datum(legendData(datum))
.call(legend);
@@ -331,7 +331,7 @@ function Rose(element, props) {
const arcs = ae
.append('path')
.attr('class', 'arc')
.attr('fill', d => color(d.name))
.attr('fill', d => colorFn(d.name))
.attr('d', arc);
function mousemove() {

View File

@@ -2,7 +2,7 @@
import d3 from 'd3';
import PropTypes from 'prop-types';
import { sankey as d3Sankey } from 'd3-sankey';
import { getColorFromScheme } from '../modules/colors';
import { getScale } from '../modules/CategoricalColorNamespace';
import './sankey.css';
const propTypes = {
@@ -49,6 +49,8 @@ function Sankey(element, props) {
.attr('class', 'sankey-tooltip')
.style('opacity', 0);
const colorFn = getScale(colorScheme).toFunction();
const sankey = d3Sankey()
.nodeWidth(15)
.nodePadding(10)
@@ -153,7 +155,7 @@ function Sankey(element, props) {
.attr('width', sankey.nodeWidth())
.style('fill', function (d) {
const name = d.name || 'N/A';
d.color = getColorFromScheme(name.replace(/ .*/, ''), colorScheme);
d.color = colorFn(name.replace(/ .*/, ''));
return d.color;
})
.style('stroke', d => d3.rgb(d.color).darker(2))

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import d3 from 'd3';
import PropTypes from 'prop-types';
import { getColorFromScheme } from '../modules/colors';
import { getScale } from '../modules/CategoricalColorNamespace';
import { wrapSvgText } from '../modules/utils';
import './sunburst.css';
@@ -68,6 +68,8 @@ function Sunburst(element, props) {
let arcs;
let gMiddleText; // dom handles
const colorFn = getScale(colorScheme).toFunction();
// Helper + path gen functions
const partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
@@ -132,7 +134,7 @@ function Sunburst(element, props) {
.attr('points', breadcrumbPoints)
.style('fill', function (d) {
return colorByCategory ?
getColorFromScheme(d.name, colorScheme) :
colorFn(d.name) :
colorScale(d.m2 / d.m1);
});
@@ -143,7 +145,7 @@ function Sunburst(element, props) {
.style('fill', function (d) {
// Make text white or black based on the lightness of the background
const col = d3.hsl(colorByCategory ?
getColorFromScheme(d.name, colorScheme) :
colorFn(d.name) :
colorScale(d.m2 / d.m1));
return col.l < 0.5 ? 'white' : 'black';
})
@@ -377,7 +379,7 @@ function Sunburst(element, props) {
.attr('d', arc)
.attr('fill-rule', 'evenodd')
.style('fill', d => colorByCategory
? getColorFromScheme(d.name, colorScheme)
? colorFn(d.name)
: colorScale(d.m2 / d.m1))
.style('opacity', 1)
.on('mouseenter', mouseenter);

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-shadow, no-param-reassign */
import d3 from 'd3';
import PropTypes from 'prop-types';
import { getColorFromScheme } from '../modules/colors';
import { getScale } from '../modules/CategoricalColorNamespace';
import './treemap.css';
// Declare PropTypes for recursive data structures
@@ -63,6 +63,7 @@ function treemap(element, props) {
} = props;
const div = d3.select(element);
const formatNumber = d3.format(numberFormat);
const colorFn = getScale(colorScheme).toFunction();
function draw(data, eltWidth, eltHeight) {
const navBarHeight = 36;
@@ -282,7 +283,7 @@ function treemap(element, props) {
.text(d => formatNumber(d.value));
t.call(text);
g.selectAll('rect')
.style('fill', d => getColorFromScheme(d.name, colorScheme));
.style('fill', d => colorFn(d.name));
return g;
};

View File

@@ -1,7 +1,7 @@
import d3 from 'd3';
import PropTypes from 'prop-types';
import cloudLayout from 'd3-cloud';
import { getColorFromScheme } from '../../modules/colors';
import { getScale } from '../../modules/CategoricalColorNamespace';
const ROTATION = {
square: () => Math.floor((Math.random() * 2)) * 90,
@@ -50,6 +50,8 @@ function wordCloud(element, props) {
.fontWeight('bold')
.fontSize(d => scale(d.size));
const colorFn = getScale(colorScheme).toFunction();
function draw(words) {
chart.selectAll('*').remove();
@@ -67,7 +69,7 @@ function wordCloud(element, props) {
.style('font-size', d => `${d.size}px`)
.style('font-weight', 'bold')
.style('font-family', 'Helvetica')
.style('fill', d => getColorFromScheme(d.text, colorScheme))
.style('fill', d => colorFn(d.text))
.attr('text-anchor', 'middle')
.attr('transform', d => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
.text(d => d.text);