Skip to content

Commit

Permalink
feat(axis): option to hide duplicate axes (#370)
Browse files Browse the repository at this point in the history
* Option to hide axes based on tick labels, position and title.
* Refactor axes render function.

closes #368
  • Loading branch information
nickofthyme authored Sep 19, 2019
1 parent 600f8e3 commit ada2ddc
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 40 deletions.
147 changes: 145 additions & 2 deletions src/chart_types/xy_chart/store/chart_state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import {
} from '../utils/specs';
import { LIGHT_THEME } from '../../../utils/themes/light_theme';
import { mergeWithDefaultTheme } from '../../../utils/themes/theme';
import { getAnnotationId, getAxisId, getGroupId, getSpecId } from '../../../utils/ids';
import { getAnnotationId, getAxisId, getGroupId, getSpecId, AxisId } from '../../../utils/ids';
import { TooltipType, TooltipValue } from '../utils/interactions';
import { ScaleBand } from '../../../utils/scales/scale_band';
import { ScaleContinuous } from '../../../utils/scales/scale_continuous';
import { ScaleType } from '../../../utils/scales/scales';
import { ChartStore } from './chart_state';
import { ChartStore, isDuplicateAxis } from './chart_state';
import { AxisTicksDimensions } from '../utils/axis_utils';

describe('Chart Store', () => {
let store = new ChartStore();
Expand Down Expand Up @@ -71,6 +72,148 @@ describe('Chart Store', () => {
store.computeChart();
});

describe('isDuplicateAxis', () => {
const AXIS_1_ID = getAxisId('spec_1');
const AXIS_2_ID = getAxisId('spec_1');
const axis1: AxisSpec = {
id: AXIS_1_ID,
groupId: getGroupId('group_1'),
hide: false,
showOverlappingTicks: false,
showOverlappingLabels: false,
position: Position.Left,
tickSize: 30,
tickPadding: 10,
tickFormat: (value: any) => `${value}%`,
};
const axis2: AxisSpec = {
...axis1,
id: AXIS_2_ID,
groupId: getGroupId('group_2'),
};
const axisTicksDimensions: AxisTicksDimensions = {
tickValues: [],
tickLabels: ['10', '20', '30'],
maxLabelBboxWidth: 1,
maxLabelBboxHeight: 1,
maxLabelTextWidth: 1,
maxLabelTextHeight: 1,
};
let tickMap: Map<AxisId, AxisTicksDimensions>;
let specMap: Map<AxisId, AxisSpec>;

beforeEach(() => {
tickMap = new Map<AxisId, AxisTicksDimensions>();
specMap = new Map<AxisId, AxisSpec>();
});

it('should return true if axisSpecs and ticks match', () => {
tickMap.set(AXIS_2_ID, axisTicksDimensions);
specMap.set(AXIS_2_ID, axis2);
const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap);

expect(result).toBe(true);
});

it('should return false if axisSpecs, ticks AND title match', () => {
tickMap.set(AXIS_2_ID, axisTicksDimensions);
specMap.set(AXIS_2_ID, {
...axis2,
title: 'TESTING',
});
const result = isDuplicateAxis(
{
...axis1,
title: 'TESTING',
},
axisTicksDimensions,
tickMap,
specMap,
);

expect(result).toBe(true);
});

it('should return true with single tick', () => {
const newAxisTicksDimensions = {
...axisTicksDimensions,
tickLabels: ['10'],
};
tickMap.set(AXIS_2_ID, newAxisTicksDimensions);
specMap.set(AXIS_2_ID, axis2);

const result = isDuplicateAxis(axis1, newAxisTicksDimensions, tickMap, specMap);

expect(result).toBe(true);
});

it('should return false if axisSpecs and ticks match but title is different', () => {
tickMap.set(AXIS_2_ID, axisTicksDimensions);
specMap.set(AXIS_2_ID, {
...axis2,
title: 'TESTING',
});
const result = isDuplicateAxis(
{
...axis1,
title: 'NOT TESTING',
},
axisTicksDimensions,
tickMap,
specMap,
);

expect(result).toBe(false);
});

it('should return false if axisSpecs and ticks match but position is different', () => {
tickMap.set(AXIS_2_ID, axisTicksDimensions);
specMap.set(AXIS_2_ID, axis2);
const result = isDuplicateAxis(
{
...axis1,
position: Position.Top,
},
axisTicksDimensions,
tickMap,
specMap,
);

expect(result).toBe(false);
});

it('should return false if tickFormat is different', () => {
tickMap.set(AXIS_2_ID, {
...axisTicksDimensions,
tickLabels: ['10%', '20%', '30%'],
});
specMap.set(AXIS_2_ID, axis2);

const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap);

expect(result).toBe(false);
});

it('should return false if tick label count is different', () => {
tickMap.set(AXIS_2_ID, {
...axisTicksDimensions,
tickLabels: ['10', '20', '25', '30'],
});
specMap.set(AXIS_2_ID, axis2);

const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap);

expect(result).toBe(false);
});

it("should return false if can't find spec", () => {
tickMap.set(AXIS_2_ID, axisTicksDimensions);
const result = isDuplicateAxis(axis1, axisTicksDimensions, tickMap, specMap);

expect(result).toBe(false);
});
});

test('can add a single spec', () => {
store.addSeriesSpec(spec);
store.updateParentDimensions(600, 600, 0, 0);
Expand Down
36 changes: 35 additions & 1 deletion src/chart_types/xy_chart/store/chart_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,35 @@ export type CursorUpdateListener = (event?: CursorEvent) => void;
export type RenderChangeListener = (isRendered: boolean) => void;
export type BasicListener = () => undefined | void;

export const isDuplicateAxis = (
{ position, title }: AxisSpec,
{ tickLabels }: AxisTicksDimensions,
tickMap: Map<AxisId, AxisTicksDimensions>,
specMap: Map<AxisId, AxisSpec>,
): boolean => {
const firstTickLabel = tickLabels[0];
const lastTickLabel = tickLabels.slice(-1)[0];

let hasDuplicate = false;
tickMap.forEach(({ tickLabels: axisTickLabels }, axisId) => {
if (
!hasDuplicate &&
axisTickLabels &&
tickLabels.length === axisTickLabels.length &&
firstTickLabel === axisTickLabels[0] &&
lastTickLabel === axisTickLabels.slice(-1)[0]
) {
const spec = specMap.get(axisId);

if (spec && spec.position === position && title === spec.title) {
hasDuplicate = true;
}
}
});

return hasDuplicate;
};

export class ChartStore {
constructor(id?: string) {
this.id = id || uuid.v4();
Expand Down Expand Up @@ -155,6 +184,7 @@ export class ChartStore {
chartRotation: Rotation = 0; // updated from jsx
chartRendering: Rendering = 'canvas'; // updated from jsx
chartTheme: Theme = LIGHT_THEME;
hideDuplicateAxes: boolean = false; // updated from jsx
axesSpecs: Map<AxisId, AxisSpec> = new Map(); // readed from jsx
axesTicksDimensions: Map<AxisId, AxisTicksDimensions> = new Map(); // computed
axesPositions: Map<AxisId, Dimensions> = new Map(); // computed
Expand Down Expand Up @@ -926,7 +956,11 @@ export class ChartStore {
barsPadding,
this.enableHistogramMode.get(),
);
if (dimensions) {

if (
dimensions &&
(!this.hideDuplicateAxes || !isDuplicateAxis(axisSpec, dimensions, this.axesTicksDimensions, this.axesSpecs))
) {
this.axesTicksDimensions.set(id, dimensions);
}
});
Expand Down
74 changes: 39 additions & 35 deletions src/components/react_canvas/reactive_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { inject, observer } from 'mobx-react';
import { ContainerConfig } from 'konva';
import { Layer, Rect, Stage } from 'react-konva';

import { isLineAnnotation, isRectAnnotation } from '../../chart_types/xy_chart/utils/specs';
import { LineAnnotationStyle, RectAnnotationStyle, mergeGridLineConfigs } from '../../utils/themes/theme';
import { AnnotationId } from '../../utils/ids';
import { isLineAnnotation, isRectAnnotation, AxisSpec } from '../../chart_types/xy_chart/utils/specs';
import { LineAnnotationStyle, RectAnnotationStyle, mergeGridLineConfigs } from '../../utils/themes/theme';
import {
AnnotationDimensions,
AnnotationLineProps,
Expand All @@ -21,7 +21,8 @@ import { Grid } from './grid';
import { LineAnnotation } from './line_annotation';
import { LineGeometries } from './line_geometries';
import { RectAnnotation } from './rect_annotation';
import { isVerticalGrid } from '../../chart_types/xy_chart/utils/axis_utils';
import { AxisTick, AxisTicksDimensions, isVerticalGrid } from '../../chart_types/xy_chart/utils/axis_utils';
import { Dimensions } from '../../utils/dimensions';

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

interface AxisProps {
key: string;
axisSpec: AxisSpec;
axisTicksDimensions: AxisTicksDimensions;
axisPosition: Dimensions;
ticks: AxisTick[];
}

interface ReactiveChartElementIndex {
element: JSX.Element;
zIndex: number;
Expand Down Expand Up @@ -157,40 +166,35 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
},
];
};
renderAxes = () => {
const {
axesVisibleTicks,
axesSpecs,
axesTicksDimensions,
axesPositions,
chartTheme,
debug,
chartDimensions,
} = this.props.chartStore!;

const axesComponents: JSX.Element[] = [];
axesVisibleTicks.forEach((axisTicks, axisId) => {
const axisSpec = axesSpecs.get(axisId);
const axisTicksDimensions = axesTicksDimensions.get(axisId);
const axisPosition = axesPositions.get(axisId);
const ticks = axesVisibleTicks.get(axisId);
if (!ticks || !axisSpec || !axisTicksDimensions || !axisPosition) {
return;
}
axesComponents.push(
<Axis
key={`axis-${axisId}`}
axisSpec={axisSpec}
axisTicksDimensions={axisTicksDimensions}
axisPosition={axisPosition}
ticks={ticks}
chartTheme={chartTheme}
debug={debug}
chartDimensions={chartDimensions}
/>,
getAxes = (): AxisProps[] => {
const { axesVisibleTicks, axesSpecs, axesTicksDimensions, axesPositions } = this.props.chartStore!;
const ids = [...axesVisibleTicks.keys()];

return ids
.map((id) => ({
key: `axis-${id}`,
ticks: axesVisibleTicks.get(id),
axisSpec: axesSpecs.get(id),
axisTicksDimensions: axesTicksDimensions.get(id),
axisPosition: axesPositions.get(id),
}))
.filter(
(config: Partial<AxisProps>): config is AxisProps => {
const { ticks, axisSpec, axisTicksDimensions, axisPosition } = config;

return Boolean(ticks && axisSpec && axisTicksDimensions && axisPosition);
},
);
});
return axesComponents;
};

renderAxes = (): JSX.Element[] => {
const { chartTheme, debug, chartDimensions } = this.props.chartStore!;
const axes = this.getAxes();

return axes.map(({ key, ...axisProps }) => (
<Axis {...axisProps} key={key} chartTheme={chartTheme} debug={debug} chartDimensions={chartDimensions} />
));
};

renderGrids = () => {
Expand Down
2 changes: 2 additions & 0 deletions src/specs/settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ describe('Settings spec component', () => {
snap: false,
},
legendPosition: Position.Bottom,
hideDuplicateAxes: false,
showLegendDisplayValue: false,
debug: true,
xDomain: { min: 0, max: 10 },
Expand Down Expand Up @@ -183,6 +184,7 @@ describe('Settings spec component', () => {
},
legendPosition: Position.Bottom,
showLegendDisplayValue: false,
hideDuplicateAxes: false,
debug: true,
xDomain: { min: 0, max: 10 },
};
Expand Down
9 changes: 9 additions & 0 deletions src/specs/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export interface SettingSpecProps {
debug: boolean;
legendPosition?: Position;
showLegendDisplayValue: boolean;
/**
* Removes duplicate axes
*
* Compares title, position and first & last tick labels
*/
hideDuplicateAxes: boolean;
onElementClick?: ElementClickListener;
onElementOver?: ElementOverListener;
onElementOut?: () => undefined | void;
Expand Down Expand Up @@ -130,6 +136,7 @@ function updateChartStore(props: SettingSpecProps) {
debug,
xDomain,
resizeDebounce,
hideDuplicateAxes,
} = props;

if (!chartStore) {
Expand All @@ -142,6 +149,7 @@ function updateChartStore(props: SettingSpecProps) {
chartStore.animateData = animateData;
chartStore.debug = debug;
chartStore.resizeDebounce = resizeDebounce!;
chartStore.hideDuplicateAxes = hideDuplicateAxes;

if (tooltip && isTooltipProps(tooltip)) {
const { type, snap, headerFormatter } = tooltip;
Expand Down Expand Up @@ -203,6 +211,7 @@ export class SettingsComponent extends PureComponent<SettingSpecProps> {
showLegend: false,
resizeDebounce: 10,
debug: false,
hideDuplicateAxes: false,
tooltip: {
type: DEFAULT_TOOLTIP_TYPE,
snap: DEFAULT_TOOLTIP_SNAP,
Expand Down
Loading

0 comments on commit ada2ddc

Please sign in to comment.