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/1-bar-chart.stories.tsx b/packages/visualizations-stories/src/vizualisations/1-bar-chart.stories.tsx index 49901f4..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], ], }; @@ -136,7 +136,11 @@ const BarChart = ({ style={(row: RowCursor) => ({ fill: colorScale(row) })} /> ))} - + @@ -146,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 ( { + 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 ( + + + + + + + + + ); + }); diff --git a/packages/visualizations/src/Axis.tsx b/packages/visualizations/src/Axis.tsx index a170399..9badabf 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, BaseType } from "d3-selection"; import React, { useEffect, useRef } from "react"; import { useAxisTransform } from "./Chart"; -import { isScaleContinuous } from "./scale"; +import { isScaleContinuous, isScaleBand } from "./scale"; +import theme from "./theme"; export interface AxisProps { /** see https://github.com/d3/d3-scale */ @@ -16,30 +17,75 @@ 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); +}; + +const getTickInterval = (scale: AxisProps["scale"], maxNumberOfTicks?: number) => { + const ticks = isScaleContinuous(scale) ? scale.ticks() : scale.domain(); + 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; +}; + +// 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(); + 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 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 tickInterval = getTickInterval(scale, maxNumberOfTicks); + const tickFormat = getTickFormatter(scale, tickInterval); + const tickSpacing = getTickSpacing(scale) * tickInterval; + 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); + axis.selectAll("text").each(function() { + wrap(this, tickSpacing); + }); break; case "top": - select(ref.current).call(axisTop(scale).tickFormat(tickFormat)); + axis = select(ref.current).call(axisTop(scale).tickFormat(tickFormat)); + applyStyles(axis); + axis.selectAll("text").each(function() { + wrap(this, tickSpacing); + }); 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 = {