Skip to content
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

[Library] Allow stat var <> color mapping to be set by users #2919

Merged
merged 14 commits into from
Jul 11, 2023
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