Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(scales): add LinearBinary scale type #1389

Merged
merged 6 commits into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ module.exports = {
'jsx-a11y/no-static-element-interactions': 1,
'jsx-a11y/mouse-events-have-key-events': 1,
'jsx-a11y/click-events-have-key-events': 1,
'@typescript-eslint/member-ordering': 1,
'@typescript-eslint/member-ordering': 0,
eqeqeq: 2,

/*
Expand Down
21 changes: 21 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

---

This product includes code that is adapted from [email protected] and [email protected],
which are both available under a "ISC" license.

ISC License

Copyright 2010-2021 Mike Bostock

Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

---
This product includes code that is adapted from https://github.com/Myndex/SAPC-APCA
which is available under a "W3C SOFTWARE NOTICE AND LICENSE" license.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions integration/tests/scales_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ describe('Scales stories', () => {
);
});
});
it('should render linear binary with nicing', async () => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/scales--linear-binary&globals=theme:light&knob-Data type=Base 2&knob-yScaleType=linear_binary&knob-Nice y ticks=true`,
);
});
});
3 changes: 2 additions & 1 deletion packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1701,7 +1701,7 @@ export type Rotation = 0 | 90 | -90 | 180;
export type ScaleBandType = ScaleOrdinalType;

// @public (undocumented)
export type ScaleContinuousType = typeof ScaleType.Linear | typeof ScaleType.Time | typeof ScaleType.Log | typeof ScaleType.Sqrt;
export type ScaleContinuousType = typeof ScaleType.LinearBinary | typeof ScaleType.Linear | typeof ScaleType.Time | typeof ScaleType.Log | typeof ScaleType.Sqrt;

// @public (undocumented)
export type ScaleOrdinalType = typeof ScaleType.Ordinal;
Expand All @@ -1714,6 +1714,7 @@ export interface ScalesConfig {

// @public
export const ScaleType: Readonly<{
LinearBinary: "linear_binary";
Linear: "linear";
Ordinal: "ordinal";
Log: "log";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export function fillSeries(

function isXFillNotRequired(spec: BasicSeriesSpec, groupScaleType: ScaleType, isStacked: boolean) {
const onlyNoFitAreaLine = (isAreaSeriesSpec(spec) || isLineSeriesSpec(spec)) && !spec.fit;
const onlyContinuous = groupScaleType === ScaleType.Linear || groupScaleType === ScaleType.Time;
const onlyContinuous =
groupScaleType === ScaleType.Linear ||
groupScaleType === ScaleType.LinearBinary ||
groupScaleType === ScaleType.Time;
return onlyNoFitAreaLine && onlyContinuous && !isStacked;
}
128 changes: 128 additions & 0 deletions packages/charts/src/chart_types/xy_chart/utils/get_linear_ticks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* eslint-disable header/header, no-param-reassign */

/**
* @notice
* This product includes code that is adapted from [email protected] and [email protected],
* which are both available under a "ISC" license.
*
* ISC License
*
* Copyright 2010-2021 Mike Bostock
* Permission to use, copy, modify, and/or distribute this software for any purpose
* with or without fee is hereby granted, provided that the above copyright notice
* and this permission notice appear in all copies.

* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
* THIS SOFTWARE.
*/

import { ScaleContinuousNumeric } from 'd3-scale';

import { PrimitiveValue } from '../../partition_chart/layout/utils/group_by_rollup';

const e10 = Math.sqrt(50);
const e5 = Math.sqrt(10);
const e2 = Math.sqrt(2);

/** @internal */
export function getLinearTicks(start: number, stop: number, count: number, base: number = 2) {
let reverse,
i = -1,
n,
ticks,
step;

stop = +stop;
start = +start;
count = +count;
if (start === stop && count > 0) return [start];
if ((reverse = stop < start)) {
n = start;
start = stop;
stop = n;
}
if ((step = tickIncrement(start, stop, count, base)) === 0 || !isFinite(step)) return [];

if (step > 0) {
let r0 = Math.round(start / step),
r1 = Math.round(stop / step);
if (r0 * step < start) ++r0;
if (r1 * step > stop) --r1;
ticks = new Array((n = r1 - r0 + 1));
while (++i < n) ticks[i] = (r0 + i) * step;
} else {
step = -step;
let r0 = Math.round(start * step),
r1 = Math.round(stop * step);
if (r0 / step < start) ++r0;
if (r1 / step > stop) --r1;
ticks = new Array((n = r1 - r0 + 1));
while (++i < n) ticks[i] = (r0 + i) / step;
}

if (reverse) ticks.reverse();

return ticks;
}

function tickIncrement(start: number, stop: number, count: number, base: number = 10) {
const step = (stop - start) / Math.max(0, count);
const power = Math.floor(Math.log(step) / Math.log(base) + Number.EPSILON);
const error = step / Math.pow(base, power);
return power >= 0
? (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1) * Math.pow(base, power)
: -Math.pow(base, -power) / (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1);
}

/** @internal */
export function getNiceLinearTicks(
scale: ScaleContinuousNumeric<PrimitiveValue, number>,
count: number = 10,
base = 10,
) {
const d = scale.domain();
let i0 = 0;
let i1 = d.length - 1;
let start = d[i0];
let stop = d[i1];
let prestep;
let step;
let maxIter = 10;

if (stop < start) {
step = start;
start = stop;
stop = step;

step = i0;
i0 = i1;
i1 = step;
}

while (maxIter-- > 0) {
step = tickIncrement(start, stop, count, base);
if (step === prestep) {
d[i0] = start;
d[i1] = stop;
return scale.domain(d);
} else if (step > 0) {
start = Math.floor(start / step) * step;
stop = Math.ceil(stop / step) * step;
} else if (step < 0) {
start = Math.ceil(start * step) / step;
stop = Math.floor(stop * step) / step;
} else {
break;
}
prestep = step;
}

return scale;
}

/* eslint-enable header/header, no-param-reassign */
4 changes: 4 additions & 0 deletions packages/charts/src/scales/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { $Values } from 'utility-types';
* @public
*/
export const ScaleType = Object.freeze({
/**
* Treated as linear scale with ticks in base 2
*/
LinearBinary: 'linear_binary' as const,
Linear: 'linear' as const,
Ordinal: 'ordinal' as const,
Log: 'log' as const,
Expand Down
1 change: 1 addition & 0 deletions packages/charts/src/scales/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ScaleType } from './constants';

/** @public */
export type ScaleContinuousType =
| typeof ScaleType.LinearBinary
| typeof ScaleType.Linear
| typeof ScaleType.Time
| typeof ScaleType.Log
Expand Down
38 changes: 33 additions & 5 deletions packages/charts/src/scales/scale_continuous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Required } from 'utility-types';

import { Scale, ScaleContinuousType } from '.';
import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup';
import { getLinearTicks, getNiceLinearTicks } from '../chart_types/xy_chart/utils/get_linear_ticks';
import { screenspaceMarkerScaleCompressor } from '../solvers/screenspace_marker_scale_compressor';
import { clamp, mergePartial } from '../utils/common';
import { getMomentWithTz } from '../utils/data/date_time';
Expand Down Expand Up @@ -48,6 +49,7 @@ const defaultScaleOptions: ScaleOptions = {
integersOnly: false,
logBase: 10,
logMinLimit: NaN, // NaN preserves the replaced `undefined` semantics
linearBase: 10,
};

const isUnitRange = ([r1, r2]: Range) => r1 === 0 && r2 === 1;
Expand All @@ -63,6 +65,7 @@ export class ScaleContinuous implements Scale<number> {
readonly domain: number[];
readonly range: Range;
readonly isInverted: boolean;
readonly linearBase: number;
readonly tickValues: number[];
readonly timeZone: string;
readonly barsPadding: number;
Expand All @@ -71,9 +74,11 @@ export class ScaleContinuous implements Scale<number> {
private readonly inverseProject: (d: number) => number | Date;

constructor(
{ type = ScaleType.Linear, domain: inputDomain, range, nice = false }: ScaleData,
{ type: scaleType = ScaleType.Linear, domain: inputDomain, range, nice = false }: ScaleData,
options?: Partial<ScaleOptions>,
) {
const isBinary = scaleType === ScaleType.LinearBinary;
const type = isBinary ? ScaleType.Linear : scaleType;
const scaleOptions: ScaleOptions = mergePartial(defaultScaleOptions, options);

const min = inputDomain.reduce((p, n) => Math.min(p, n), Infinity);
Expand All @@ -94,6 +99,7 @@ export class ScaleContinuous implements Scale<number> {
this.bandwidthPadding = bandwidthPadding;
this.type = type;
this.range = range;
this.linearBase = isBinary ? 2 : scaleOptions.linearBase;
this.minInterval = minInterval;
this.step = bandwidth + barsPadding + bandwidthPadding;
this.timeZone = scaleOptions.timeZone;
Expand All @@ -110,7 +116,19 @@ export class ScaleContinuous implements Scale<number> {

/** Start of Projection (desiredTickCount and screenspace dependent part) */

if (isNice) (d3Scale as ScaleContinuousNumeric<PrimitiveValue, number>).nice(scaleOptions.desiredTickCount);
if (isNice) {
if (type === ScaleType.Linear) {
getNiceLinearTicks(
d3Scale as ScaleContinuousNumeric<PrimitiveValue, number>,
scaleOptions.desiredTickCount,
this.linearBase,
);
} else {
(d3Scale as ScaleContinuousNumeric<PrimitiveValue, number>)
.domain(dataDomain)
.nice(scaleOptions.desiredTickCount);
}
}

const niceDomain = isNice ? (d3Scale.domain() as number[]) : dataDomain;

Expand All @@ -135,9 +153,14 @@ export class ScaleContinuous implements Scale<number> {
type === ScaleType.Time
? getTimeTicks(scaleOptions.desiredTickCount, scaleOptions.timeZone, nicePaddedDomain)
: scaleOptions.minInterval <= 0 || scaleOptions.bandwidth <= 0
? (d3Scale as D3ScaleNonTime)
.ticks(scaleOptions.desiredTickCount)
.filter(scaleOptions.integersOnly ? Number.isInteger : () => true)
? this.type === ScaleType.Linear
? getLinearTicks(
nicePaddedDomain[0],
nicePaddedDomain[nicePaddedDomain.length - 1],
scaleOptions.desiredTickCount,
this.linearBase,
)
: (d3Scale as D3ScaleNonTime).ticks(scaleOptions.desiredTickCount)
: new Array(Math.floor((nicePaddedDomain[1] - nicePaddedDomain[0]) / minInterval) + 1)
.fill(0)
.map((_, i) => nicePaddedDomain[0] + i * minInterval);
Expand Down Expand Up @@ -372,4 +395,9 @@ type ScaleOptions = Required<LogScaleOptions, 'logBase'> & {
* We need to limit the domain scale to the right value on all possible cases.
*/
logMinLimit: number;
/**
* scale base used to determine ticks in linear scales
* @defaultValue 10
*/
linearBase: number;
nickofthyme marked this conversation as resolved.
Show resolved Hide resolved
};
62 changes: 62 additions & 0 deletions storybook/stories/scales/8_linear_binary.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { boolean, select } from '@storybook/addon-knobs';
import React from 'react';

import { Axis, Chart, LineSeries, Position, ScaleType, Settings } from '@elastic/charts';
import { getRandomNumberGenerator } from '@elastic/charts/src/mocks/utils';

import { useBaseTheme } from '../../use_base_theme';

const rng = getRandomNumberGenerator();

function generateRandomBinary(bitCount: number) {
return rng(0, 2 ** bitCount - 1);
}

const data = new Array(20).fill(1).map((_, x) => ({
x,
'Base 10': rng(0, 2000),
'Base 2': generateRandomBinary(11),
}));

export const Example = () => {
const yAccessor = select('Data type', ['Base 10', 'Base 2'], 'Base 2');
const yScaleType = select(
'yScaleType',
{
Linear: ScaleType.Linear,
'Linear Binary': ScaleType.LinearBinary,
},
ScaleType.LinearBinary,
);
const yNice = boolean('Nice y ticks', false);

return (
<Chart>
<Settings baseTheme={useBaseTheme()} />
<Axis id="bottom" title="" position={Position.Bottom} showOverlappingTicks />
<Axis id="binary" title={yAccessor} position={Position.Left} />
<LineSeries
id="lines"
xScaleType={ScaleType.Linear}
yScaleType={yScaleType}
data={data}
yNice={yNice}
yAccessors={[yAccessor]}
/>
</Chart>
);
};

Example.parameters = {
markdown: `By default, \`Linear\` scales compute scale ticks per base 10 numerical system.
You may set the \`yScaleType\` to \`LinearBinary\` to compute ticks per base 2 numerical system.
This base is also applied to tick nicing.`,
};
1 change: 1 addition & 0 deletions storybook/stories/scales/scales.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export { Example as tooltipInUTC } from './3_utc_tooltip.story';
export { Example as specifiedTimezone } from './4_specified_timezone.story';
export { Example as xScaleFallback } from './6_x_scale_fallback.story';
export { Example as logScaleOptions } from './7_log_scale_options.story';
export { Example as linearBinary } from './8_linear_binary.story';