Skip to content

Commit b418b67

Browse files
feat: add histogram mode (#218)
1 parent 91dbcb6 commit b418b67

29 files changed

+978
-100
lines changed

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ export { timeFormatter, niceTimeFormatter, niceTimeFormatByDay } from './utils/d
1313
export { DataGenerator } from './utils/data_generators/data_generator';
1414
export { DataSeriesColorsValues } from './lib/series/series';
1515
export {
16+
AnnotationDomainType,
1617
AnnotationDomainTypes,
1718
CustomSeriesColorsMap,
19+
HistogramModeAlignment,
20+
HistogramModeAlignments,
1821
LineAnnotationDatum,
1922
LineAnnotationSpec,
2023
RectAnnotationDatum,

src/lib/axes/axis_utils.test.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { XDomain } from '../series/domains/x_domain';
22
import { YDomain } from '../series/domains/y_domain';
33
import { AxisSpec, DomainRange, Position } from '../series/specs';
44
import { LIGHT_THEME } from '../themes/light_theme';
5-
import { getAxisId, getGroupId, GroupId } from '../utils/ids';
5+
import { AxisId, getAxisId, getGroupId, GroupId } from '../utils/ids';
66
import { ScaleType } from '../utils/scales/scales';
77
import {
8+
AxisTick,
9+
AxisTicksDimensions,
810
centerRotationOrigin,
911
computeAxisGridLinePositions,
1012
computeAxisTicksDimensions,
@@ -65,8 +67,6 @@ describe('Axis computational utils', () => {
6567
left: 0,
6668
};
6769
const axis1Dims = {
68-
axisScaleType: ScaleType.Linear,
69-
axisScaleDomain: [0, 1],
7070
tickValues: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
7171
tickLabels: ['0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1'],
7272
maxLabelBboxWidth: 10,
@@ -223,7 +223,7 @@ describe('Axis computational utils', () => {
223223

224224
test('should compute available ticks', () => {
225225
const scale = getScaleForAxisSpec(verticalAxisSpec, xDomain, [yDomain], 0, 0, 100, 0);
226-
const axisPositions = getAvailableTicks(verticalAxisSpec, scale!, 0);
226+
const axisPositions = getAvailableTicks(verticalAxisSpec, scale!, 0, false);
227227
const expectedAxisPositions = [
228228
{ label: '0', position: 100, value: 0 },
229229
{ label: '0.1', position: 90, value: 0.1 },
@@ -238,6 +238,19 @@ describe('Axis computational utils', () => {
238238
{ label: '1', position: 0, value: 1 },
239239
];
240240
expect(axisPositions).toEqual(expectedAxisPositions);
241+
242+
// histogram mode axis ticks should add an additional tick
243+
const xBandDomain: XDomain = {
244+
type: 'xDomain',
245+
scaleType: ScaleType.Linear,
246+
domain: [0, 100],
247+
isBandScale: true,
248+
minInterval: 10,
249+
};
250+
const xScale = getScaleForAxisSpec(horizontalAxisSpec, xBandDomain, [yDomain], 1, 0, 100, 0);
251+
const histogramAxisPositions = getAvailableTicks(horizontalAxisSpec, xScale!, 1, true);
252+
const histogramTickLabels = histogramAxisPositions.map((tick: AxisTick) => tick.label);
253+
expect(histogramTickLabels).toEqual(['0', '10', '20', '30', '40', '50', '60', '70', '80', '90', '100', '110']);
241254
});
242255
test('should compute visible ticks for a vertical axis', () => {
243256
const allTicks = [
@@ -723,7 +736,7 @@ describe('Axis computational utils', () => {
723736
test('should compute axis ticks positions with title', () => {
724737
const chartRotation = 0;
725738
const showLegend = false;
726-
739+
727740
// validate assumptions for test
728741
expect(verticalAxisSpec.id).toEqual(verticalAxisSpecWTitle.id);
729742

@@ -743,6 +756,7 @@ describe('Axis computational utils', () => {
743756
xDomain,
744757
[yDomain],
745758
1,
759+
false,
746760
);
747761

748762
let left = 12 + 5 + 10 + 10; // font size + title padding + chart margin left + label width
@@ -763,6 +777,7 @@ describe('Axis computational utils', () => {
763777
xDomain,
764778
[yDomain],
765779
1,
780+
false,
766781
);
767782

768783
left = 0 + 10 + 10; // no title + chart margin left + label width
@@ -918,10 +933,10 @@ describe('Axis computational utils', () => {
918933
const showLegend = true;
919934
const leftLegendPosition = Position.Left;
920935

921-
const axisSpecs = new Map();
936+
const axisSpecs = new Map<AxisId, AxisSpec>();
922937
axisSpecs.set(verticalAxisSpec.id, verticalAxisSpec);
923938

924-
const axisDims = new Map();
939+
const axisDims = new Map<AxisId, AxisTicksDimensions>();
925940
axisDims.set(getAxisId('not_a_mapped_one'), axis1Dims);
926941

927942
const axisTicksPosition = getAxisTicksPositions(
@@ -934,6 +949,7 @@ describe('Axis computational utils', () => {
934949
xDomain,
935950
[yDomain],
936951
1,
952+
false,
937953
leftLegendPosition,
938954
);
939955
expect(axisTicksPosition.axisPositions.size).toBe(0);
@@ -948,10 +964,10 @@ describe('Axis computational utils', () => {
948964
const leftLegendPosition = Position.Left;
949965
const topLegendPosition = Position.Top;
950966

951-
const axisSpecs = new Map();
967+
const axisSpecs = new Map<AxisId, AxisSpec>();
952968
axisSpecs.set(verticalAxisSpec.id, verticalAxisSpec);
953969

954-
const axisDims = new Map();
970+
const axisDims = new Map<AxisId, AxisTicksDimensions>();
955971
axisDims.set(verticalAxisSpec.id, axis1Dims);
956972

957973
const axisTicksPosition = getAxisTicksPositions(
@@ -964,6 +980,7 @@ describe('Axis computational utils', () => {
964980
xDomain,
965981
[yDomain],
966982
1,
983+
false,
967984
leftLegendPosition,
968985
);
969986

@@ -995,6 +1012,7 @@ describe('Axis computational utils', () => {
9951012
xDomain,
9961013
[yDomain],
9971014
1,
1015+
false,
9981016
topLegendPosition,
9991017
);
10001018

@@ -1010,7 +1028,7 @@ describe('Axis computational utils', () => {
10101028
expect(verticalAxisWithTopLegendPosition).toEqual(expectedPositionWithTopLegend);
10111029

10121030
const ungroupedAxisSpec = { ...verticalAxisSpec, groupId: getGroupId('foo') };
1013-
const invalidSpecs = new Map();
1031+
const invalidSpecs = new Map<AxisId, AxisSpec>();
10141032
invalidSpecs.set(verticalAxisSpec.id, ungroupedAxisSpec);
10151033
const computeScalelessSpec = () => {
10161034
getAxisTicksPositions(
@@ -1023,6 +1041,7 @@ describe('Axis computational utils', () => {
10231041
xDomain,
10241042
[yDomain],
10251043
1,
1044+
false,
10261045
leftLegendPosition,
10271046
);
10281047
};
@@ -1073,7 +1092,7 @@ describe('Axis computational utils', () => {
10731092

10741093
verticalAxisSpec.domain = domainRange1;
10751094

1076-
const axesSpecs = new Map();
1095+
const axesSpecs = new Map<AxisId, AxisSpec>();
10771096
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);
10781097

10791098
// Base case
@@ -1129,7 +1148,7 @@ describe('Axis computational utils', () => {
11291148

11301149
verticalAxisSpec.domain = domainRange1;
11311150

1132-
const axesSpecs = new Map();
1151+
const axesSpecs = new Map<AxisId, AxisSpec>();
11331152
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);
11341153

11351154
const axis2 = { ...verticalAxisSpec, id: getAxisId('axis2') };
@@ -1157,7 +1176,7 @@ describe('Axis computational utils', () => {
11571176

11581177
verticalAxisSpec.domain = domainRange1;
11591178

1160-
const axesSpecs = new Map();
1179+
const axesSpecs = new Map<AxisId, AxisSpec>();
11611180
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);
11621181

11631182
const axis2 = { ...verticalAxisSpec, id: getAxisId('axis2') };
@@ -1188,7 +1207,7 @@ describe('Axis computational utils', () => {
11881207

11891208
verticalAxisSpec.domain = domainRange1;
11901209

1191-
const axesSpecs = new Map();
1210+
const axesSpecs = new Map<AxisId, AxisSpec>();
11921211
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);
11931212

11941213
const axis2 = { ...verticalAxisSpec, id: getAxisId('axis2') };
@@ -1224,7 +1243,7 @@ describe('Axis computational utils', () => {
12241243

12251244
verticalAxisSpec.domain = domainRange1;
12261245

1227-
const axesSpecs = new Map();
1246+
const axesSpecs = new Map<AxisId, AxisSpec>();
12281247
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);
12291248

12301249
const axis2 = { ...verticalAxisSpec, id: getAxisId('axis2') };
@@ -1252,7 +1271,7 @@ describe('Axis computational utils', () => {
12521271

12531272
verticalAxisSpec.domain = domainRange1;
12541273

1255-
const axesSpecs = new Map();
1274+
const axesSpecs = new Map<AxisId, AxisSpec>();
12561275
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);
12571276

12581277
const attemptToMerge = () => {

src/lib/axes/axis_utils.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ import {
1313
} from '../series/specs';
1414
import { AxisConfig, Theme } from '../themes/theme';
1515
import { Dimensions, Margins } from '../utils/dimensions';
16-
import { Domain } from '../utils/domain';
1716
import { AxisId, GroupId } from '../utils/ids';
18-
import { Scale, ScaleType } from '../utils/scales/scales';
17+
import { Scale } from '../utils/scales/scales';
1918
import { BBox, BBoxCalculator } from './bbox_calculator';
2019

2120
export type AxisLinePosition = [number, number, number, number];
@@ -27,8 +26,6 @@ export interface AxisTick {
2726
}
2827

2928
export interface AxisTicksDimensions {
30-
axisScaleType: ScaleType;
31-
axisScaleDomain: Domain;
3229
tickValues: string[] | number[];
3330
tickLabels: string[];
3431
maxLabelBboxWidth: number;
@@ -90,8 +87,6 @@ export function computeAxisTicksDimensions(
9087
);
9188

9289
return {
93-
axisScaleDomain: xDomain.domain,
94-
axisScaleType: xDomain.scaleType,
9590
...dimensions,
9691
};
9792
}
@@ -291,7 +286,7 @@ export function getTickLabelProps(
291286
}
292287

293288
return {
294-
x: tickPosition - maxLabelBboxWidth / 2,
289+
x: (tickPosition - maxLabelBboxWidth / 2),
295290
y: isAxisTop ? 0 : tickSize + tickPadding,
296291
align,
297292
verticalAlign,
@@ -393,10 +388,24 @@ export function getLeftAxisMinMaxRange(chartRotation: Rotation, height: number)
393388
}
394389
}
395390

396-
export function getAvailableTicks(axisSpec: AxisSpec, scale: Scale, totalBarsInCluster: number) {
391+
export function getAvailableTicks(
392+
axisSpec: AxisSpec,
393+
scale: Scale,
394+
totalBarsInCluster: number,
395+
enableHistogramMode: boolean,
396+
): AxisTick[] {
397397
const ticks = scale.ticks();
398+
399+
if (enableHistogramMode && scale.bandwidth > 0) {
400+
const finalTick = ticks[ticks.length - 1] + scale.minInterval;
401+
ticks.push(finalTick);
402+
}
403+
398404
const shift = totalBarsInCluster > 0 ? totalBarsInCluster : 1;
399-
const offset = (scale.bandwidth * shift) / 2;
405+
406+
const band = scale.bandwidth / (1 - scale.barsPadding);
407+
const halfPadding = (band - scale.bandwidth) / 2;
408+
const offset = enableHistogramMode ? -halfPadding : (scale.bandwidth * shift) / 2;
400409
return ticks.map((tick) => {
401410
return {
402411
value: tick,
@@ -507,6 +516,7 @@ export function getAxisTicksPositions(
507516
xDomain: XDomain,
508517
yDomain: YDomain[],
509518
totalGroupsCount: number,
519+
enableHistogramMode: boolean,
510520
legendPosition?: Position,
511521
barsPadding?: number,
512522
) {
@@ -567,7 +577,7 @@ export function getAxisTicksPositions(
567577
throw new Error(`Cannot compute scale for axis spec ${axisSpec.id}`);
568578
}
569579

570-
const allTicks = getAvailableTicks(axisSpec, scale, totalGroupsCount);
580+
const allTicks = getAvailableTicks(axisSpec, scale, totalGroupsCount, enableHistogramMode);
571581
const visibleTicks = getVisibleTicks(allTicks, axisSpec, axisDim);
572582

573583
if (axisSpec.showGridLines) {

0 commit comments

Comments
 (0)