-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(color palette): implementing different color palettes with d3 #459
Conversation
Codecov Report
@@ Coverage Diff @@
## master #459 +/- ##
=========================================
+ Coverage 83.77% 83.8% +0.03%
=========================================
Files 158 171 +13
Lines 4703 5027 +324
Branches 952 1017 +65
=========================================
+ Hits 3940 4213 +273
- Misses 748 798 +50
- Partials 15 16 +1
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left few comments to avoid adding ts-ignore and fix unexpected results
src/utils/colors/color_palette.ts
Outdated
import { interpolateHcl, quantize, interpolateLab } from 'd3-interpolate'; | ||
import { scaleSequential, scaleLinear } from 'd3-scale'; | ||
|
||
export type ColorPaletteName = 'blues' | 'greens' | 'greys' | 'oranges' | 'purples' | 'reds'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add also the support for the multi-hue palettes? https://github.com/d3/d3-scale-chromatic#sequential-multi-hue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how are we going to explain PuBu
and PuBuGn
color palette names 😂
src/utils/colors/color_palette.ts
Outdated
@@ -0,0 +1,90 @@ | |||
import * as d3ScaleChromatic from 'd3-scale-chromatic'; | |||
import * as d3Color from 'd3-color'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can also import only {rbg, lab}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think prefixing the function with d3-
equivalents improves readability of the code, but if there are performance concerns around importing the whole namespace, I'm happy to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is not about namespace, but avoid importing all the d3-color library to allow an optimized treeshaking of the library.
You can still use prefix d3 function if you prefer with import { rgb as d3Rgb } from 'd3-color'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another reason for individual, aliased D3 imports in repos that are not all-in D3 applications - it's a convenient way to find all entry points to D3 use, just by grepping d3_
such as d3_rgb
. The underscore might have a slight edge:
- super precise grepping, as
d3_
is unlikely to occur elsewhere (whiled3
is noisy - it also occurs in colors, UUIDs, tokens, Base64 encoded files eg. encoded SVG, yarn integrity and other hash numbers etc.) - there's no change to the capitalization of the D3 library functions
- the
_
is slightly weird visually (though easier to spot), but so is the presence of a number inside a camel3Cased identifier
Having said this, either prefix is OK for me (or even their lack thereof), the main point is a slight preference for named, aliased, easy to find imports from D3 in code where it is more of a utility. Sometimes I put all D3 functions into a single file eg. d3utils.ts
for some given functionality so it's easy to version bump or replace any of them centrally, if needed.
src/utils/colors/color_palette.ts
Outdated
export function getCategoricalPalette(name: CategoricalSchemeName): ReadonlyArray<string> { | ||
const schemeName: string = transformSchemeName(name); | ||
// @ts-ignore | ||
return d3ScaleChromatic[schemeName]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we import each scheme separately and we use a switch here to avoid adding ts-ignore
various places
import { schemeCategory10} from 'd3-scale-chromatic';
switch(schemeName){
case 'schemeCategory10':
return schemeCategory10;
default:
.....
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a lot of hardcoding for something that works fine without it. I am not sure it's worth the benefit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's for sure hardcoding, but what if scaleChromatic removes one of their scheme names? we will never catch that issue until someone call the function with that removed scale.
The same happens for the opposite: we add a new Scheme name but it's not exported or available on the scale chromatic library (see for example the typo in the previous comment, one value of the ColorPaletteName was misspelled, with a proper typing we can catch these error much more easily).
Maybe we can find some some way to handle that getting the available keys from d3ScaleChromatic with a type like keyof typeof d3ScaleChromatic
but I'm not sure how to apply that to the current implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My 1st impression was keyof typeof
too, hopefully it can be combined with conditionals - eg. check that the user-specified object key exists on the color object - so type guarding is possible. In this case, if D3 retires a color, it can fall back onto some default palette.
Maybe relying on the simplest test cases to detect if a palette was dropped or renamed by D3 is safer than modeling the set of colors as a black box and falling back onto default colors if some go missing
5fb0fc6
to
ca22637
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added few comments on the code after your changes.
Still missing to export the functions in the src/index.ts
file
src/utils/colors/color_palette.ts
Outdated
export function getCategoricalPalette(name: CategoricalSchemeName): ReadonlyArray<string> { | ||
const schemeName: string = transformSchemeName(name); | ||
// @ts-ignore | ||
return d3ScaleChromatic[schemeName]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's for sure hardcoding, but what if scaleChromatic removes one of their scheme names? we will never catch that issue until someone call the function with that removed scale.
The same happens for the opposite: we add a new Scheme name but it's not exported or available on the scale chromatic library (see for example the typo in the previous comment, one value of the ColorPaletteName was misspelled, with a proper typing we can catch these error much more easily).
Maybe we can find some some way to handle that getting the available keys from d3ScaleChromatic with a type like keyof typeof d3ScaleChromatic
but I'm not sure how to apply that to the current implementation
src/utils/colors/color_palette.ts
Outdated
@@ -0,0 +1,90 @@ | |||
import * as d3ScaleChromatic from 'd3-scale-chromatic'; | |||
import * as d3Color from 'd3-color'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is not about namespace, but avoid importing all the d3-color library to allow an optimized treeshaking of the library.
You can still use prefix d3 function if you prefer with import { rgb as d3Rgb } from 'd3-color'
src/utils/colors/color_palette.ts
Outdated
return d3ScaleChromatic[schemeName]; | ||
} | ||
|
||
export function getCustomCategoricalPalette(colors: string[], steps: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if within this PR, but it'd be neat to brainstorm about names, eg. it's a great name overall (no abbreviations to forget etc.) so the only question is, is the get
adding value. Usually, functions are equivalent (replaceable by) their return value, or to put it differently, I think it should be the norm rather than the exception that a function is referentially transparent, just doing its thing of returning some value determined by its input. Calling it customCategoricalPalette
would suggest that we're doing value based programming rather than place oriented programming
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we have this here and there in the whole library, I think it's ok for now keep the name like this
src/utils/colors/color_palette.ts
Outdated
return paletteColors; | ||
} | ||
|
||
export function getSequentialPalette(name: ColorPaletteName, steps: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like all current ways for getting the colors require the specification of the steps
, materializing into a array. We should be able to use continuous color scales too, is it out of scope for this PR? May be useful to add eg. a function that'd just take a D3 continuous color ramp, a fromValue
and a toValue
and it'd return a function that can be called with arbitrary numbers between the two values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can have that as a next-phase PR
I've incorporated your comments and added a better check if the color palette exists in d3 namespace. Also, got rid of ts-warnings. Rebasing against master and marking this as ready for review. |
05c5c1a
to
47bbd4b
Compare
Importing only relevant d3 modules
Adding implementation for the multi-hue palette
47bbd4b
to
bec9d1e
Compare
Adding diverging interpolators; adding fail-safe mechanism to get d3-interpolators; getting rid of ts-ignore warnings;
bec9d1e
to
fdea1e3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the efforts on this.
I've left few comments, after handling those I think we can merge this.
I think we can also remove the prototyping
term in the PR title
package.json
Outdated
"@types/d3-color": "^1.2.2", | ||
"@types/d3-interpolate": "^1.3.1", | ||
"@types/d3-scale-chromatic": "^1.3.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to have the type deps here since we are not exposing any function, return type or type that comes directly from d3. I think it's safe to move them under devDependencies
src/utils/colors/color_palette.ts
Outdated
return d3ScaleChromatic[schemeName]; | ||
} | ||
|
||
export function getCustomCategoricalPalette(colors: string[], steps: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we have this here and there in the whole library, I think it's ok for now keep the name like this
src/utils/colors/color_palette.ts
Outdated
const paletteColors = []; | ||
const scale = quantize(interpolateHcl(colors[0], colors[1]), steps); | ||
for (let i = 0; i < steps; i++) { | ||
paletteColors.push(d3Rgb(scale[i]).hex()); | ||
} | ||
return paletteColors; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A categorical palette is composed by a set of colors where each one differs from the other like the EUI palette or the any other palette in https://github.com/d3/d3-scale-chromatic#categorical.
Interpolating the HCL for the first two colors create a sequential scale rather then a categorical one.
I think we should remove this function and let the consumer just input a custom array of different color if they want a custom categorical palette
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sequential will have very similar colors in the range. the quantize
in the categorical will make sure that the colors are uniformly distributed, which will produce very distinct color. plus, the first and last color in the categorical palette will be the same as user defined, while in the sequential it will be an interpolated value.
Example, for the input colors: ['#007AFF', '#FFF500']
and 10 steps:
generated categorical palette:
It's true that it's sequential in nature, but I think it's still useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that more as a sequential, multi hue palette than a categorical/qualitative color scale.
It's indeed a nice palette, but doesn't provide the same function as a categorical palette. In the categorical, the color should be easily distinguishable between each other, in your example i can barely distinguish between the 3 yellow, 3 purples, 2 blues, one orange.
I personally don't see that in line with the description of a categorical scale:
http://colorbrewer2.org/learnmore/schemes_full.html#qualitative
https://seaborn.pydata.org/tutorial/color_palettes.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @cchaos
src/utils/colors/color_palette.ts
Outdated
return paletteColors; | ||
} | ||
|
||
export function getSequentialPalette(name: SequentialColorPaletteName, steps: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you please add the return type: ReadonlyArray<string>
to be aligned with the getCategoricalPalette
src/utils/colors/color_palette.ts
Outdated
return paletteColors; | ||
} | ||
|
||
export function getCustomSequentialPalette(colors: string[], steps: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you please add the return type: ReadonlyArray<string>
to be aligned with the getCategoricalPalette
src/utils/colors/color_palette.ts
Outdated
return paletteColors; | ||
} | ||
|
||
export function getCyclicalPalette(name: CyclicalPaletteName, steps: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return type: ReadonlyArray<string>
src/utils/colors/color_palette.ts
Outdated
return paletteColors; | ||
} | ||
|
||
export function getDivergingPalette(steps: number, interpolatorName?: DivergingInterpolatorName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return type: ReadonlyArray<string>
and interpolator default to spectral
directly in the arguments
Removing custom categorical palette; Adding Readonlyarray as a return type
Re-adding removed d3 librarires
| 'euiPaletteForLightBackground' | ||
| 'euiPaletteForDarkBackground' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove these from being an option? They're specific to Elastic branding and statefulness and shouldn't really be used for charts.
import { interpolateLab } from 'd3-interpolate'; | ||
import { scaleLinear, scaleSequential } from 'd3-scale'; | ||
|
||
export type SequentialColorPaletteName = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a lot of options in this list. While choice is great, it can cripple decision and allow for incorrect choices to be made. Can we reduce this list to just a few most useful ones like "warm", "cool"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, are these options going to be documented anywhere with visual examples/representations? I have no idea what cubehelixDefault
would result as and consumers would need to apply the palette, compile their app, then view the chart (and hope they have a full number of series) in order to see the palette.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we'll have a storybook example with palettes being applied to charts and users will get to try it out. This is just the first step of the implementation.
Caroline, I agree with your suggestion that we keep some of the palettes - but the problem here is that I don't really know what "a few most useful" palettes are, as this is quite far from my domain of expertise. Could you please advise on which palettes should be kept and which removed? You can use d3-scale-chromatic for representation of palettes:
https://github.com/d3/d3-scale-chromatic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All color palettes, except EUI are taken from d3 scale chromatic -> https://github.com/d3/d3-scale-chromatic
We will document all of them in storybook (we are going to improve the docs soon #457)
If you like to select just some of them for us, please feel free
| 'set1' | ||
| 'set2' | ||
| 'set3' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are these generically named palettes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the categorical palettes that d3 has to offer: https://github.com/d3/d3-scale-chromatic#schemeSet1
For simplicity, I decided to leave out the scheme
prefix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the naming comes from https://github.com/d3/d3-scale-chromatic schemas
My advice is to use some sort of object/map that names these color palettes based on their source but also have a few common and shortly named palettes for easy use. For example: Commonly named palettesCharts would provide a simple list of commonly named palettes like "temperature", "warm", "cool", "grayscale", "category". These would map to hard-coded values provided by Charts (which could also be taken from the D3/EUI options). The consumer would then write: const theme = {
colors: {
vizColors: 'temperature',
},
}; What these common palettes are still need to be determined. I'll get some thoughts started on this. D3 palettesProviding an easy way for consumers to use these D3 palettes is nice, especially for applications that also allow users to select their own. However, for Elastic engineers we would like to have them stick to the common palettes or EUI palettes so we're consistent throughout. But for consumers to understand that some palettes are coming from D3, they should have to write it like: const theme = {
colors: {
vizColors: d3Palletes.schemeSet1,
},
}; And the names shouldn't change from the way they're named from D3. This gives consumers the reference point from where these palettes are coming from for them to go view/find them. EUI palettesThis will also give us the opportunity to build EUI custom palettes within EUI and provide them to consumers of both EUI and Charts. const theme = {
colors: {
vizColors: euiPaletteForStatus, // Imported from EUI not Charts
},
}; Custom palettesAnd of course still being able to create custom palettes by providing 2+ colors to interpolate. const theme = {
colors: {
vizColors: getCustomSequentialPalette(['#FFFFE0', '#017F75'], 5),
},
}; |
I have done some looking at palettes and reached out to the other designers (doc) to get consensus on a few "common" palettes. I've then come up with a mapping of these common palettes to the D3 options (or custom) to use as defaults straight from Elastic charts. Links to palettes found in this section of the doc. Palettes = {
'categorical': d3.schemeCategory10 || euiPaletteColorBlind, // The D3 version is not color blind safe
'grayscale': d3.schemeGreys[k]
'status': d3.schemeRdYlGn[k]
'temperature': d3.schemeRdYlBu[k]
'warm': d3.schemeOrRd[k]
'cool': d3.schemePuBu[k]
'complimentary': custom(#a97143, #f1a261, #dddddd, #5795d5, #3d6995)
} In EUI, we will make our own version of the same object using the EUI (Elastic) color palette and ensuring better accessibility. Consumers of both EUI and Elastic charts can then pass in the EUI version of the palette. Or consumers can grab any of the other D3 palettes or create their own. |
@cchaos one question: |
So the custom sequential palette function doesn't allow you to specify more than 2 colors? Hmm, that seems pretty limiting given the fact that you might have at least 3 colors to interpolate from (start, middle, end) so that you can specify the middle to be gray (for example). |
Adding a layer above current color_palette implementation
|
||
function getColorPalette(colorPalette: ColorPalette): ReadonlyArray<string> { | ||
const { name, steps } = colorPalette; | ||
switch (name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending on the name, we get the actual color palette.
} else { | ||
finalTheme = theme ? mergeWithDefaultTheme(theme, base) : base; | ||
} | ||
if (colorPaletteSpec) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If color palette exists, it's applied here.
export type ColorPaletteName = | ||
| 'categorical' | ||
| 'colorBlind' | ||
| 'grayscale' | ||
| 'status' | ||
| 'temperature' | ||
| 'warm' | ||
| 'cool' | ||
| 'complimentary'; | ||
|
||
export type ColorPalette = { | ||
name: ColorPaletteName; | ||
steps?: number; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the user facing palettes.
I think this issue will be handled now in kibana via the new palette service in the charts plugin. If not we can revisit these changes in the future, but this PR is too out of date to continue. cc: @markov00 |
Summary
Implements color palette calcualtions using d3 libraries.
There are 3 different palettes available at the moment:
This PR does not include an API to be used from the chart itself. This will be added in a subsequent PR.
Checklist
Use
strikethroughsto remove checklist items you don't feel are applicable to this PR.src/index.ts
(and stories only import from../src
except for test data & storybook)