Skip to content

Commit

Permalink
Merge pull request #110 from VisActor/feat/fish-eye
Browse files Browse the repository at this point in the history
Feat/fish eye
  • Loading branch information
xile611 authored Oct 13, 2023
2 parents 26f1ef0 + 326e950 commit 6266e08
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "feat: add fish eye effect of scale\n\n",
"type": "none",
"packageName": "@visactor/vscale"
}
],
"packageName": "@visactor/vscale",
"email": "[email protected]"
}
41 changes: 41 additions & 0 deletions packages/vscale/__tests__/fish-eye.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { LinearScale } from '../src/linear-scale';
import { BandScale } from '../src/band-scale';

test('fishEye of band scale', function () {
const s = new BandScale(true).domain(['A', 'B', 'C', 'D', 'E']).range([0, 100]);

expect(s.scale('A')).toBeCloseTo(0);
expect(s.scale('B')).toBeCloseTo(20);
expect(s.scale('C')).toBeCloseTo(40);
expect(s.scale('D')).toBeCloseTo(60);
expect(s.scale('E')).toBeCloseTo(80);

s.fishEye({ focus: 50, radius: 20 });
expect(s.scale('A')).toBeCloseTo(0);
expect(s.scale('B')).toBeCloseTo(20);
expect(s.scale('C')).toBeCloseTo(36.53412132054993);
expect(s.scale('D')).toBeCloseTo(63.46587867945007);
expect(s.scale('E')).toBeCloseTo(80);
});

test('fishEye of linear scale', function () {
const s = new LinearScale().domain([20, 60]).range([0, 100]);

expect(s.scale(20)).toBeCloseTo(0);
expect(s.scale(35)).toBeCloseTo(37.5);
expect(s.scale(40)).toBeCloseTo(50);
expect(s.scale(45)).toBeCloseTo(62.5);
expect(s.scale(60)).toBeCloseTo(100);

s.fishEye({ focus: 50, radius: 20 });

expect(s.scale(20)).toBeCloseTo(0);
expect(s.scale(35)).toBeCloseTo(34.4974531432733);
expect(s.scale(40)).toBeCloseTo(50);
expect(s.scale(45)).toBeCloseTo(65.5025468567267);
expect(s.scale(60)).toBeCloseTo(100);

s.fishEye(null, false, true);
expect(s.scale(35)).toBeCloseTo(37.5);
expect(s.scale(45)).toBeCloseTo(62.5);
});
18 changes: 17 additions & 1 deletion packages/vscale/src/band-scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { OrdinalScale } from './ordinal-scale';
import { bandSpace, calculateBandwidthFromWholeRangeSize, scaleWholeRangeSize } from './utils/utils';
import { ScaleEnum } from './type';
import { stepTicks, ticks } from './utils/tick-sample-int';
import type { DiscreteScaleType, IBandLikeScale, TickData } from './interface';
import type { DiscreteScaleType, IBandLikeScale, ScaleFishEyeOptions, TickData } from './interface';

// band scale 各参数参考图示 https://raw.githubusercontent.com/d3/d3-scale/master/img/band.png
export class BandScale extends OrdinalScale implements IBandLikeScale {
Expand Down Expand Up @@ -68,6 +68,9 @@ export class BandScale extends OrdinalScale implements IBandLikeScale {
return start + this._step * i;
});
super.range(reverse ? values.reverse() : values);

this.generateFishEyeTransform();

return this;
}

Expand Down Expand Up @@ -374,6 +377,19 @@ export class BandScale extends OrdinalScale implements IBandLikeScale {
return this.rescale(slience);
}

fishEye(): ScaleFishEyeOptions;
fishEye(options: ScaleFishEyeOptions, slience?: boolean, clear?: boolean): this;
fishEye(options?: ScaleFishEyeOptions, slience?: boolean, clear?: boolean): this | ScaleFishEyeOptions {
if (options || clear) {
this._fishEyeOptions = options;
this._fishEyeTransform = null;

return this.rescale(slience);
}

return this._fishEyeOptions;
}

isBandwidthFixed() {
return this._isFixed && !!this._bandwidth;
}
Expand Down
51 changes: 47 additions & 4 deletions packages/vscale/src/base-scale.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { IRangeFactor } from './interface';
import { clamp, isNil } from '@visactor/vutils';
import type { IRangeFactor, ScaleFishEyeOptions } from './interface';

export abstract class BaseScale implements IRangeFactor {
protected _wholeRange: any[];
protected _rangeFactor?: number[];
protected _unknown: any;
protected _fishEyeOptions?: ScaleFishEyeOptions;
protected _fishEyeTransform?: (output: number) => number;

abstract range(): any[];
abstract domain(): any[];
Expand All @@ -29,19 +32,59 @@ export abstract class BaseScale implements IRangeFactor {
abstract calculateVisibleDomain(range: any[]): any[];

rangeFactor(): [number, number];
rangeFactor(_: [number, number], slience?: boolean): this;
rangeFactor(_?: [number, number], slience?: boolean): this | any[] {
rangeFactor(_: [number, number], slience?: boolean, clear?: boolean): this;
rangeFactor(_?: [number, number], slience?: boolean, clear?: boolean): this | any[] {
if (!_) {
if (clear) {
this._wholeRange = null;
this._rangeFactor = null;
return this;
}

return this._rangeFactor;
}
if (_.length === 2 && _.every(r => r >= 0 && r <= 1)) {
this._wholeRange = null;
this._rangeFactor = _;
this._rangeFactor = _[0] === 0 && _[1] === 1 ? null : _;
}

return this;
}

protected generateFishEyeTransform() {
if (!this._fishEyeOptions) {
this._fishEyeTransform = null;

return;
}
const { distortion = 2, radiusRatio = 0.1, radius } = this._fishEyeOptions;
const range = this.range();
const first = range[0];
const last = range[range.length - 1];
const min = Math.min(first, last);
const max = Math.max(first, last);
const focus = clamp(this._fishEyeOptions.focus ?? 0, min, max);
const rangeRadius = isNil(radius) ? (max - min) * radiusRatio : radius;
let k0 = Math.exp(distortion);
k0 = (k0 / (k0 - 1)) * rangeRadius;
const k1 = distortion / rangeRadius;

this._fishEyeTransform = (output: number) => {
const delta = Math.abs(output - focus);

if (delta >= rangeRadius) {
return output;
}

if (delta <= 1e-6) {
return focus;
}
const k = ((k0 * (1 - Math.exp(-delta * k1))) / delta) * 0.75 + 0.25;

return focus + (output - focus) * k;
};
}

unknown(): any[];
unknown(_: any): this;
unknown(_?: any): this | any {
Expand Down
21 changes: 19 additions & 2 deletions packages/vscale/src/continuous-scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type {
IContinuousScale,
ContinuousScaleType,
TickData,
NiceType
NiceType,
ScaleFishEyeOptions
} from './interface';
import { interpolate } from './utils/interpolate';
import { bimap, identity, polymap } from './utils/utils';
Expand Down Expand Up @@ -57,6 +58,19 @@ export class ContinuousScale extends BaseScale implements IContinuousScale {
return this._niceDomain ?? this._domain;
}

fishEye(): ScaleFishEyeOptions;
fishEye(options: ScaleFishEyeOptions, slience?: boolean, clear?: boolean): this;
fishEye(options?: ScaleFishEyeOptions, slience?: boolean, clear?: boolean): this | ScaleFishEyeOptions {
if (options || clear) {
this._fishEyeOptions = options;
this._fishEyeTransform = null;

return this.rescale(slience);
}

return this._fishEyeOptions;
}

scale(x: any): any {
x = Number(x);
if (Number.isNaN(x) || (this._domainValidator && !this._domainValidator(x))) {
Expand All @@ -69,8 +83,9 @@ export class ContinuousScale extends BaseScale implements IContinuousScale {
this._interpolate
);
}
const output = this._output(this.transformer(this._clamp(x)));

return this._output(this.transformer(this._clamp(x)));
return this._fishEyeTransform ? this._fishEyeTransform(output) : output;
}

invert(y: any): any {
Expand Down Expand Up @@ -145,6 +160,8 @@ export class ContinuousScale extends BaseScale implements IContinuousScale {
this._piecewise = n > 2 ? polymap : bimap;
this._output = this._input = null;
this._wholeRange = null;

this.generateFishEyeTransform();
return this;
}

Expand Down
11 changes: 10 additions & 1 deletion packages/vscale/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type ScaleType = DiscreteScaleType | ContinuousScaleType | DiscretizingSc

export interface IRangeFactor {
calculateVisibleDomain: (range: any[]) => any;
rangeFactor: (_?: [number, number], slience?: boolean) => this | any;
rangeFactor: (_?: [number, number], slience?: boolean, clear?: boolean) => this | any;
unknown: (_?: any) => this | any;
}

Expand Down Expand Up @@ -101,6 +101,7 @@ export interface IBandLikeScale extends IOrdinalScale, IRangeFactor {
round: (_?: boolean, slience?: boolean) => this | boolean;
align: (_?: number, slience?: boolean) => this | number;
clone: () => IBandLikeScale;
fishEye: (options?: ScaleFishEyeOptions, slience?: boolean, clear?: boolean) => this | ScaleFishEyeOptions;
}

export interface IContinuousScale extends IBaseScale, IRangeFactor {
Expand All @@ -111,6 +112,7 @@ export interface IContinuousScale extends IBaseScale, IRangeFactor {
interpolate: (_?: InterpolateType<any>, slience?: boolean) => this | InterpolateType<any>;
clone?: () => IContinuousScale;
rescale: () => this;
fishEye: (options?: ScaleFishEyeOptions, slience?: boolean, clear?: boolean) => this | ScaleFishEyeOptions;
}

export type ILinearScale = IContinuousScale & IContinuesScaleTicks;
Expand Down Expand Up @@ -166,3 +168,10 @@ export interface NiceOptions {
}

export type NiceType = 'all' | 'min' | 'max';

export interface ScaleFishEyeOptions {
distortion?: number;
focus?: number;
radius?: number;
radiusRatio?: number;
}
4 changes: 3 additions & 1 deletion packages/vscale/src/ordinal-scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export class OrdinalScale extends BaseScale implements IBaseScale {
i = this._domain.push(d);
this._index.set(key, i);
}
return this._ordinalRange[(i - 1) % this._ordinalRange.length];
const output = this._ordinalRange[(i - 1) % this._ordinalRange.length];

return this._fishEyeTransform ? this._fishEyeTransform(output) : output;
}

// d3-scale里没有对ordinal-scale添加invert能力,这里只做简单的映射
Expand Down
15 changes: 15 additions & 0 deletions packages/vscale/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,18 @@ export function isDiscretizing(type: string) {
return false;
}
}
export function supportRangeFactor(type: string) {
switch (type) {
case ScaleEnum.Linear:
case ScaleEnum.Log:
case ScaleEnum.Pow:
case ScaleEnum.Sqrt:
case ScaleEnum.Symlog:
case ScaleEnum.Time:
case ScaleEnum.Ordinal:
case ScaleEnum.Point:
return true;
default:
return false;
}
}

0 comments on commit 6266e08

Please sign in to comment.