From 08bf6b42b97c2948f039e5a49121e0f7aeb4352d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Sat, 1 Jul 2023 15:50:55 +0200 Subject: [PATCH 1/2] feat: Add support for custom SVG color backgrounds --- src/charts/BarChart.ts | 46 +++++++++++++++++++++++++++++------ src/charts/BubbleChart.ts | 27 ++++++++++++++++---- src/charts/Chart.ts | 25 +++++++++++-------- src/charts/Datum.ts | 11 +++------ src/charts/PieChart.ts | 32 ++++++++++++++++++++---- src/charts/ScatterChart.ts | 36 ++++++++++++++++++++++----- src/charts/TreemapChart.ts | 33 +++++++++++++++++++++---- src/components/Axis.ts | 4 +++ src/components/AxisTick.ts | 23 +++++++++++++++--- src/components/ColorLegend.ts | 7 +++++- src/components/Step.ts | 10 +++++++- src/components/Svg.ts | 6 ++++- src/components/Text.ts | 7 +++++- src/index.ts | 19 +++++++++++---- src/types.ts | 4 +++ src/utils.ts | 36 +++++++++++++++++++++++++++ 16 files changed, 269 insertions(+), 57 deletions(-) diff --git a/src/charts/BarChart.ts b/src/charts/BarChart.ts index b0b9009..e85906e 100644 --- a/src/charts/BarChart.ts +++ b/src/charts/BarChart.ts @@ -13,7 +13,13 @@ import { InputGroupValue, TextDims, } from "../types"; -import { FONT_SIZE, getTextColor, max, sum } from "../utils"; +import { + FONT_SIZE, + deriveSubtlerColor, + getTextColor, + max, + sum, +} from "../utils"; import * as Chart from "./Chart"; import { STROKE_WIDTH, @@ -33,7 +39,10 @@ export type Info = Chart.BaseInfo & { horizontalAxis: InputAxis | undefined; }; -export const info = (inputStep: BarInputStep): Info => { +export const info = ( + svgBackgroundColor: string, + inputStep: BarInputStep +): Info => { const { chartSubtype = "grouped", layout = "vertical", @@ -42,7 +51,7 @@ export const info = (inputStep: BarInputStep): Info => { } = inputStep; return { - ...Chart.baseInfo(inputStep, shareDomain), + ...Chart.baseInfo(svgBackgroundColor, inputStep, shareDomain), type: "bar", subtype: chartSubtype, layout, @@ -115,6 +124,7 @@ export const getters = ( shareDomain, showValues, maxValue, + svgBackgroundColor, } = info; const { showDatumLabels, @@ -130,6 +140,10 @@ export const getters = ( const labelYShift = textDims.datumLabel.yShift; const valueHeight = textDims.datumValue.height; const valueYShift = textDims.datumValue.yShift; + const groupFill = deriveSubtlerColor(svgBackgroundColor); + const groupLabelFill = getTextColor(svgBackgroundColor); + const groupLabelStroke = svgBackgroundColor; + const datumStroke = svgBackgroundColor; if (isVertical) { const { x0Scale, x0bw, x1Scale, x1bw, yScale } = getVerticalScales({ @@ -164,7 +178,10 @@ export const getters = ( labelX, labelY, labelFontSize, + labelStroke: groupLabelStroke, labelStrokeWidth, + labelFill: groupLabelFill, + fill: groupFill, opacity, }; }, @@ -225,8 +242,12 @@ export const getters = ( const labelY = isGrouped ? s(0, dHeight * 0.5 - labelHeight - TEXT_MARGIN) : s(0, -(dHeight * 0.5 + labelYShift)); - const labelFontSize = showDatumLabels ? FONT_SIZE.datumLabel : 0; + const labelFontSize = s( + 0, + showDatumLabels ? FONT_SIZE.datumLabel : 0 + ); const labelFill = getTextColor(datumFill); + const labelStroke = datumFill; const valueWidth = svg.measureText(value, "datumValue").width; const valueX = isGrouped ? 0 @@ -248,7 +269,7 @@ export const getters = ( : -(dHeight * 0.5 + valueYShift) ); const valueFontSize = showValues ? s(0, FONT_SIZE.datumValue) : 0; - const valueFill = isGrouped ? "black" : labelFill; + const valueFill = isGrouped ? groupLabelFill : labelFill; const opacity = datum.opacity ?? 1; return { @@ -258,10 +279,12 @@ export const getters = ( y, rotate, fill: datumFill, + stroke: datumStroke, strokeWidth, labelX, labelY, labelFontSize, + labelStroke, labelFill, valueX, valueY, @@ -317,7 +340,10 @@ export const getters = ( labelX, labelY, labelFontSize, + labelStroke: groupLabelStroke, labelStrokeWidth, + labelFill: groupLabelFill, + fill: groupFill, opacity, }; }, @@ -379,8 +405,12 @@ export const getters = ( -dWidth * 0.5 + labelWidth * 0.5 + BASE_MARGIN * 0.5 ); const labelY = s(0, labelYShift); - const labelFontSize = showDatumLabels ? FONT_SIZE.datumLabel : 0; + const labelFontSize = s( + 0, + showDatumLabels ? FONT_SIZE.datumLabel : 0 + ); const labelFill = getTextColor(datumFill); + const labelStroke = datumFill; const valueWidth = svg.measureText(value, "datumValue").width; const valueX = isGrouped ? labelX + labelWidth > (dWidth + valueWidth + BASE_MARGIN) * 0.5 @@ -391,7 +421,7 @@ export const getters = ( : 0; const valueY = s(valueHeight * 0.5, valueYShift); const valueFontSize = showValues ? s(0, FONT_SIZE.datumValue) : 0; - const valueFill = isGrouped ? "black" : labelFill; + const valueFill = isGrouped ? groupLabelFill : labelFill; const opacity = datum.opacity ?? 1; return { @@ -401,10 +431,12 @@ export const getters = ( y, rotate, fill: datumFill, + stroke: datumStroke, strokeWidth, labelX, labelY, labelFontSize, + labelStroke, labelFill, valueX, valueY, diff --git a/src/charts/BubbleChart.ts b/src/charts/BubbleChart.ts index 47a33a6..8fa615e 100644 --- a/src/charts/BubbleChart.ts +++ b/src/charts/BubbleChart.ts @@ -4,7 +4,7 @@ import { Svg } from "../components"; import { BUBBLE, getPathData } from "../coords"; import { Dimensions, ResolvedDimensions } from "../dims"; import { BaseMax, BubbleInputStep, InputGroupValue, TextDims } from "../types"; -import { FONT_SIZE, getTextColor } from "../utils"; +import { FONT_SIZE, deriveSubtlerColor, getTextColor } from "../utils"; import * as Chart from "./Chart"; import { STROKE_WIDTH, @@ -22,11 +22,14 @@ export type Info = Chart.BaseInfo & { canUseHorizontalAxis: false; }; -export const info = (inputStep: BubbleInputStep): Info => { +export const info = ( + svgBackgroundColor: string, + inputStep: BubbleInputStep +): Info => { const { groups, shareDomain = false } = inputStep; return { - ...Chart.baseInfo(inputStep, shareDomain), + ...Chart.baseInfo(svgBackgroundColor, inputStep, shareDomain), type: "bubble", groups, maxValue: getMaxValue(inputStep), @@ -51,7 +54,8 @@ export const getters = ( cartoonize: boolean; } ) => { - const { groups, maxValue, shareDomain, showValues } = info; + const { groups, maxValue, shareDomain, showValues, svgBackgroundColor } = + info; const { showDatumLabels, dims: { width, height, size, margin }, @@ -64,6 +68,10 @@ export const getters = ( // If a custom maxValue was provided, we need to shift the bubbles to the center. const maxValueShift = maxValue.kc * size * 0.5; const showDatumLabelsAndValues = showDatumLabels && showValues; + const groupFill = deriveSubtlerColor(svgBackgroundColor); + const groupLabelFill = getTextColor(svgBackgroundColor); + const groupLabelStroke = svgBackgroundColor; + const datumStroke = svgBackgroundColor; for (const group of root.children || []) { const { key } = group.data; @@ -103,7 +111,10 @@ export const getters = ( labelX, labelY, labelFontSize, + labelStroke: groupLabelStroke, labelStrokeWidth, + labelFill: groupLabelFill, + fill: groupFill, opacity, }; }, @@ -137,8 +148,12 @@ export const getters = ( const labelY = textDims.datumLabel.yShift - (showDatumLabelsAndValues ? textDims.datumLabel.height * 0.5 : 0); - const labelFontSize = showDatumLabels ? FONT_SIZE.datumLabel : 0; + const labelFontSize = s( + 0, + showDatumLabels ? FONT_SIZE.datumLabel : 0 + ); const labelFill = getTextColor(datumFill); + const labelStroke = datumFill; const valueX = 0; const valueY = s( 0, @@ -159,10 +174,12 @@ export const getters = ( y, rotate, fill: datumFill, + stroke: datumStroke, strokeWidth, labelX, labelY, labelFontSize, + labelStroke, labelFill, valueX, valueY, diff --git a/src/charts/Chart.ts b/src/charts/Chart.ts index 413ff13..83c220b 100644 --- a/src/charts/Chart.ts +++ b/src/charts/Chart.ts @@ -13,9 +13,11 @@ export type BaseInfo = { dataKeys: string[]; shareDomain: boolean; showValues: boolean; + svgBackgroundColor: string; }; export const baseInfo = ( + svgBackgroundColor: string, inputStep: InputStep, shareDomain: boolean ): BaseInfo => { @@ -25,21 +27,21 @@ export const baseInfo = ( ); const showValues = inputStep.showValues ?? false; - return { groupsKeys, dataKeys, shareDomain, showValues }; + return { groupsKeys, dataKeys, shareDomain, showValues, svgBackgroundColor }; }; -export const info = (inputStep: InputStep) => { +export const info = (svgBackgroundColor: string, inputStep: InputStep) => { switch (inputStep.chartType) { case "bar": - return BarChart.info(inputStep); + return BarChart.info(svgBackgroundColor, inputStep); case "bubble": - return BubbleChart.info(inputStep); + return BubbleChart.info(svgBackgroundColor, inputStep); case "pie": - return PieChart.info(inputStep); + return PieChart.info(svgBackgroundColor, inputStep); case "scatter": - return ScatterChart.info(inputStep); + return ScatterChart.info(svgBackgroundColor, inputStep); case "treemap": - return TreemapChart.info(inputStep); + return TreemapChart.info(svgBackgroundColor, inputStep); default: const _exhaustiveCheck: never = inputStep; return _exhaustiveCheck; @@ -73,7 +75,10 @@ export type G = { labelX: number; labelY: number; labelFontSize: number; + labelStroke: string; labelStrokeWidth: number; + labelFill: string; + fill: string; opacity: number; }; @@ -205,7 +210,7 @@ export const render = ({ .join("path") .attr("class", "background") .attr("transform", (d) => `translate(${d.x}, ${d.y})`) - .style("fill", "#f5f5f5") + .style("fill", (d) => d.fill) .attr("d", (d) => d.d); const dataSelection = groupsSelection @@ -265,9 +270,9 @@ export const render = ({ .attr("class", "label") .attr("x", (d) => d.labelX) .attr("y", (d) => d.labelY) - .style("fill", "#333333") + .style("fill", (d) => d.labelFill) .style("paint-order", "stroke") - .style("stroke", "white") + .style("stroke", (d) => d.labelStroke) .style("stroke-width", (d) => d.labelStrokeWidth) .style("stroke-linecap", "round") .style("stroke-linejoin", "round") diff --git a/src/charts/Datum.ts b/src/charts/Datum.ts index 227a3b1..3034e6c 100644 --- a/src/charts/Datum.ts +++ b/src/charts/Datum.ts @@ -11,10 +11,12 @@ export type G = { y: number; rotate: number; fill: string; + stroke: string; strokeWidth: number; labelX: number; labelY: number; labelFontSize: number; + labelStroke: string; labelFill: string; valueX: number; valueY: number; @@ -144,7 +146,7 @@ export const render = ({ .join("path") .attr("class", "shape") .attr("d", (d) => d.d) - .style("stroke", "white") + .style("stroke", (d) => d.stroke) .style("stroke-width", (d) => d.strokeWidth) .style("fill", (d) => d.fill); @@ -200,12 +202,7 @@ export const render = ({ .style("text-anchor", "middle") .style("dominant-baseline", "hanging") .style("paint-order", "stroke") - .style("stroke", (d) => - // FIXME - d.labelFill === "white" || d.labelFill === "rgb(255, 255, 255)" - ? "black" - : "white" - ) + .style("stroke", (d) => d.labelStroke) .style("stroke-width", 2) .style("user-select", "none") .style("pointer-events", "none") diff --git a/src/charts/PieChart.ts b/src/charts/PieChart.ts index 8481899..83e8c27 100644 --- a/src/charts/PieChart.ts +++ b/src/charts/PieChart.ts @@ -11,7 +11,12 @@ import { PieInputStep, TextDims, } from "../types"; -import { FONT_SIZE, getTextColor, radiansToDegrees } from "../utils"; +import { + FONT_SIZE, + deriveSubtlerColor, + getTextColor, + radiansToDegrees, +} from "../utils"; import * as Chart from "./Chart"; import { STROKE_WIDTH, @@ -28,11 +33,14 @@ export type Info = Chart.BaseInfo & { canUseHorizontalAxis: false; }; -export const info = (inputStep: PieInputStep): Info => { +export const info = ( + svgBackgroundColor: string, + inputStep: PieInputStep +): Info => { const { groups, shareDomain = true } = inputStep; return { - ...Chart.baseInfo(inputStep, shareDomain), + ...Chart.baseInfo(svgBackgroundColor, inputStep, shareDomain), type: "pie", groups, maxValue: getMaxValue(inputStep), @@ -57,7 +65,8 @@ export const getters = ( cartoonize: boolean; } ) => { - const { groups, maxValue, shareDomain, showValues } = info; + const { groups, maxValue, shareDomain, showValues, svgBackgroundColor } = + info; const { showDatumLabels, dims: { width, height, size, margin }, @@ -69,6 +78,10 @@ export const getters = ( const groupsGetters: Chart.Getter[] = []; const maxValueShift = maxValue.kc * size * 0.5; const showDatumLabelsAndValues = showDatumLabels && showValues; + const groupFill = deriveSubtlerColor(svgBackgroundColor); + const groupLabelFill = getTextColor(svgBackgroundColor); + const groupLabelStroke = svgBackgroundColor; + const datumStroke = svgBackgroundColor; for (const group of root.children || []) { const { key } = group.data; @@ -101,7 +114,10 @@ export const getters = ( labelX, labelY, labelFontSize, + labelStroke: groupLabelStroke, labelStrokeWidth, + labelFill: groupLabelFill, + fill: groupFill, opacity, }; }, @@ -177,8 +193,12 @@ export const getters = ( textDims.datumLabel.yShift - // TODO: move by cos / sin. (showDatumLabelsAndValues ? textDims.datumLabel.height * 0.5 : 0); - const labelFontSize = showDatumLabels ? FONT_SIZE.datumLabel : 0; + const labelFontSize = s( + 0, + showDatumLabels ? FONT_SIZE.datumLabel : 0 + ); const labelFill = getTextColor(datumFill); + const labelStroke = labelFill === "white" ? "black" : "white"; const valueX = labelX; const valueY = labelY + (showDatumLabels ? textDims.datumLabel.height : 0); @@ -196,10 +216,12 @@ export const getters = ( ? rotateDegrees - 360 : rotateDegrees, fill: datumFill, + stroke: datumStroke, strokeWidth, labelX, labelY, labelFontSize, + labelStroke, labelFill, valueX, valueY, diff --git a/src/charts/ScatterChart.ts b/src/charts/ScatterChart.ts index 3c052f1..2cc2c9a 100644 --- a/src/charts/ScatterChart.ts +++ b/src/charts/ScatterChart.ts @@ -12,7 +12,13 @@ import { ScatterInputStep, TextDims, } from "../types"; -import { FONT_SIZE, getTextColor, max, min } from "../utils"; +import { + FONT_SIZE, + deriveSubtlerColor, + getTextColor, + max, + min, +} from "../utils"; import * as Chart from "./Chart"; import { STROKE_WIDTH, @@ -29,11 +35,14 @@ export type Info = Chart.BaseInfo & { horizontalAxis: InputAxis | undefined; }; -export const info = (inputStep: ScatterInputStep): Info => { +export const info = ( + svgBackgroundColor: string, + inputStep: ScatterInputStep +): Info => { const { groups, shareDomain = false } = inputStep; return { - ...Chart.baseInfo(inputStep, shareDomain), + ...Chart.baseInfo(svgBackgroundColor, inputStep, shareDomain), type: "scatter", groups, maxValue: getMaxValue(inputStep), @@ -74,15 +83,21 @@ export const getters = ( groups, maxValue: { x: xMaxValue, y: yMaxValue }, shareDomain, + svgBackgroundColor, } = info; const { showDatumLabels, dims: { width, height, margin, BASE_MARGIN }, colorMap, cartoonize, + textDims, } = props; const { xScale, yScale } = getScales({ xMaxValue, yMaxValue, width, height }); const groupsGetters: Chart.Getter[] = []; + const groupFill = deriveSubtlerColor(svgBackgroundColor); + const groupLabelFill = getTextColor(svgBackgroundColor); + const groupLabelStroke = svgBackgroundColor; + const datumStroke = svgBackgroundColor; for (const group of groups) { const { key } = group; @@ -113,7 +128,10 @@ export const getters = ( labelX, labelY, labelFontSize, + labelStroke: groupLabelStroke, labelStrokeWidth, + labelFill: groupLabelFill, + fill: groupFill, opacity, }; }, @@ -153,9 +171,13 @@ export const getters = ( const rotate = getRotate(_g?.rotate); const strokeWidth = s(0, STROKE_WIDTH); const labelX = 0; - const labelY = 0; - const labelFontSize = showDatumLabels ? FONT_SIZE.datumLabel : 0; - const labelFill = getTextColor(datumFill); + const labelY = -textDims.datumLabel.yShift; + const labelFontSize = s( + 0, + showDatumLabels ? FONT_SIZE.datumLabel : 0 + ); + const labelFill = groupLabelFill; + const labelStroke = svgBackgroundColor; const valueX = 0; const valueY = 0; const valueFontSize = 0; @@ -169,10 +191,12 @@ export const getters = ( y, rotate, fill: datumFill, + stroke: datumStroke, strokeWidth, labelX, labelY, labelFontSize, + labelStroke, labelFill, valueX, valueY, diff --git a/src/charts/TreemapChart.ts b/src/charts/TreemapChart.ts index ecb8e61..63d3161 100644 --- a/src/charts/TreemapChart.ts +++ b/src/charts/TreemapChart.ts @@ -20,7 +20,7 @@ import { TreemapInputStep, TreemapLayout, } from "../types"; -import { FONT_SIZE, getTextColor } from "../utils"; +import { FONT_SIZE, deriveSubtlerColor, getTextColor } from "../utils"; import * as Chart from "./Chart"; import { TreemapHierarchyRoot } from "./types"; import { @@ -39,11 +39,14 @@ export type Info = Chart.BaseInfo & { maxValue: BaseMax; }; -export const info = (inputStep: TreemapInputStep): Info => { +export const info = ( + svgBackgroundColor: string, + inputStep: TreemapInputStep +): Info => { const { layout = "resquarify", groups, shareDomain = false } = inputStep; return { - ...Chart.baseInfo(inputStep, shareDomain), + ...Chart.baseInfo(svgBackgroundColor, inputStep, shareDomain), type: "treemap", layout, groups, @@ -67,7 +70,14 @@ export const getters = ( cartoonize: boolean; } ) => { - const { layout, groups, maxValue, shareDomain, showValues } = info; + const { + layout, + groups, + maxValue, + shareDomain, + showValues, + svgBackgroundColor, + } = info; const { showDatumLabels, svg, @@ -83,6 +93,10 @@ export const getters = ( layout, }); const groupsGetters: Chart.Getter[] = []; + const groupFill = deriveSubtlerColor(svgBackgroundColor); + const groupLabelFill = getTextColor(svgBackgroundColor); + const groupLabelStroke = svgBackgroundColor; + const datumStroke = svgBackgroundColor; for (const group of root.children || []) { const { key } = group.data; @@ -119,7 +133,10 @@ export const getters = ( labelX, labelY, labelFontSize, + labelStroke: groupLabelStroke, labelStrokeWidth, + labelFill: groupLabelFill, + fill: groupFill, opacity, }; }, @@ -162,8 +179,12 @@ export const getters = ( const labelWidth = svg.measureText(key, "datumLabel").width; const labelX = s(0, (labelWidth - dWidth) * 0.5 + TEXT_MARGIN); const labelY = s(0, -(dHeight * 0.5 + textDims.datumLabel.yShift)); - const labelFontSize = showDatumLabels ? FONT_SIZE.datumLabel : 0; + const labelFontSize = s( + 0, + showDatumLabels ? FONT_SIZE.datumLabel : 0 + ); const labelFill = getTextColor(datumFill); + const labelStroke = datumFill; const valueWidth = svg.measureText(value, "datumValue").width; const valueX = s(0, (valueWidth - dWidth) * 0.5 + TEXT_MARGIN); const valueY = @@ -179,10 +200,12 @@ export const getters = ( y, rotate, fill: datumFill, + stroke: datumStroke, strokeWidth, labelX, labelY, labelFontSize, + labelStroke, labelFill, valueX, valueY, diff --git a/src/components/Axis.ts b/src/components/Axis.ts index 19313af..6fd01ba 100644 --- a/src/components/Axis.ts +++ b/src/components/Axis.ts @@ -96,6 +96,7 @@ export const getters = ({ title, titleMargin, svg, + svgBackgroundColor, dims, tickHeight, ticksCount, @@ -107,6 +108,7 @@ export const getters = ({ title: string; titleMargin: Margin; svg: Svg; + svgBackgroundColor: string; dims: ResolvedDimensions; tickHeight: number; ticksCount: number; @@ -142,6 +144,7 @@ export const getters = ({ type: "axisTitle", anchor: type === "vertical" ? "start" : "end", svg, + svgBackgroundColor, dims: { ...dims, margin: titleMargin }, }) : undefined, @@ -153,6 +156,7 @@ export const getters = ({ tickHeight, tickFormat, dims, + svgBackgroundColor, }), }; }; diff --git a/src/components/AxisTick.ts b/src/components/AxisTick.ts index 8fde2c1..4e1c3c0 100644 --- a/src/components/AxisTick.ts +++ b/src/components/AxisTick.ts @@ -3,7 +3,12 @@ import { Selection } from "d3-selection"; import { HALF_FONT_K } from "../charts/utils"; import { ResolvedDimensions } from "../dims"; import { AxisType } from "../types"; -import { FONT_SIZE, FONT_WEIGHT } from "../utils"; +import { + FONT_SIZE, + FONT_WEIGHT, + deriveSubtlerColor, + getTextColor, +} from "../utils"; import * as Axis from "./Axis"; import * as Generic from "./Generic"; @@ -18,6 +23,9 @@ type G = { size: number; tickLabelHeight: number; fontSize: number; + color: string; + lightLineColor: string; + boldLineColor: string; opacity: number; }; @@ -29,6 +37,7 @@ export const getters = ({ maxValue, _maxValue, dims: { width, height }, + svgBackgroundColor, tickHeight, tickFormat, }: { @@ -37,6 +46,7 @@ export const getters = ({ maxValue: number; _maxValue: number | undefined; dims: ResolvedDimensions; + svgBackgroundColor: string; tickHeight: number; tickFormat: (d: number) => string; }): Getter[] => { @@ -61,6 +71,9 @@ export const getters = ({ (isVerticalAxis ? 1 - tick / maxValue : tick / maxValue) * size, scale(tick) ); + const color = getTextColor(svgBackgroundColor); + const lightLineColor = deriveSubtlerColor(svgBackgroundColor); + const boldLineColor = deriveSubtlerColor(svgBackgroundColor, 3); return { x: isVerticalAxis ? x : y, @@ -68,6 +81,9 @@ export const getters = ({ size: isVerticalAxis ? width : height, tickLabelHeight: tickHeight, fontSize: FONT_SIZE.axisTick, + color, + lightLineColor, + boldLineColor, opacity: s(0, 1), }; }, @@ -123,6 +139,7 @@ export const render = ({ .style("font-size", (d) => `${d.fontSize}px`) .style("font-weight", FONT_WEIGHT.axisTick) .style("dominant-baseline", "hanging") + .style("fill", (d) => d.color) .text((d) => d.key) ) .call((g) => @@ -133,7 +150,7 @@ export const render = ({ .attr("class", "bold-line") .attr(isVerticalAxis ? "x1" : "y1", isVerticalAxis ? -SIZE : 0) .attr(isVerticalAxis ? "x2" : "y2", isVerticalAxis ? 0 : SIZE) - .style("stroke", "#696969") + .style("stroke", (d) => d.boldLineColor) ) .call((g) => g @@ -145,6 +162,6 @@ export const render = ({ .attr(isVerticalAxis ? "x2" : "y2", (d) => isVerticalAxis ? d.size : -d.size ) - .style("stroke", "#ededed") + .style("stroke", (d) => d.lightLineColor) ); }; diff --git a/src/components/ColorLegend.ts b/src/components/ColorLegend.ts index 9e9c1f9..12aa9e2 100644 --- a/src/components/ColorLegend.ts +++ b/src/components/ColorLegend.ts @@ -2,7 +2,7 @@ import * as Chart from "../charts/Chart"; import { ColorMap } from "../colors"; import { Dimensions, ResolvedDimensions } from "../dims"; import { Anchor, InputDatum, InputStep } from "../types"; -import { FONT_SIZE, FONT_WEIGHT, max } from "../utils"; +import { FONT_SIZE, FONT_WEIGHT, getTextColor, max } from "../utils"; import * as Generic from "./Generic"; import { SVGSelection, Svg } from "./Svg"; @@ -52,6 +52,7 @@ type G = { labelY: number; labelFontSize: number; labelFontWeight: number; + labelColor: string; fill: string; opacity: number; }; @@ -64,6 +65,7 @@ export const getters = ({ title, itemHeight, svg, + svgBackgroundColor, dims: { width, height, margin, BASE_MARGIN }, }: { colorMap: ColorMap; @@ -71,6 +73,7 @@ export const getters = ({ title: string; itemHeight: number; svg: Svg; + svgBackgroundColor: string; dims: ResolvedDimensions; }): Getter[] => { const getters: Getter[] = []; @@ -218,6 +221,7 @@ export const getters = ({ labelFontWeight: isTitle ? FONT_WEIGHT.legendTitle : FONT_WEIGHT.legendItem, + labelColor: getTextColor(svgBackgroundColor), fill: color, opacity: s(0, 1), }; @@ -273,6 +277,7 @@ export const render = ({ .style("font-size", (d) => `${d.labelFontSize}px`) .style("font-weight", (d) => d.labelFontWeight) .style("dominant-baseline", "hanging") + .style("fill", (d) => d.labelColor) .text((d) => d.key) ); }; diff --git a/src/components/Step.ts b/src/components/Step.ts index 24c0088..9a74fce 100644 --- a/src/components/Step.ts +++ b/src/components/Step.ts @@ -16,16 +16,19 @@ export type Getter = { }; export const getters = ({ + options, steps: inputSteps, svg, width, height, }: { + options: { svgBackgroundColor: string }; steps: InputStep[]; svg: Svg; width: number; height: number; }): Getter[] => { + const { svgBackgroundColor } = options; const steps: Getter[] = []; let _maxHorizontalAxisValue: number | undefined; let _maxVerticalAxisValue: number | undefined; @@ -46,7 +49,7 @@ export const getters = ({ } = step; const dims = new Dimensions(width, height); - const chartInfo = Chart.info(step); + const chartInfo = Chart.info(svgBackgroundColor, step); const colorLegendInfo = ColorLegend.info(step, chartInfo, colorMap); const verticalAxisInfo = Axis.info("vertical", chartInfo); const horizontalAxisInfo = Axis.info("horizontal", chartInfo); @@ -55,6 +58,7 @@ export const getters = ({ if (title !== undefined) { titleGetter = Text.getter({ svg, + svgBackgroundColor, text: title, type: "title", anchor: titleAnchor, @@ -72,6 +76,7 @@ export const getters = ({ if (subtitle !== undefined) { subtitleGetter = Text.getter({ svg, + svgBackgroundColor, text: subtitle, type: "subtitle", anchor: subtitleAnchor, @@ -97,6 +102,7 @@ export const getters = ({ title: legendTitle, itemHeight: textDims.legendItem.height, svg, + svgBackgroundColor, dims: dims.resolve(), }); ColorLegend.updateDims({ @@ -163,6 +169,7 @@ export const getters = ({ left: 0, }, svg, + svgBackgroundColor, dims: resolvedDims, tickHeight: textDims.axisTick.height, ticksCount, @@ -209,6 +216,7 @@ export const getters = ({ left: 0, }, svg, + svgBackgroundColor, dims: dims.resolve(), maxValue, _maxValue: _maxVerticalAxisValue, diff --git a/src/components/Svg.ts b/src/components/Svg.ts index b1f98c7..0743cff 100644 --- a/src/components/Svg.ts +++ b/src/components/Svg.ts @@ -15,7 +15,10 @@ export type SVGSelection = Selection< undefined >; -export const makeSvg = (div: HTMLDivElement): Svg => { +export const makeSvg = ( + div: HTMLDivElement, + background: string = "#FFFFFF" +): Svg => { const selection = select(div) .selectAll("svg") .data([null]) @@ -24,6 +27,7 @@ export const makeSvg = (div: HTMLDivElement): Svg => { .style("height", "100%") .style("transform", "translate3d(0, 0, 0)") .style("border-left", "3px solid transparent") + .style("background", background) .style("transition", "border-left 0.3s ease") as SVGSelection; const measure = (): DOMRect => { diff --git a/src/components/Text.ts b/src/components/Text.ts index 5c96a52..d2e3827 100644 --- a/src/components/Text.ts +++ b/src/components/Text.ts @@ -1,7 +1,7 @@ import { Selection } from "d3-selection"; import { Dimensions, ResolvedDimensions } from "../dims"; import { Anchor, TextType } from "../types"; -import { FONT_SIZE, FONT_WEIGHT } from "../utils"; +import { FONT_SIZE, FONT_WEIGHT, getTextColor } from "../utils"; import * as Generic from "./Generic"; import { Svg } from "./Svg"; @@ -10,6 +10,7 @@ type G = { y: number; fontSize: number; fontWeight: number; + color: string; opacity: number; }; @@ -20,12 +21,14 @@ export const getter = ({ type, anchor, svg, + svgBackgroundColor, dims: { fullWidth, margin }, }: { text: string; type: TextType; anchor: Anchor; svg: Svg; + svgBackgroundColor: string; dims: ResolvedDimensions; }): Getter => { const { width: textWidth } = svg.measureText(text, type); @@ -51,6 +54,7 @@ export const getter = ({ y: s(margin.top, null, _g?.y), fontSize: FONT_SIZE[type], fontWeight: FONT_WEIGHT[type], + color: getTextColor(svgBackgroundColor), opacity: s(0, 1), }; }, @@ -85,6 +89,7 @@ export const render = ({ .style("font-size", (d) => `${d.fontSize}px`) .style("font-weight", (d) => d.fontWeight) .style("dominant-baseline", "hanging") + .style("fill", (d) => d.color) .style("opacity", (d) => d.opacity) .text((d) => d.key); }; diff --git a/src/index.ts b/src/index.ts index 9d99c6a..d108874 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,19 @@ import { makeSvg, makeTooltip, Step } from "./components"; -import { InputStep } from "./types"; +import { InputStep, StoryOptions } from "./types"; /** * Creates a new `Story` object. * * @param div - `div` element that will be used as Story's container. * @param steps - Array of step configs. + * @param options - Story options. * * @returns`Story` object. */ const makeStory = ( div: HTMLDivElement, - steps: InputStep[] + steps: InputStep[], + options: StoryOptions = {} ): { /** * Renders a given step. @@ -26,7 +28,8 @@ const makeStory = ( indicateProgress?: boolean ) => void; } => { - const svg = makeSvg(div); + const { svgBackgroundColor = "#FFFFFF" } = options; + const svg = makeSvg(div, svgBackgroundColor); const tooltip = makeTooltip(div); let loaded = false; @@ -54,7 +57,13 @@ const makeStory = ( }).observe(div); const prepareStepsIntsMap = (width: number, height: number): void => { - const getters = Step.getters({ steps, svg, width, height }); + const getters = Step.getters({ + options: { svgBackgroundColor }, + steps, + svg, + width, + height, + }); intsMap = Step.intsMap({ steps: getters, svg }); }; @@ -83,4 +92,4 @@ const makeStory = ( return { render }; }; -export { makeStory, InputStep }; +export { InputStep, makeStory }; diff --git a/src/types.ts b/src/types.ts index a62f9c7..a495c9d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,10 @@ import { PaletteName } from "./colors"; // External. +export type StoryOptions = { + svgBackgroundColor?: string; +}; + type BaseInputStep = { key: string; diff --git a/src/utils.ts b/src/utils.ts index 7ebf35a..d67b122 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -81,3 +81,39 @@ export const getTextColor = (hex: string): "white" | "black" => { return yiq >= 128 ? "black" : "white"; }; + +export const reverseTextColor = ( + color: "white" | "black" +): "white" | "black" => { + return color === "white" ? "black" : "white"; +}; + +export const brighten = (hex: string, percent: number): string => { + const f = parseInt(hex.slice(1), 16); + const t = percent < 0 ? 0 : 255; + const p = percent < 0 ? percent * -1 : percent; + const R = f >> 16; + const G = (f >> 8) & 0x00ff; + const B = f & 0x0000ff; + + return ( + "#" + + ( + 0x1000000 + + (Math.round((t - R) * p) + R) * 0x10000 + + (Math.round((t - G) * p) + G) * 0x100 + + (Math.round((t - B) * p) + B) + ) + .toString(16) + .slice(1) + ); +}; + +export const darken = (hex: string, percent: number): string => { + return brighten(hex, percent * -1); +}; + +export const deriveSubtlerColor = (hex: string, k: number = 1): string => { + const textColor = getTextColor(hex); + return textColor === "white" ? brighten(hex, 0.2 * k) : darken(hex, 0.08 * k); +}; From 62e4bc66620a7f64eac2c48160d88f26a6efdf2e Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Sat, 1 Jul 2023 16:02:14 +0200 Subject: [PATCH 2/2] fix: Tests ...do not compare colors, as they can have different formats, especially when interpolated with d3 --- src/components/Text.spec.ts | 57 +++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/components/Text.spec.ts b/src/components/Text.spec.ts index 81c75c9..a823611 100644 --- a/src/components/Text.spec.ts +++ b/src/components/Text.spec.ts @@ -11,6 +11,7 @@ describe("Text", () => { type: "title", anchor: "middle", dims: dims.resolve(), + svgBackgroundColor: "white", }); const enterInts = Text.ints({ getters: [enterGetter], @@ -62,14 +63,17 @@ describe("Text", () => { describe("resolve", () => { test("0", () => { - const { key, ...r } = Text.resolve({ ints: enterInts, t: 0 })[0]; - const g = enterGetter.g({ s: (enter) => enter }); + const { key, color, ...r } = Text.resolve({ ints: enterInts, t: 0 })[0]; + const { color: gColor, ...g } = enterGetter.g({ s: (enter) => enter }); expect(r).toEqual(g); }); test("0.5", () => { - const { key, ...r } = Text.resolve({ ints: enterInts, t: 0.5 })[0]; + const { key, color, ...r } = Text.resolve({ + ints: enterInts, + t: 0.5, + })[0]; expect(r).toEqual({ x: 400, @@ -81,8 +85,10 @@ describe("Text", () => { }); test("1", () => { - const { key, ...r } = Text.resolve({ ints: enterInts, t: 1 })[0]; - const g = enterGetter.g({ s: (enter, update) => update ?? enter }); + const { key, color, ...r } = Text.resolve({ ints: enterInts, t: 1 })[0]; + const { color: gColor, ...g } = enterGetter.g({ + s: (enter, update) => update ?? enter, + }); expect(r).toEqual(g); }); @@ -95,6 +101,7 @@ describe("Text", () => { type: "datumLabel", anchor: "start", dims: dims.resolve(), + svgBackgroundColor: "white", }); const updateInts = Text.ints({ getters: [updateGetter], @@ -146,14 +153,22 @@ describe("Text", () => { describe("resolve", () => { test("0", () => { - const { key, ...r } = Text.resolve({ ints: updateInts, t: 0 })[0]; - const g = enterGetter.g({ s: (enter, update) => update ?? enter }); + const { key, color, ...r } = Text.resolve({ + ints: updateInts, + t: 0, + })[0]; + const { color: gColor, ...g } = enterGetter.g({ + s: (enter, update) => update ?? enter, + }); expect(r).toEqual(g); }); test("0.5", () => { - const { key, ...r } = Text.resolve({ ints: updateInts, t: 0.5 })[0]; + const { key, color, ...r } = Text.resolve({ + ints: updateInts, + t: 0.5, + })[0]; expect(r).toEqual({ x: 208, @@ -168,8 +183,13 @@ describe("Text", () => { }); test("1", () => { - const { key, ...r } = Text.resolve({ ints: updateInts, t: 1 })[0]; - const g = updateGetter.g({ s: (enter, update) => update ?? enter }); + const { key, color, ...r } = Text.resolve({ + ints: updateInts, + t: 1, + })[0]; + const { color: gColor, ...g } = updateGetter.g({ + s: (enter, update) => update ?? enter, + }); expect(r).toEqual(g); }); @@ -193,14 +213,19 @@ describe("Text", () => { describe("resolve", () => { test("0", () => { - const { key, ...r } = Text.resolve({ ints: exitInts, t: 0 })[0]; - const g = updateGetter.g({ s: (enter, update) => update ?? enter }); + const { key, color, ...r } = Text.resolve({ ints: exitInts, t: 0 })[0]; + const { color: gColor, ...g } = updateGetter.g({ + s: (enter, update) => update ?? enter, + }); expect(r).toEqual(g); }); test("0.5", () => { - const { key, ...r } = Text.resolve({ ints: exitInts, t: 0.5 })[0]; + const { key, color, ...r } = Text.resolve({ + ints: exitInts, + t: 0.5, + })[0]; expect(r).toEqual({ x: 16, @@ -212,8 +237,10 @@ describe("Text", () => { }); test("1", () => { - const { key, ...r } = Text.resolve({ ints: exitInts, t: 1 })[0]; - const g = updateGetter.g({ s: (enter, _, exit) => exit ?? enter }); + const { key, color, ...r } = Text.resolve({ ints: exitInts, t: 1 })[0]; + const { color: gColor, ...g } = updateGetter.g({ + s: (enter, _, exit) => exit ?? enter, + }); expect(r).toEqual(g); });