Skip to content
This repository was archived by the owner on Jun 6, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,12 @@ storiesOf("@operational/grid/1. Pivot table", module)
viewBox={`0 0 ${widthWithoutPadding} ${height}`}
style={{ margin: `${padding} 0` }}
>
<Axis scale={xScale} transform={`translate(${padding}, ${35 - padding - 1})`} position="top" />
<Axis
scale={xScale}
transform={`translate(${padding}, ${35 - padding - 1})`}
position="top"
maxNumberOfTicks={5}
/>
</svg>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
],
};

Expand Down Expand Up @@ -136,7 +136,11 @@ const BarChart = <Name extends string>({
style={(row: RowCursor) => ({ fill: colorScale(row) })}
/>
))}
<Axis scale={categoricalScale} position={!isVertical ? "left" : "bottom"} />
<Axis
scale={categoricalScale}
position={!isVertical ? "left" : "bottom"}
maxNumberOfTicks={isVertical ? 4 : undefined}
/>
<Axis scale={metricScale} position={!isVertical ? "bottom" : "left"} />
</Chart>
</div>
Expand All @@ -146,7 +150,7 @@ const BarChart = <Name extends string>({
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 (
<BarChart
Expand Down
106 changes: 106 additions & 0 deletions packages/visualizations-stories/src/vizualisations/7-axis.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Chart width={300} height={300} margin={60}>
<Axis scale={scale(true)} position="left" />
<Axis scale={scale(true)} position="right" />
<Axis scale={scale(false)} position="top" />
<Axis scale={scale(false)} position="bottom" />
<AxisRules scale={scale(true)} position="left" length={300} />
<AxisRules scale={scale(false)} position="top" length={300} />
</Chart>
);
})
.add("categorical axes", () => {
const scale = (flipped: boolean) =>
scaleBand()
.domain(["A", "B", "C", "D", "E", "F", "G"])
.range(flipped ? [300, 0] : [0, 300]);
return (
<Chart width={300} height={300} margin={60}>
<Axis scale={scale(true)} position="left" />
<Axis scale={scale(true)} position="right" />
<Axis scale={scale(false)} position="top" />
<Axis scale={scale(false)} position="bottom" />
<AxisRules scale={scale(true)} position="left" length={300} />
<AxisRules scale={scale(false)} position="top" length={300} />
</Chart>
);
})
.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 (
<Chart width={300} height={300} margin={60}>
<Axis scale={scale(true)} position="left" maxNumberOfTicks={5} />
<Axis scale={scale(true)} position="right" maxNumberOfTicks={8} />
<Axis scale={scale(false)} position="top" maxNumberOfTicks={10} />
<Axis scale={scale(false)} position="bottom" maxNumberOfTicks={15} />
<AxisRules scale={scale(true)} position="left" length={300} />
<AxisRules scale={scale(false)} position="top" length={300} />
</Chart>
);
})
.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 (
<Chart width={300} height={300} margin={60}>
<Axis scale={scale(true)} position="left" maxNumberOfTicks={20} />
<Axis scale={scale(true)} position="right" maxNumberOfTicks={20} />
<Axis scale={scale(false)} position="top" maxNumberOfTicks={20} />
<Axis scale={scale(false)} position="bottom" maxNumberOfTicks={20} />
<AxisRules scale={scale(true)} position="left" length={300} />
<AxisRules scale={scale(false)} position="top" length={300} />
</Chart>
);
});
70 changes: 58 additions & 12 deletions packages/visualizations/src/Axis.tsx
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -16,30 +17,75 @@ export interface AxisProps {
maxNumberOfTicks?: number;
}

const applyStyles = (axis: D3Selection<SVGGElement, unknown, null, undefined>) => {
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<number, number>).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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calculate size through getBoundingClientRect is expensive operation. We can use "binary search" here

textEl.text(text + "...");
width = (textEl.node() as Element).getBoundingClientRect().width;
}
};

export const Axis: React.FC<AxisProps> = React.memo(({ scale, transform, position, maxNumberOfTicks }) => {
const defaultTransform = useAxisTransform(position!);
const ref = useRef<SVGGElement>(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<SVGGElement, unknown, null, undefined>;
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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/visualizations/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const palettes = {
const axisColors = {
border: "#adadad",
rules: "#e8e8e8",
label: "#999999",
label: "#545454",
};

const focusColors = {
Expand Down