Skip to content

Commit 3f1358e

Browse files
fix: adjust domain & range for single value histogram (#265)
1 parent ebe9d3f commit 3f1358e

File tree

6 files changed

+149
-9
lines changed

6 files changed

+149
-9
lines changed

src/lib/axes/axis_utils.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,23 @@ export function computeAxisTicksDimensions(
6161
chartRotation: Rotation,
6262
axisConfig: AxisConfig,
6363
barsPadding?: number,
64+
enableHistogramMode?: boolean,
6465
): AxisTicksDimensions | null {
6566
if (axisSpec.hide) {
6667
return null;
6768
}
6869

69-
const scale = getScaleForAxisSpec(axisSpec, xDomain, yDomain, totalBarsInCluster, chartRotation, 0, 1, barsPadding);
70+
const scale = getScaleForAxisSpec(
71+
axisSpec,
72+
xDomain,
73+
yDomain,
74+
totalBarsInCluster,
75+
chartRotation,
76+
0,
77+
1,
78+
barsPadding,
79+
enableHistogramMode,
80+
);
7081
if (!scale) {
7182
throw new Error(`Cannot compute scale for axis spec ${axisSpec.id}`);
7283
}
@@ -112,6 +123,7 @@ export function getScaleForAxisSpec(
112123
minRange: number,
113124
maxRange: number,
114125
barsPadding?: number,
126+
enableHistogramMode?: boolean,
115127
): Scale | null {
116128
const axisIsYDomain = isYDomain(axisSpec.position, chartRotation);
117129

@@ -122,7 +134,7 @@ export function getScaleForAxisSpec(
122134
}
123135
return null;
124136
} else {
125-
return computeXScale(xDomain, totalBarsInCluster, minRange, maxRange, barsPadding);
137+
return computeXScale(xDomain, totalBarsInCluster, minRange, maxRange, barsPadding, enableHistogramMode);
126138
}
127139
}
128140

@@ -580,6 +592,7 @@ export function getAxisTicksPositions(
580592
minMaxRanges.minRange,
581593
minMaxRanges.maxRange,
582594
barsPadding,
595+
enableHistogramMode,
583596
);
584597

585598
if (!scale) {

src/lib/series/scales.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,44 @@ describe('Series scales', () => {
4141
expect(scale.scale(3)).toBe(expectedBandwidth * 0);
4242
});
4343

44+
describe('computeXScale with single value domain', () => {
45+
const maxRange = 120;
46+
const singleDomainValue = 3;
47+
const minInterval = 1;
48+
49+
test('should return extended domain & range when in histogram mode', () => {
50+
const xDomainSingleValue: XDomain = {
51+
type: 'xDomain',
52+
isBandScale: true,
53+
domain: [singleDomainValue, singleDomainValue],
54+
minInterval: minInterval,
55+
scaleType: ScaleType.Linear,
56+
};
57+
const enableHistogramMode = true;
58+
59+
const scale = computeXScale(xDomainSingleValue, 1, 0, maxRange, 0, enableHistogramMode);
60+
expect(scale.bandwidth).toBe(maxRange);
61+
expect(scale.domain).toEqual([singleDomainValue, singleDomainValue + minInterval]);
62+
expect(scale.range).toEqual([0, maxRange]);
63+
});
64+
65+
test('should return unextended domain & range when not in histogram mode', () => {
66+
const xDomainSingleValue: XDomain = {
67+
type: 'xDomain',
68+
isBandScale: true,
69+
domain: [singleDomainValue, singleDomainValue],
70+
minInterval: minInterval,
71+
scaleType: ScaleType.Linear,
72+
};
73+
const enableHistogramMode = false;
74+
75+
const scale = computeXScale(xDomainSingleValue, 1, 0, maxRange, 0, enableHistogramMode);
76+
expect(scale.bandwidth).toBe(maxRange);
77+
expect(scale.domain).toEqual([singleDomainValue, singleDomainValue]);
78+
expect(scale.range).toEqual([0, 0]);
79+
});
80+
});
81+
4482
test('should compute X Scale ordinal', () => {
4583
const nonZeroGroupScale = computeXScale(xDomainOrdinal, 1, 120, 0);
4684
const expectedBandwidth = 60;

src/lib/series/scales.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ export function countBarsInCluster(
3838
};
3939
}
4040

41+
function getBandScaleRange(
42+
isInverse: boolean,
43+
isSingleValueHistogram: boolean,
44+
minRange: number,
45+
maxRange: number,
46+
bandwidth: number,
47+
): {
48+
start: number;
49+
end: number;
50+
} {
51+
const rangeEndOffset = isSingleValueHistogram ? 0 : bandwidth;
52+
const start = isInverse ? minRange - rangeEndOffset : minRange;
53+
const end = isInverse ? maxRange : maxRange - rangeEndOffset;
54+
55+
return { start, end };
56+
}
57+
4158
/**
4259
* Compute the x scale used to align geometries to the x axis.
4360
* @param xDomain the x domain
@@ -50,6 +67,7 @@ export function computeXScale(
5067
minRange: number,
5168
maxRange: number,
5269
barsPadding?: number,
70+
enableHistogramMode?: boolean,
5371
): Scale {
5472
const { scaleType, minInterval, domain, isBandScale, timeZone } = xDomain;
5573
const rangeDiff = Math.abs(maxRange - minRange);
@@ -60,20 +78,30 @@ export function computeXScale(
6078
return new ScaleBand(domain, [minRange, maxRange], bandwidth, barsPadding);
6179
} else {
6280
if (isBandScale) {
63-
const intervalCount = (domain[1] - domain[0]) / minInterval;
64-
const bandwidth = rangeDiff / (intervalCount + 1);
65-
const start = isInverse ? minRange - bandwidth : minRange;
66-
const end = isInverse ? maxRange : maxRange - bandwidth;
67-
return new ScaleContinuous(
81+
const [domainMin, domainMax] = domain;
82+
const isSingleValueHistogram = !!enableHistogramMode && domainMax - domainMin === 0;
83+
84+
const adjustedDomainMax = isSingleValueHistogram ? domainMin + minInterval : domainMax;
85+
const adjustedDomain = [domainMin, adjustedDomainMax];
86+
87+
const intervalCount = (adjustedDomain[1] - adjustedDomain[0]) / minInterval;
88+
const intervalCountOffest = isSingleValueHistogram ? 0 : 1;
89+
const bandwidth = rangeDiff / (intervalCount + intervalCountOffest);
90+
91+
const { start, end } = getBandScaleRange(isInverse, isSingleValueHistogram, minRange, maxRange, bandwidth);
92+
93+
const scale = new ScaleContinuous(
6894
scaleType,
69-
domain,
95+
adjustedDomain,
7096
[start, end],
7197
bandwidth / totalBarsInCluster,
7298
minInterval,
7399
timeZone,
74100
totalBarsInCluster,
75101
barsPadding,
76102
);
103+
104+
return scale;
77105
} else {
78106
return new ScaleContinuous(
79107
scaleType,

src/state/chart_state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ export class ChartStore {
840840
this.chartRotation,
841841
this.chartTheme.axes,
842842
barsPadding,
843+
this.enableHistogramMode.get(),
843844
);
844845
if (dimensions) {
845846
this.axesTicksDimensions.set(id, dimensions);

src/state/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export function computeSeriesGeometries(
212212
const { stackedBarsInCluster, totalBarsInCluster } = countBarsInCluster(stacked, nonStacked);
213213

214214
// compute scales
215-
const xScale = computeXScale(xDomain, totalBarsInCluster, 0, width, barsPadding);
215+
const xScale = computeXScale(xDomain, totalBarsInCluster, 0, width, barsPadding, enableHistogramMode);
216216
const yScales = computeYScales(yDomain, height, 0);
217217

218218
// compute colors

stories/annotations.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,4 +547,64 @@ storiesOf('Annotations', module)
547547
/>
548548
</Chart>
549549
);
550+
})
551+
.add('[test] line annotation single value histogram', () => {
552+
const dataValues = [
553+
{
554+
dataValue: 3.5,
555+
},
556+
];
557+
558+
const style = {
559+
line: {
560+
strokeWidth: 3,
561+
stroke: '#f00',
562+
opacity: 1,
563+
},
564+
details: {
565+
fontSize: 12,
566+
fontFamily: 'Arial',
567+
fontStyle: 'bold',
568+
fill: 'gray',
569+
padding: 0,
570+
},
571+
};
572+
573+
const chartRotation = select<Rotation>(
574+
'chartRotation',
575+
{
576+
'0 deg': 0,
577+
'90 deg': 90,
578+
'-90 deg': -90,
579+
'180 deg': 180,
580+
},
581+
0,
582+
);
583+
584+
const xDomain = {
585+
minInterval: 1,
586+
};
587+
588+
return (
589+
<Chart className={'story-chart'}>
590+
<Settings debug={boolean('debug', false)} rotation={chartRotation} xDomain={xDomain} />
591+
<LineAnnotation
592+
annotationId={getAnnotationId('anno_1')}
593+
domainType={AnnotationDomainTypes.XDomain}
594+
dataValues={dataValues}
595+
style={style}
596+
/>
597+
<Axis id={getAxisId('horizontal')} position={Position.Bottom} title={'x-domain axis'} />
598+
<Axis id={getAxisId('vertical')} title={'y-domain axis'} position={Position.Left} />
599+
<BarSeries
600+
enableHistogramMode={true}
601+
id={getSpecId('bars')}
602+
xScaleType={ScaleType.Linear}
603+
yScaleType={ScaleType.Linear}
604+
xAccessor="x"
605+
yAccessors={['y']}
606+
data={[{ x: 3, y: 2 }]}
607+
/>
608+
</Chart>
609+
);
550610
});

0 commit comments

Comments
 (0)