Skip to content

Commit ada2ddc

Browse files
authored
feat(axis): option to hide duplicate axes (#370)
* Option to hide axes based on tick labels, position and title. * Refactor axes render function. closes #368
1 parent 600f8e3 commit ada2ddc

File tree

6 files changed

+266
-40
lines changed

6 files changed

+266
-40
lines changed

src/chart_types/xy_chart/store/chart_state.test.ts

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import {
1111
} from '../utils/specs';
1212
import { LIGHT_THEME } from '../../../utils/themes/light_theme';
1313
import { mergeWithDefaultTheme } from '../../../utils/themes/theme';
14-
import { getAnnotationId, getAxisId, getGroupId, getSpecId } from '../../../utils/ids';
14+
import { getAnnotationId, getAxisId, getGroupId, getSpecId, AxisId } from '../../../utils/ids';
1515
import { TooltipType, TooltipValue } from '../utils/interactions';
1616
import { ScaleBand } from '../../../utils/scales/scale_band';
1717
import { ScaleContinuous } from '../../../utils/scales/scale_continuous';
1818
import { ScaleType } from '../../../utils/scales/scales';
19-
import { ChartStore } from './chart_state';
19+
import { ChartStore, isDuplicateAxis } from './chart_state';
20+
import { AxisTicksDimensions } from '../utils/axis_utils';
2021

2122
describe('Chart Store', () => {
2223
let store = new ChartStore();
@@ -71,6 +72,148 @@ describe('Chart Store', () => {
7172
store.computeChart();
7273
});
7374

75+
describe('isDuplicateAxis', () => {
76+
const AXIS_1_ID = getAxisId('spec_1');
77+
const AXIS_2_ID = getAxisId('spec_1');
78+
const axis1: AxisSpec = {
79+
id: AXIS_1_ID,
80+
groupId: getGroupId('group_1'),
81+
hide: false,
82+
showOverlappingTicks: false,
83+
showOverlappingLabels: false,
84+
position: Position.Left,
85+
tickSize: 30,
86+
tickPadding: 10,
87+
tickFormat: (value: any) => `${value}%`,
88+
};
89+
const axis2: AxisSpec = {
90+
...axis1,
91+
id: AXIS_2_ID,
92+
groupId: getGroupId('group_2'),
93+
};
94+
const axisTicksDimensions: AxisTicksDimensions = {
95+
tickValues: [],
96+
tickLabels: ['10', '20', '30'],
97+
maxLabelBboxWidth: 1,
98+
maxLabelBboxHeight: 1,
99+
maxLabelTextWidth: 1,
100+
maxLabelTextHeight: 1,
101+
};
102+
let tickMap: Map<AxisId, AxisTicksDimensions>;
103+
let specMap: Map<AxisId, AxisSpec>;
104+
105+
beforeEach(() => {
106+
tickMap = new Map<AxisId, AxisTicksDimensions>();
107+
specMap = new Map<AxisId, AxisSpec>();
108+
});
109+
110+
it('should return true if axisSpecs and ticks match', () => {
111+
tickMap.set(AXIS_2_ID, axisTicksDimensions);
112+
specMap.set(AXIS_2_ID, axis2);
113+
const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap);
114+
115+
expect(result).toBe(true);
116+
});
117+
118+
it('should return false if axisSpecs, ticks AND title match', () => {
119+
tickMap.set(AXIS_2_ID, axisTicksDimensions);
120+
specMap.set(AXIS_2_ID, {
121+
...axis2,
122+
title: 'TESTING',
123+
});
124+
const result = isDuplicateAxis(
125+
{
126+
...axis1,
127+
title: 'TESTING',
128+
},
129+
axisTicksDimensions,
130+
tickMap,
131+
specMap,
132+
);
133+
134+
expect(result).toBe(true);
135+
});
136+
137+
it('should return true with single tick', () => {
138+
const newAxisTicksDimensions = {
139+
...axisTicksDimensions,
140+
tickLabels: ['10'],
141+
};
142+
tickMap.set(AXIS_2_ID, newAxisTicksDimensions);
143+
specMap.set(AXIS_2_ID, axis2);
144+
145+
const result = isDuplicateAxis(axis1, newAxisTicksDimensions, tickMap, specMap);
146+
147+
expect(result).toBe(true);
148+
});
149+
150+
it('should return false if axisSpecs and ticks match but title is different', () => {
151+
tickMap.set(AXIS_2_ID, axisTicksDimensions);
152+
specMap.set(AXIS_2_ID, {
153+
...axis2,
154+
title: 'TESTING',
155+
});
156+
const result = isDuplicateAxis(
157+
{
158+
...axis1,
159+
title: 'NOT TESTING',
160+
},
161+
axisTicksDimensions,
162+
tickMap,
163+
specMap,
164+
);
165+
166+
expect(result).toBe(false);
167+
});
168+
169+
it('should return false if axisSpecs and ticks match but position is different', () => {
170+
tickMap.set(AXIS_2_ID, axisTicksDimensions);
171+
specMap.set(AXIS_2_ID, axis2);
172+
const result = isDuplicateAxis(
173+
{
174+
...axis1,
175+
position: Position.Top,
176+
},
177+
axisTicksDimensions,
178+
tickMap,
179+
specMap,
180+
);
181+
182+
expect(result).toBe(false);
183+
});
184+
185+
it('should return false if tickFormat is different', () => {
186+
tickMap.set(AXIS_2_ID, {
187+
...axisTicksDimensions,
188+
tickLabels: ['10%', '20%', '30%'],
189+
});
190+
specMap.set(AXIS_2_ID, axis2);
191+
192+
const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap);
193+
194+
expect(result).toBe(false);
195+
});
196+
197+
it('should return false if tick label count is different', () => {
198+
tickMap.set(AXIS_2_ID, {
199+
...axisTicksDimensions,
200+
tickLabels: ['10', '20', '25', '30'],
201+
});
202+
specMap.set(AXIS_2_ID, axis2);
203+
204+
const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap);
205+
206+
expect(result).toBe(false);
207+
});
208+
209+
it("should return false if can't find spec", () => {
210+
tickMap.set(AXIS_2_ID, axisTicksDimensions);
211+
const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap);
212+
213+
expect(result).toBe(false);
214+
});
215+
});
216+
74217
test('can add a single spec', () => {
75218
store.addSeriesSpec(spec);
76219
store.updateParentDimensions(600, 600, 0, 0);

src/chart_types/xy_chart/store/chart_state.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,35 @@ export type CursorUpdateListener = (event?: CursorEvent) => void;
114114
export type RenderChangeListener = (isRendered: boolean) => void;
115115
export type BasicListener = () => undefined | void;
116116

117+
export const isDuplicateAxis = (
118+
{ position, title }: AxisSpec,
119+
{ tickLabels }: AxisTicksDimensions,
120+
tickMap: Map<AxisId, AxisTicksDimensions>,
121+
specMap: Map<AxisId, AxisSpec>,
122+
): boolean => {
123+
const firstTickLabel = tickLabels[0];
124+
const lastTickLabel = tickLabels.slice(-1)[0];
125+
126+
let hasDuplicate = false;
127+
tickMap.forEach(({ tickLabels: axisTickLabels }, axisId) => {
128+
if (
129+
!hasDuplicate &&
130+
axisTickLabels &&
131+
tickLabels.length === axisTickLabels.length &&
132+
firstTickLabel === axisTickLabels[0] &&
133+
lastTickLabel === axisTickLabels.slice(-1)[0]
134+
) {
135+
const spec = specMap.get(axisId);
136+
137+
if (spec && spec.position === position && title === spec.title) {
138+
hasDuplicate = true;
139+
}
140+
}
141+
});
142+
143+
return hasDuplicate;
144+
};
145+
117146
export class ChartStore {
118147
constructor(id?: string) {
119148
this.id = id || uuid.v4();
@@ -155,6 +184,7 @@ export class ChartStore {
155184
chartRotation: Rotation = 0; // updated from jsx
156185
chartRendering: Rendering = 'canvas'; // updated from jsx
157186
chartTheme: Theme = LIGHT_THEME;
187+
hideDuplicateAxes: boolean = false; // updated from jsx
158188
axesSpecs: Map<AxisId, AxisSpec> = new Map(); // readed from jsx
159189
axesTicksDimensions: Map<AxisId, AxisTicksDimensions> = new Map(); // computed
160190
axesPositions: Map<AxisId, Dimensions> = new Map(); // computed
@@ -926,7 +956,11 @@ export class ChartStore {
926956
barsPadding,
927957
this.enableHistogramMode.get(),
928958
);
929-
if (dimensions) {
959+
960+
if (
961+
dimensions &&
962+
(!this.hideDuplicateAxes || !isDuplicateAxis(axisSpec, dimensions, this.axesTicksDimensions, this.axesSpecs))
963+
) {
930964
this.axesTicksDimensions.set(id, dimensions);
931965
}
932966
});

src/components/react_canvas/reactive_chart.tsx

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { inject, observer } from 'mobx-react';
33
import { ContainerConfig } from 'konva';
44
import { Layer, Rect, Stage } from 'react-konva';
55

6-
import { isLineAnnotation, isRectAnnotation } from '../../chart_types/xy_chart/utils/specs';
7-
import { LineAnnotationStyle, RectAnnotationStyle, mergeGridLineConfigs } from '../../utils/themes/theme';
86
import { AnnotationId } from '../../utils/ids';
7+
import { isLineAnnotation, isRectAnnotation, AxisSpec } from '../../chart_types/xy_chart/utils/specs';
8+
import { LineAnnotationStyle, RectAnnotationStyle, mergeGridLineConfigs } from '../../utils/themes/theme';
99
import {
1010
AnnotationDimensions,
1111
AnnotationLineProps,
@@ -21,7 +21,8 @@ import { Grid } from './grid';
2121
import { LineAnnotation } from './line_annotation';
2222
import { LineGeometries } from './line_geometries';
2323
import { RectAnnotation } from './rect_annotation';
24-
import { isVerticalGrid } from '../../chart_types/xy_chart/utils/axis_utils';
24+
import { AxisTick, AxisTicksDimensions, isVerticalGrid } from '../../chart_types/xy_chart/utils/axis_utils';
25+
import { Dimensions } from '../../utils/dimensions';
2526

2627
interface ReactiveChartProps {
2728
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
@@ -36,6 +37,14 @@ interface ReactiveChartState {
3637
};
3738
}
3839

40+
interface AxisProps {
41+
key: string;
42+
axisSpec: AxisSpec;
43+
axisTicksDimensions: AxisTicksDimensions;
44+
axisPosition: Dimensions;
45+
ticks: AxisTick[];
46+
}
47+
3948
interface ReactiveChartElementIndex {
4049
element: JSX.Element;
4150
zIndex: number;
@@ -157,40 +166,35 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
157166
},
158167
];
159168
};
160-
renderAxes = () => {
161-
const {
162-
axesVisibleTicks,
163-
axesSpecs,
164-
axesTicksDimensions,
165-
axesPositions,
166-
chartTheme,
167-
debug,
168-
chartDimensions,
169-
} = this.props.chartStore!;
170169

171-
const axesComponents: JSX.Element[] = [];
172-
axesVisibleTicks.forEach((axisTicks, axisId) => {
173-
const axisSpec = axesSpecs.get(axisId);
174-
const axisTicksDimensions = axesTicksDimensions.get(axisId);
175-
const axisPosition = axesPositions.get(axisId);
176-
const ticks = axesVisibleTicks.get(axisId);
177-
if (!ticks || !axisSpec || !axisTicksDimensions || !axisPosition) {
178-
return;
179-
}
180-
axesComponents.push(
181-
<Axis
182-
key={`axis-${axisId}`}
183-
axisSpec={axisSpec}
184-
axisTicksDimensions={axisTicksDimensions}
185-
axisPosition={axisPosition}
186-
ticks={ticks}
187-
chartTheme={chartTheme}
188-
debug={debug}
189-
chartDimensions={chartDimensions}
190-
/>,
170+
getAxes = (): AxisProps[] => {
171+
const { axesVisibleTicks, axesSpecs, axesTicksDimensions, axesPositions } = this.props.chartStore!;
172+
const ids = [...axesVisibleTicks.keys()];
173+
174+
return ids
175+
.map((id) => ({
176+
key: `axis-${id}`,
177+
ticks: axesVisibleTicks.get(id),
178+
axisSpec: axesSpecs.get(id),
179+
axisTicksDimensions: axesTicksDimensions.get(id),
180+
axisPosition: axesPositions.get(id),
181+
}))
182+
.filter(
183+
(config: Partial<AxisProps>): config is AxisProps => {
184+
const { ticks, axisSpec, axisTicksDimensions, axisPosition } = config;
185+
186+
return Boolean(ticks && axisSpec && axisTicksDimensions && axisPosition);
187+
},
191188
);
192-
});
193-
return axesComponents;
189+
};
190+
191+
renderAxes = (): JSX.Element[] => {
192+
const { chartTheme, debug, chartDimensions } = this.props.chartStore!;
193+
const axes = this.getAxes();
194+
195+
return axes.map(({ key, ...axisProps }) => (
196+
<Axis {...axisProps} key={key} chartTheme={chartTheme} debug={debug} chartDimensions={chartDimensions} />
197+
));
194198
};
195199

196200
renderGrids = () => {

src/specs/settings.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ describe('Settings spec component', () => {
7979
snap: false,
8080
},
8181
legendPosition: Position.Bottom,
82+
hideDuplicateAxes: false,
8283
showLegendDisplayValue: false,
8384
debug: true,
8485
xDomain: { min: 0, max: 10 },
@@ -183,6 +184,7 @@ describe('Settings spec component', () => {
183184
},
184185
legendPosition: Position.Bottom,
185186
showLegendDisplayValue: false,
187+
hideDuplicateAxes: false,
186188
debug: true,
187189
xDomain: { min: 0, max: 10 },
188190
};

src/specs/settings.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ export interface SettingSpecProps {
7878
debug: boolean;
7979
legendPosition?: Position;
8080
showLegendDisplayValue: boolean;
81+
/**
82+
* Removes duplicate axes
83+
*
84+
* Compares title, position and first & last tick labels
85+
*/
86+
hideDuplicateAxes: boolean;
8187
onElementClick?: ElementClickListener;
8288
onElementOver?: ElementOverListener;
8389
onElementOut?: () => undefined | void;
@@ -130,6 +136,7 @@ function updateChartStore(props: SettingSpecProps) {
130136
debug,
131137
xDomain,
132138
resizeDebounce,
139+
hideDuplicateAxes,
133140
} = props;
134141

135142
if (!chartStore) {
@@ -142,6 +149,7 @@ function updateChartStore(props: SettingSpecProps) {
142149
chartStore.animateData = animateData;
143150
chartStore.debug = debug;
144151
chartStore.resizeDebounce = resizeDebounce!;
152+
chartStore.hideDuplicateAxes = hideDuplicateAxes;
145153

146154
if (tooltip && isTooltipProps(tooltip)) {
147155
const { type, snap, headerFormatter } = tooltip;
@@ -203,6 +211,7 @@ export class SettingsComponent extends PureComponent<SettingSpecProps> {
203211
showLegend: false,
204212
resizeDebounce: 10,
205213
debug: false,
214+
hideDuplicateAxes: false,
206215
tooltip: {
207216
type: DEFAULT_TOOLTIP_TYPE,
208217
snap: DEFAULT_TOOLTIP_SNAP,

0 commit comments

Comments
 (0)