Skip to content

Commit

Permalink
[Library] Allow stat var <> color mapping to be set by users (#2919)
Browse files Browse the repository at this point in the history
Adds an optional "colors" attribute to each of the chart web components,
to allow the user to set specific colors for the stat vars being
plotted. Any CSS valid color value is allowed.

Changes made:
* `colors` attribute is added to each chart web component
* the corresponding tile gets color added as a chart option
* some cleanup of attributes/props -- resorted to alphabetical order
* Examples and docs are updated

Examples:
```
<datacommons-bar
  title="Median income by gender of select US states (grouped)"
  comparisonVariables="Median_Income_Person_15OrMoreYears_Male_WithIncome Median_Income_Person_15OrMoreYears_Female_WithIncome"
  comparisonPlaces="geoId/01 geoId/02 geoId/04 geoId/05"
  colors="#4287f5 #f542a4"
></datacommons-bar>
```
![Screenshot 2023-07-11 at 1 37 09
PM](https://github.com/datacommonsorg/website/assets/4034366/36f6b322-639a-436e-94f0-c2442d7b050e)

For the maps, providing up to three colors are supported (will ignore
colors past the first three):

For a single color, we get a luminance based color scale
```
<datacommons-map
  title="Population"
  place="country/USA"
  childPlaceType="State"
  variable="Count_Person"
  colors="red"
></datacommons-map>
```
![Screenshot 2023-07-11 at 1 17 05
PM](https://github.com/datacommonsorg/website/assets/4034366/01eadecd-2f1b-4467-a611-fb8ac2853f34)

For two colors, we get a diverging color scale
```
<datacommons-map
  title="Population"
  place="country/USA"
  childPlaceType="State"
  variable="Count_Person"
  colors="blue red"
></datacommons-map>
```
![Screenshot 2023-07-11 at 1 16 40
PM](https://github.com/datacommonsorg/website/assets/4034366/9196c6af-a1f0-49ee-be4d-d1232bd69940)

For three colors, we have [min, mean, max] mapping:
```
<datacommons-map
  title="Population"
  place="country/USA"
  childPlaceType="State"
  variable="Count_Person"
  colors="blue purple red"
></datacommons-map>
```
![Screenshot 2023-07-11 at 1 17 27
PM](https://github.com/datacommonsorg/website/assets/4034366/64d66b55-ad7b-4777-a038-a5d60b150b9d)
  • Loading branch information
juliawu committed Jul 11, 2023
1 parent 93aad80 commit f734137
Show file tree
Hide file tree
Showing 16 changed files with 335 additions and 148 deletions.
16 changes: 15 additions & 1 deletion static/js/chart/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,21 @@ export function getDashes(n: number): string[] {
}
}

export function getColorFn(labels: string[]): d3.ScaleOrdinal<string, string> {
/**
* Creates a color function mapping labels to specific colors.
*
* @param labels labels to map to colors
* @param colors colors to assign
* @returns D3 scale mapping labels to its assigned color.
*/
export function getColorFn(
labels: string[],
colors?: string[]
): d3.ScaleOrdinal<string, string> {
if (colors) {
return d3.scaleOrdinal<string, string>().domain(labels).range(colors);
}

let domain = labels;
let range;
if (
Expand Down
29 changes: 15 additions & 14 deletions static/js/chart/draw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,14 +857,14 @@ function drawStackBarChart(
);
updateXAxis(xAxis, bottomHeight, chartHeight, y);

const color = getColorFn(keys);
const colorFn = getColorFn(keys, options?.colors);

chart
.selectAll("g")
.data(series)
.enter()
.append("g")
.attr("fill", (d) => color(d.key))
.attr("fill", (d) => colorFn(d.key))
.selectAll("rect")
.data((d) => d)
.join("rect")
Expand All @@ -877,7 +877,7 @@ function drawStackBarChart(

appendLegendElem(
containerElement,
color,
colorFn,
dataGroups[0].value.map((dp) => ({
label: dp.label,
link: dp.link,
Expand Down Expand Up @@ -1095,7 +1095,7 @@ function drawGroupBarChart(
);
updateXAxis(xAxis, bottomHeight, chartHeight, y);

const colorFn = getColorFn(keys);
const colorFn = getColorFn(keys, options.colors);

if (options?.lollipop) {
drawLollipops(chart, colorFn, dataGroups, x0, x1, y);
Expand Down Expand Up @@ -1128,7 +1128,8 @@ function drawGaugeChart(
chartWidth: number,
data: GaugeChartData,
formatNumberFn: (value: number, unit?: string) => string,
minChartHeight: number
minChartHeight: number,
options?: ChartOptions
): void {
if (_.isEmpty(data)) {
return;
Expand All @@ -1144,11 +1145,10 @@ function drawGaugeChart(
// color to use for unfilled portion of the arc
const backgroundArcColor = "#ddd";
// color scale for [low, med, high] values
const colorOptions = [
"#d63031", // red
"#fdcb6e", // yellow
"#00b894", // green
];
const colorOptions = options?.colors
? options?.colors
: ["#d63031", "#fdcb6e", "#00b894"]; // red, yellow, green

// minimum thickness of the arc, in px
const minArcThickness = 10;
// how thickness of arc should scale with chart's width
Expand Down Expand Up @@ -1318,7 +1318,7 @@ function drawHorizontalBarChart(
.rangeRound([marginTop, height - marginBottom])
.padding(0.15);

const color = getColorFn(keys);
const color = getColorFn(keys, options?.colors);

// Create the SVG container.
const svg = d3
Expand Down Expand Up @@ -1508,7 +1508,7 @@ function drawLineChart(
const legendText = dataGroups.map((dataGroup) =>
dataGroup.label ? dataGroup.label : "A"
);
const colorFn = getColorFn(legendText);
const colorFn = getColorFn(legendText, options?.colors);

let hasFilledInValues = false;
const timePoints = new Set<number>();
Expand Down Expand Up @@ -1912,7 +1912,8 @@ function drawDonutChart(
chartWidth: number,
chartHeight: number,
dataGroups: DataGroup[],
drawAsPie: boolean
drawAsPie: boolean,
options?: ChartOptions
): void {
if (_.isEmpty(dataGroups)) {
return;
Expand All @@ -1922,7 +1923,7 @@ function drawDonutChart(
labelToLink[dataGroup.label] = dataGroup.link;
}
const keys = dataGroups[0].value.map((dp) => dp.label);
const colorFn = getColorFn(keys);
const colorFn = getColorFn(keys, options?.colors);
// minimum thickness of the donut, in px
const minArcThickness = 10;
// how thickness of donut should scale with donut's radius
Expand Down
133 changes: 99 additions & 34 deletions static/js/chart/draw_map_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,85 @@ const TEMP_MODEL_DIFF_DOMAIN = [0, 15];
const TEMP_DOMAIN = [-40, -20, 0, 20, 40];
const TEMP_AGGREGATE_DIFF_DOMAIN = [-30, -15, 0, 15, 30];

/**
* Generate a blue to red color scale for temperature statistical variables.
* Used as a helper function for the more general getColorScale().
*
* @param statVar name of the stat var we are drawing choropleth for
* @param domain the domain of the scale. The first number is the min, second
* number is the middle, and the last number is the max
* @returns a blue to red color scale to use with temperature values
*/
function getTemperatureColorScale(
statVar: string,
domain?: [number, number, number]
): d3.ScaleLinear<number, number> {
let domainValues: number[];
let range: any[] = [
d3.interpolateBlues(1),
d3.interpolateBlues(0.8),
MIN_COLOR,
d3.interpolateReds(0.8),
d3.interpolateReds(1),
];

if (statVar.indexOf("Difference") >= 0) {
if (statVar.indexOf("Base") >= 0) {
domainValues = domain || TEMP_BASE_DIFF_DOMAIN;
} else if (statVar.indexOf("Dc Aggregate")) {
domainValues = domain || TEMP_AGGREGATE_DIFF_DOMAIN;
} else {
domainValues = domain || TEMP_MODEL_DIFF_DOMAIN;
}
} else {
domainValues = domain || TEMP_DOMAIN;
}
const min = domainValues[0];
const max = domainValues[domainValues.length - 1];
if (min >= 0) {
domainValues = [0, max / 2, max];
range = [MIN_COLOR, d3.interpolateReds(0.8), d3.interpolateReds(1)];
} else if (max <= 0) {
domainValues = [min, min / 2, 0];
range = [d3.interpolateBlues(1), d3.interpolateBlues(0.8), MIN_COLOR];
}
return d3.scaleLinear().domain(domainValues).nice().range(range);
}

/**
* Generate a color scale using specific color values.
*
* @param colors list of colors to use. If only one color is provided, will
* generate a scale that varies by luminance. If two colors are
* provided, will generate a diverging color scale with the first
* color at min value, and second color at high value. Otherwise,
* the first three colors will be taken to correspond to
* [min, mean, max] values.
* @param domain the domain of the scale. The first number is the min, second
* number is the middle number, and the last number is the max.
*/
function getCustomColorScale(
colors: string[],
domain: [number, number, number]
): d3.ScaleLinear<number, number> {
const midColor = d3.color(colors[0]);
const rangeValues =
colors.length == 1
? [MIN_COLOR, midColor, midColor.darker(2)]
: colors.length == 2
? [colors[0], MIN_COLOR, colors[1]]
: colors.slice(0, 3);

return d3
.scaleLinear()
.domain(domain)
.nice()
.range(rangeValues as unknown as number[])
.interpolate(
d3.interpolateRgb as unknown as (colors: unknown) => (t: number) => number
);
}

/**
* Generates a color scale to be used for drawing choropleth map and legend.
* NOTE: Only return linear scales.
Expand All @@ -52,53 +131,39 @@ const TEMP_AGGREGATE_DIFF_DOMAIN = [-30, -15, 0, 15, 30];
* @param color the color to use as the middle color in the scale
* @param domain the domain of the scale. The first number is the min, second
* number is the middle number, and the last number is the max.
* @param customColorRange specific colors to use for the scale. See
* getCustomColorScale() for specifics.
*/
export function getColorScale(
statVar: string,
minValue: number,
meanValue: number,
maxValue: number,
color?: string,
domain?: [number, number, number]
domain?: [number, number, number],
customColorRange?: string[]
): d3.ScaleLinear<number, number> {
if (customColorRange) {
// If a specific set of colors is provided, use those colors
return getCustomColorScale(customColorRange, [
minValue,
meanValue,
maxValue,
]);
}

if (isTemperatureStatVar(statVar)) {
// Special handling of temperature stat vars, which need blue to red scale
return getTemperatureColorScale(statVar, domain);
}

const label = getStatsVarLabel(statVar);
const maxColor = color
? d3.color(color)
: isWetBulbStatVar(statVar)
? d3.color(d3.interpolateReds(1))
? d3.color(d3.interpolateReds(1)) // Use a red scale for wet-bulb temps
: d3.color(getColorFn([label])(label));
let domainValues: number[] = domain || [minValue, meanValue, maxValue];
if (isTemperatureStatVar(statVar)) {
let range: any[] = [
d3.interpolateBlues(1),
d3.interpolateBlues(0.8),
MIN_COLOR,
d3.interpolateReds(0.8),
d3.interpolateReds(1),
];

if (statVar.indexOf("Difference") >= 0) {
if (statVar.indexOf("Base") >= 0) {
domainValues = domain || TEMP_BASE_DIFF_DOMAIN;
} else if (statVar.indexOf("Dc Aggregate")) {
domainValues = domain || TEMP_AGGREGATE_DIFF_DOMAIN;
} else {
domainValues = domain || TEMP_MODEL_DIFF_DOMAIN;
}
} else {
domainValues = domain || TEMP_DOMAIN;
}
const min = domainValues[0];
const max = domainValues[domainValues.length - 1];
if (min >= 0) {
domainValues = [0, max / 2, max];
range = [MIN_COLOR, d3.interpolateReds(0.8), d3.interpolateReds(1)];
} else if (max <= 0) {
domainValues = [min, min / 2, 0];
range = [d3.interpolateBlues(1), d3.interpolateBlues(0.8), MIN_COLOR];
}
return d3.scaleLinear().domain(domainValues).nice().range(range);
}
const domainValues: number[] = domain || [minValue, meanValue, maxValue];
const rangeValues =
domainValues.length == 3
? [MIN_COLOR, maxColor, maxColor.darker(2)]
Expand Down
4 changes: 3 additions & 1 deletion static/js/chart/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,11 @@ export interface MapPoint {

export interface ChartOptions {
apiRoot?: string;
unit?: string;
// specific colors to use
colors?: string[];
// whether to draw chart in lollipop style, used for bar charts
lollipop?: boolean;
unit?: string;
}

export interface GroupLineChartOptions extends ChartOptions {
Expand Down
7 changes: 6 additions & 1 deletion static/js/components/tiles/bar_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export interface BarTilePropType {
className?: string;
// A list of related places to show comparison with the main place.
comparisonPlaces: string[];
// A list of specific colors to use
colors?: string[];
enclosedPlaceType: string;
horizontal?: boolean;
id: string;
Expand Down Expand Up @@ -293,6 +295,7 @@ export function draw(
chartData.dataGroup,
formatNumber,
{
colors: props.colors,
stacked: props.stacked,
style: {
barHeight: props.barHeight,
Expand All @@ -311,6 +314,7 @@ export function draw(
chartData.dataGroup,
formatNumber,
{
colors: props.colors,
unit: chartData.unit,
}
);
Expand All @@ -323,8 +327,9 @@ export function draw(
chartData.dataGroup,
formatNumber,
{
unit: chartData.unit,
colors: props.colors,
lollipop: props.useLollipop,
unit: chartData.unit,
}
);
}
Expand Down
23 changes: 14 additions & 9 deletions static/js/components/tiles/donut_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,24 @@ const FILTER_STAT_VAR = "Count_Person";
const DEFAULT_X_LABEL_LINK_ROOT = "/place/";

export interface DonutTilePropType {
// API root
apiRoot?: string;
// Extra classes to add to the container.
className?: string;
// Colors to use
colors?: string[];
// Id for the chart
id: string;
// Title to put at top of chart
title: string;
// Whether to draw as full pie chart instead
pie?: boolean;
// The primary place of the page (disaster, topic, nl)
place: NamedTypedPlace;
// Stat vars to plot
statVarSpec: StatVarSpec[];
// Height, in px, for the SVG chart.
svgChartHeight: number;
// Whether to draw as full pie chart instead
pie?: boolean;
// Extra classes to add to the container.
className?: string;
// API root
apiRoot?: string;
// Title to put at top of chart
title: string;
}

interface DonutChartData {
Expand Down Expand Up @@ -236,6 +238,9 @@ export function draw(
svgWidth || svgContainer.offsetWidth,
props.svgChartHeight,
chartData.dataGroup,
props.pie
props.pie,
{
colors: props.colors,
}
);
}
Loading

0 comments on commit f734137

Please sign in to comment.