From 34b3a1903bc16654cd17120fb3777678389d952a Mon Sep 17 00:00:00 2001 From: Imogen Fletcher Date: Thu, 19 Sep 2019 14:23:59 +0200 Subject: [PATCH 1/6] Add CSS to axes. --- packages/visualizations/src/Axis.tsx | 22 +++++++++++++++++----- packages/visualizations/src/theme.ts | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/visualizations/src/Axis.tsx b/packages/visualizations/src/Axis.tsx index a170399..2f57f6a 100644 --- a/packages/visualizations/src/Axis.tsx +++ b/packages/visualizations/src/Axis.tsx @@ -1,10 +1,11 @@ import { axisBottom, axisLeft, axisTop, axisRight } from "d3-axis"; import { format as d3Format } from "d3-format"; import { ScaleBand, ScaleLinear } from "d3-scale"; -import { select } from "d3-selection"; +import { select, Selection as D3Selection } from "d3-selection"; import React, { useEffect, useRef } from "react"; import { useAxisTransform } from "./Chart"; import { isScaleContinuous } from "./scale"; +import theme from "./theme"; export interface AxisProps { /** see https://github.com/d3/d3-scale */ @@ -16,6 +17,12 @@ export interface AxisProps { maxNumberOfTicks?: number; } +const applyStyles = (axis: D3Selection) => { + axis.selectAll("text").style("color", theme.colors.axis.label); + axis.selectAll("path").style("color", theme.colors.axis.border); + axis.selectAll("line").style("color", theme.colors.axis.border); +}; + export const Axis: React.FC = React.memo(({ scale, transform, position, maxNumberOfTicks }) => { const defaultTransform = useAxisTransform(position!); const ref = useRef(null); @@ -28,18 +35,23 @@ export const Axis: React.FC = React.memo(({ scale, transform, positio ? (d: any, i: number) => (i % maxNumberOfTicks === 0 ? formatter(d) : null) : formatter; + let axis: D3Selection; switch (position) { case "bottom": - select(ref.current).call(axisBottom(scale).tickFormat(tickFormat)); + axis = select(ref.current).call(axisBottom(scale).tickFormat(tickFormat)); + applyStyles(axis); break; case "top": - select(ref.current).call(axisTop(scale).tickFormat(tickFormat)); + axis = select(ref.current).call(axisTop(scale).tickFormat(tickFormat)); + applyStyles(axis); break; case "left": - select(ref.current).call(axisLeft(scale).tickFormat(tickFormat)); + axis = select(ref.current).call(axisLeft(scale).tickFormat(tickFormat)); + applyStyles(axis); break; case "right": - select(ref.current).call(axisRight(scale).tickFormat(tickFormat)); + axis = select(ref.current).call(axisRight(scale).tickFormat(tickFormat)); + applyStyles(axis); break; } } diff --git a/packages/visualizations/src/theme.ts b/packages/visualizations/src/theme.ts index ccfc1c1..fb0d70b 100644 --- a/packages/visualizations/src/theme.ts +++ b/packages/visualizations/src/theme.ts @@ -35,7 +35,7 @@ const palettes = { const axisColors = { border: "#adadad", rules: "#e8e8e8", - label: "#999999", + label: "#545454", }; const focusColors = { From 5509412d5952a465a57b3bd9d2442e70875d6ed3 Mon Sep 17 00:00:00 2001 From: Imogen Fletcher Date: Thu, 19 Sep 2019 14:24:50 +0200 Subject: [PATCH 2/6] Fix maxNumberOfTicks behaviour. --- .../src/vizualisations/1-bar-chart.stories.tsx | 6 +++++- packages/visualizations/src/Axis.tsx | 17 +++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx b/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx index 49901f4..dbaaa7d 100644 --- a/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx +++ b/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx @@ -136,7 +136,11 @@ const BarChart = ({ style={(row: RowCursor) => ({ fill: colorScale(row) })} /> ))} - + diff --git a/packages/visualizations/src/Axis.tsx b/packages/visualizations/src/Axis.tsx index 2f57f6a..fe7eec9 100644 --- a/packages/visualizations/src/Axis.tsx +++ b/packages/visualizations/src/Axis.tsx @@ -23,17 +23,22 @@ const applyStyles = (axis: D3Selection) = axis.selectAll("line").style("color", theme.colors.axis.border); }; +const getTickFormatter = (scale: AxisProps["scale"], maxNumberOfTicks?: number) => { + const formatter = isScaleContinuous(scale) ? d3Format("~s") : (d: any) => d; + if (maxNumberOfTicks === undefined) { + return formatter; + } + const ticks = isScaleContinuous(scale) ? scale.ticks() : scale.domain(); + const tickInterval = Math.ceil(ticks.length / maxNumberOfTicks); + return (d: any, i: number) => (i % tickInterval === 0 ? formatter(d) : null); +}; + export const Axis: React.FC = React.memo(({ scale, transform, position, maxNumberOfTicks }) => { const defaultTransform = useAxisTransform(position!); const ref = useRef(null); useEffect(() => { if (ref.current) { - const nTicks = (isScaleContinuous(scale) ? scale.ticks() : scale.domain()).length; - const formatter = isScaleContinuous(scale) ? d3Format("~s") : (d: any) => d; - const tickFormat = - maxNumberOfTicks !== undefined && nTicks > maxNumberOfTicks - ? (d: any, i: number) => (i % maxNumberOfTicks === 0 ? formatter(d) : null) - : formatter; + const tickFormat = getTickFormatter(scale, maxNumberOfTicks); let axis: D3Selection; switch (position) { From 3b6048328cbaaee47112b3f3778b09a7ec958bd3 Mon Sep 17 00:00:00 2001 From: Imogen Fletcher Date: Thu, 19 Sep 2019 15:08:08 +0200 Subject: [PATCH 3/6] Truncate axis labels if overlapping. --- .../vizualisations/1-bar-chart.stories.tsx | 16 +++---- packages/visualizations/src/Axis.tsx | 43 +++++++++++++++---- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx b/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx index dbaaa7d..01d94d4 100644 --- a/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx +++ b/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx @@ -46,13 +46,13 @@ const rawData = { }, ], rows: [ - ["Europe", "Germany", "Berlin", "<50", "Female", 1001, 10.2], - ["Europe", "Germany", "Dresden", "<50", "Female", 2001, 20.2], - ["Europe", "Germany", "Hamburg", "<50", "Female", 3001, 30.2], - ["Europe", "UK", "London", "<50", "Female", 4001, 40.2], - ["Europe", "UK", "Edinburgh", "<50", "Female", 5001, 50.2], - ["North America", "USA", "New York", "<50", "Female", 8001, 80.2], - ["North America", "Canada", "Toronto", "<50", "Female", 8001, 80.2], + ["Europe", "Germany", "Berlin", "<50", "Female", 10000001, 10.2], + ["Europe", "Germany", "Dresden", "<50", "Female", 20000001, 20.2], + ["Europe", "Germany", "Hamburg", "<50", "Female", 30000001, 30.2], + ["Europe", "UK", "London", "<50", "Female", 40000001, 40.2], + ["Europe", "UK", "Edinburgh", "<50", "Female", 50000001, 50.2], + ["North America", "USA", "New York", "<50", "Female", 80000001, 80.2], + ["North America", "Canada", "Toronto", "<50", "Female", 80000001, 80.2], ], }; @@ -150,7 +150,7 @@ const BarChart = ({ storiesOf("@operational/visualizations/1. Bar chart", module) .add("horizontal", () => { // number of pixels picked manually to make sure that YAxis fits on the screen - const magicMargin = [5, 30, 20, 60] as ChartProps["margin"]; + const magicMargin = [5, 60, 20, 60] as ChartProps["margin"]; return ( ) = axis.selectAll("line").style("color", theme.colors.axis.border); }; -const getTickFormatter = (scale: AxisProps["scale"], maxNumberOfTicks?: number) => { - const formatter = isScaleContinuous(scale) ? d3Format("~s") : (d: any) => d; - if (maxNumberOfTicks === undefined) { - return formatter; - } +const getTickInterval = (scale: AxisProps["scale"], maxNumberOfTicks?: number) => { const ticks = isScaleContinuous(scale) ? scale.ticks() : scale.domain(); - const tickInterval = Math.ceil(ticks.length / maxNumberOfTicks); + return Math.ceil(ticks.length / (maxNumberOfTicks || ticks.length)); +}; + +const getTickFormatter = (scale: AxisProps["scale"], tickInterval: number) => { + const formatter = isScaleContinuous(scale) ? d3Format("~s") : (d: any) => d; return (d: any, i: number) => (i % tickInterval === 0 ? formatter(d) : null); }; +const getTickSpacing = (scale: AxisProps["scale"]) => { + if (isScaleBand(scale)) { + return scale.bandwidth(); + } + const range = scale.range(); + const nTicks = (scale as ScaleLinear).ticks().length; + return (range[1] - range[0]) / nTicks; +}; + +const wrap = (ctx: BaseType, maxWidth: number) => { + const textEl = select(ctx); + let text = textEl.text(); + let width = (textEl.node() as Element).getBoundingClientRect().width; + while (width > maxWidth && text.length > 0) { + text = text.slice(0, -1); + textEl.text(text + "..."); + width = (textEl.node() as Element).getBoundingClientRect().width; + } +}; + export const Axis: React.FC = React.memo(({ scale, transform, position, maxNumberOfTicks }) => { const defaultTransform = useAxisTransform(position!); const ref = useRef(null); useEffect(() => { if (ref.current) { - const tickFormat = getTickFormatter(scale, maxNumberOfTicks); + const tickInterval = getTickInterval(scale, maxNumberOfTicks); + const tickFormat = getTickFormatter(scale, tickInterval); + const tickSpacing = getTickSpacing(scale) * tickInterval; let axis: D3Selection; switch (position) { case "bottom": axis = select(ref.current).call(axisBottom(scale).tickFormat(tickFormat)); applyStyles(axis); + axis.selectAll("text").each(function() { + wrap(this, tickSpacing); + }); break; case "top": axis = select(ref.current).call(axisTop(scale).tickFormat(tickFormat)); From b97f23dffde73c8011aa71dce770d955d05b4e97 Mon Sep 17 00:00:00 2001 From: Imogen Fletcher Date: Thu, 19 Sep 2019 15:13:51 +0200 Subject: [PATCH 4/6] Add comment. --- packages/visualizations/src/Axis.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/visualizations/src/Axis.tsx b/packages/visualizations/src/Axis.tsx index f237464..7677e71 100644 --- a/packages/visualizations/src/Axis.tsx +++ b/packages/visualizations/src/Axis.tsx @@ -42,6 +42,7 @@ const getTickSpacing = (scale: AxisProps["scale"]) => { return (range[1] - range[0]) / nTicks; }; +// Truncate axis tick labels where necessary and append ellipsis to denote truncation. const wrap = (ctx: BaseType, maxWidth: number) => { const textEl = select(ctx); let text = textEl.text(); From 776219ea71bd9b43a75999daa5c3d713bb93a114 Mon Sep 17 00:00:00 2001 From: Imogen Fletcher Date: Thu, 19 Sep 2019 15:16:28 +0200 Subject: [PATCH 5/6] Apply label truncation to x2 axes. --- packages/visualizations/src/Axis.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/visualizations/src/Axis.tsx b/packages/visualizations/src/Axis.tsx index 7677e71..9badabf 100644 --- a/packages/visualizations/src/Axis.tsx +++ b/packages/visualizations/src/Axis.tsx @@ -75,6 +75,9 @@ export const Axis: React.FC = React.memo(({ scale, transform, positio case "top": axis = select(ref.current).call(axisTop(scale).tickFormat(tickFormat)); applyStyles(axis); + axis.selectAll("text").each(function() { + wrap(this, tickSpacing); + }); break; case "left": axis = select(ref.current).call(axisLeft(scale).tickFormat(tickFormat)); From b963e6a6a4b9b77963ba86905bb6cf0c185541f7 Mon Sep 17 00:00:00 2001 From: Imogen Fletcher Date: Fri, 20 Sep 2019 09:15:16 +0200 Subject: [PATCH 6/6] Add story for Axis. --- .../src/grid/1-pivot-grid.stories.tsx | 7 +- .../src/vizualisations/7-axis.stories.tsx | 106 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 packages/visualizations-stories/src/vizualisations/7-axis.stories.tsx diff --git a/packages/visualizations-stories/src/grid/1-pivot-grid.stories.tsx b/packages/visualizations-stories/src/grid/1-pivot-grid.stories.tsx index 45001a4..15a22e0 100644 --- a/packages/visualizations-stories/src/grid/1-pivot-grid.stories.tsx +++ b/packages/visualizations-stories/src/grid/1-pivot-grid.stories.tsx @@ -459,7 +459,12 @@ storiesOf("@operational/grid/1. Pivot table", module) viewBox={`0 0 ${widthWithoutPadding} ${height}`} style={{ margin: `${padding} 0` }} > - + ); }; diff --git a/packages/visualizations-stories/src/vizualisations/7-axis.stories.tsx b/packages/visualizations-stories/src/vizualisations/7-axis.stories.tsx new file mode 100644 index 0000000..fd74dd4 --- /dev/null +++ b/packages/visualizations-stories/src/vizualisations/7-axis.stories.tsx @@ -0,0 +1,106 @@ +import * as React from "react"; +import { storiesOf } from "@storybook/react"; +import { + Axis, + // AxisRules, + Chart, + AxisRules, +} from "@operational/visualizations"; +import { scaleLinear, scaleBand } from "d3-scale"; + +/** + * Example of how you can compose more complex charts out of 'atoms' + */ + +storiesOf("@operational/visualizations/7. Axis", module) + .add("linear axes", () => { + const scale = (flipped: boolean) => + scaleLinear() + .domain([0, 300]) + .range(flipped ? [300, 0] : [0, 300]); + return ( + + + + + + + + + ); + }) + .add("categorical axes", () => { + const scale = (flipped: boolean) => + scaleBand() + .domain(["A", "B", "C", "D", "E", "F", "G"]) + .range(flipped ? [300, 0] : [0, 300]); + return ( + + + + + + + + + ); + }) + .add("categorical axes, some ticks hidden", () => { + const scale = (flipped: boolean) => + scaleBand() + .domain([ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + ]) + .range(flipped ? [300, 0] : [0, 300]); + return ( + + + + + + + + + ); + }) + .add("categorical axes, truncated labels", () => { + const scale = (flipped: boolean) => + scaleBand() + .domain(["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet"]) + .range(flipped ? [300, 0] : [0, 300]); + return ( + + + + + + + + + ); + });