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 = {