Skip to content

ts defs #1001

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

Closed
wants to merge 23 commits into from
Closed

ts defs #1001

Show file tree
Hide file tree
Changes from all 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
227 changes: 227 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

/*
* API
*/

/**
* Aggregation options for the group transform:
* * a string describing an aggregation (first, min, sum, count…)
* * a function - passed the array of values for each group
* * an object with a reduce method, an optionally a scope
* @link https://github.com/observablehq/plot/blob/main/README.md#group
*/
export type AggregationMethod = "first" | "last" | "count" | "sum" | "proportion" | "proportion-facet" | "min" | "min-index" | "max" | "max-index" | "mean" | "median" | "mode" | PXX | "deviation" | "variance" | ReduceFunction | Reduce1;
export type Reduce1 = {
label?: string;
reduce: (I: IndexArray, X: any, context?: any, extent?: any) => any;
scope?: "data" | "facet"
}; // TODO: rename to ReduceMethod + ReduceObject?
type ReduceFunction = ((data?: ArrayLike<any>, extent?: any) => any);

/**
* Facets expressed as an array of arrays of indices
*/
export type MaybeFacetArray = IndexArray[] | undefined;

/**
* Array of indices into the data
*/
export type IndexArray = number[] | Uint32Array;


/**
* The inset option is a number
*/
export type InsetOption = number | undefined;

/**
* Plot.column()
* @link https://github.com/observablehq/plot/blob/main/README.md#plotcolumnsource
*/
export type Column = [column, setColumn];
export type column = {transform: () => any[]; label?: string}
export type setColumn = (v: Array<any>) => Array<any>;


/**
* Map methods for Plot.map, Plot.mapX, Plot.mapY
* * cumsum - a cumulative sum
* * rank - the rank of each value in the sorted array
* * quantile - the rank, normalized between 0 and 1
* * a function to be passed an array of values, returning new values
* * an object that implements the map method
*/
export type MapMethod = "cumsum" | "rank" | "quantile" | ((S: Channel) => Channel) | {map: (I: IndexArray, S: Channel, T: any[]) => any};

/*
* COMMON
*/


/*
* UNSORTED
*/

export type nullish = null | undefined;
export type DataSource = Iterable<any> | ArrayLike<any>;
export type UserOption = unknown; // TODO: remove this type by checking which options are allowed in each case
export type booleanOption = boolean | nullish;
export type numberOption = number | nullish;
export type stringOption = number | any[] | string | nullish;
export type TextChannel = string[];
export type NumberChannel = number[] | Float32Array | Float64Array;
export type Channel = TextChannel | NumberChannel | any[];
export type ConstantOrFieldOption = string | IAccessor | number | Channel | Date | ITransform | nullish;
export type Comparator = (a: any, b: any) => number;

/**
* Definition for both transform and initializer functions.
* TODO: clarify the difference (when facets are returned or not, in the case of an initializer)
*/
export type TransformFunction = (this: IMark, data: any, facets: MaybeFacetArray, channels?: any, scales ?: any, dimensions?: IDimensions) => {data?: any, facets?: IndexArray[], channels?: any};


export type OutputOptions = Partial<{[P in FieldOptionsKey]: AggregationMethod}> & {
data?: any;
reverse?: boolean;
}



export interface FieldOptions {
x?: ConstantOrFieldOption;
x1?: ConstantOrFieldOption;
x2?: ConstantOrFieldOption;
y?: ConstantOrFieldOption;
y1?: ConstantOrFieldOption;
y2?: ConstantOrFieldOption;
z?: ConstantOrFieldOption;
fill?: ConstantOrFieldOption;
stroke?: ConstantOrFieldOption;
title?: ConstantOrFieldOption;
href?: ConstantOrFieldOption;
filter?: ConstantOrFieldOption;
sort?: ConstantOrFieldOption;
}

export interface MarkOptionsDefined extends FieldOptions {
transform?: TransformFunction | null;
initializer?: TransformFunction | null;
reverse?: boolean;
}

export type FieldOptionsKey = keyof FieldOptions;
export type MarkOptions = MarkOptionsDefined | undefined;
export type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor;
export type IAccessor = (d: any, i: number, data?: ArrayLike<any>) => any;
export type booleanish = boolean | undefined;
export type ITransform = {transform: (data: DataSource) => DataSource};

/**
* The document context, used to create new DOM elements.
* @link https://github.com/observablehq/plot/blob/main/README.md#layout-options
*/
export interface IContext {
document: Document;
}

/**
* A restrictive definition of D3 selections
*/
export interface ISelection {
append: (name: string) => ISelection;
attr: (name: string, value: any) => ISelection;
call: (callback: (selection: ISelection, ...args: any[]) => void, ...args: any[]) => ISelection;
each: (callback: (d: any) => void) => ISelection;
filter: (filter: (d: any, i: number) => boolean) => ISelection;
property: (name: string, value: any) => ISelection;
style: (name: string, value: any) => ISelection;
text: (value: any) => ISelection;
[Symbol.iterator]: () => IterableIterator<SVGElement | HTMLElement>;
}

/**
* A restrictive definition of D3 scales
*/
export interface IScale {
bandwidth?: () => number;
}

/**
* An object of style definitions to apply to DOM elements
*/
export type IStyleObject = Record<string, any>;

/**
* A mark
* @link https://github.com/observablehq/plot/blob/main/README.md#mark-options
*/
export interface IMark {
z?: UserOption; // copy the user option for error messages
clip?: "frame";
dx: number;
dy: number;
marker?: MaybeMarkerFunction;
markerStart?: MaybeMarkerFunction;
markerMid?: MaybeMarkerFunction;
markerEnd?: MaybeMarkerFunction;
stroke?: string | nullish;
// common styles
fill?: string | nullish;
fillOpacity?: number | nullish;
strokeWidth?: number | nullish;
strokeOpacity?: number | nullish;
strokeLinejoin?: string | nullish;
strokeLinecap?: string | nullish;
strokeMiterlimit?: number | nullish;
strokeDasharray?: string | nullish;
strokeDashoffset?: string | nullish;
target?: string | nullish;
ariaLabel?: string | nullish;
ariaDescription?: string | nullish;
ariaHidden?: string | nullish; // "true" | "false" | undefined
opacity?: number | nullish;
mixBlendMode?: string | nullish;
paintOrder?: string | nullish;
pointerEvents?: string | nullish;
shapeRendering?: string | nullish;
// other styles, some of which are not supported by all marks
frameAnchor?: string;
}

/**
* A key: value record of channels values
*/
export type ChannelObject = Record<string, TextChannel | NumberChannel>;

/**
* The dimensions of the plot or the facet
*/
export interface IDimensions {
width: number;
height: number;
marginLeft: number;
marginRight: number;
marginTop: number;
marginBottom: number;
}

/*
* Reducers
*/

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
/**
* Percentile reducer, for transforms such as bin, group, map and window
* @link https://github.com/observablehq/plot/blob/main/README.md#bin
*/
export type PXX = `p${Digit}${Digit}`;

/**
* A marker defines a graphic drawn on vertices of a line or a link mark
* @link https://github.com/observablehq/plot/blob/main/README.md#markers
*/
export type MarkerOption = string | boolean | nullish;
export type MarkerFunction = (color: any, context: any) => Element;
export type MaybeMarkerFunction = MarkerFunction | null;
9 changes: 0 additions & 9 deletions src/context.js

This file was deleted.

10 changes: 10 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type {IContext} from "./common.js";
import {creator, select} from "d3";

export function Context({document = window.document} = {}): IContext {
return {document};
}

export function create(name: string, {document}: IContext) {
return select(creator(name).call(document.documentElement));
}
4 changes: 3 additions & 1 deletion src/format.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {nullish} from "./common.js";

import {format as isoFormat} from "isoformat";
import {string} from "./options.js";
import {memoize1} from "./memoize.js";
Expand Down Expand Up @@ -26,7 +28,7 @@ export function formatIsoDate(date: Date): string {
return isoFormat(date, "Invalid Date");
}

export function formatAuto(locale = "en-US"): (value: any) => string | number | undefined {
export function formatAuto(locale = "en-US"): (value: any) => string | number | nullish {
const number = formatNumber(locale);
return (v: any) => (v instanceof Date ? formatIsoDate : typeof v === "number" ? number : string)(v);
}
Expand Down
53 changes: 35 additions & 18 deletions src/marks/marker.js → src/marks/marker.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {ISelection, IMark, IContext, MarkerOption, MarkerFunction, MaybeMarkerFunction, nullish, IndexArray} from "../common.js";

import {create} from "../context.js";

export function markers(mark, {
export function markers(mark: IMark, {
marker,
markerStart = marker,
markerMid = marker,
markerEnd = marker
}: {
marker?: MarkerOption,
markerStart?: MarkerOption,
markerMid?: MarkerOption,
markerEnd?: MarkerOption
} = {}) {
mark.markerStart = maybeMarker(markerStart);
mark.markerMid = maybeMarker(markerMid);
mark.markerEnd = maybeMarker(markerEnd);
}

function maybeMarker(marker) {
function maybeMarker(marker: MarkerOption): MaybeMarkerFunction {
if (marker == null || marker === false) return null;
if (marker === true) return markerCircleFill;
if (typeof marker === "function") return marker;
Expand All @@ -25,7 +33,7 @@ function maybeMarker(marker) {
throw new Error(`invalid marker: ${marker}`);
}

function markerArrow(color, context) {
function markerArrow(color: string, context: IContext) {
return create("svg:marker", context)
.attr("viewBox", "-5 -5 10 10")
.attr("markerWidth", 6.67)
Expand All @@ -37,21 +45,21 @@ function markerArrow(color, context) {
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
.call(marker => marker.append("path").attr("d", "M-1.5,-3l3,3l-3,3"))
.node();
.node() as Element;
}

function markerDot(color, context) {
function markerDot(color: string, context: IContext) {
return create("svg:marker", context)
.attr("viewBox", "-5 -5 10 10")
.attr("markerWidth", 6.67)
.attr("markerHeight", 6.67)
.attr("fill", color)
.attr("stroke", "none")
.call(marker => marker.append("circle").attr("r", 2.5))
.node();
.node() as Element;
}

function markerCircleFill(color, context) {
function markerCircleFill(color: string, context: IContext) {
return create("svg:marker", context)
.attr("viewBox", "-5 -5 10 10")
.attr("markerWidth", 6.67)
Expand All @@ -60,10 +68,10 @@ function markerCircleFill(color, context) {
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.call(marker => marker.append("circle").attr("r", 3))
.node();
.node() as Element;
}

function markerCircleStroke(color, context) {
function markerCircleStroke(color: string, context: IContext) {
return create("svg:marker", context)
.attr("viewBox", "-5 -5 10 10")
.attr("markerWidth", 6.67)
Expand All @@ -72,31 +80,40 @@ function markerCircleStroke(color, context) {
.attr("stroke", color)
.attr("stroke-width", 1.5)
.call(marker => marker.append("circle").attr("r", 3))
.node();
.node() as Element;
}

let nextMarkerId = 0;

export function applyMarkers(path, mark, {stroke: S} = {}) {
return applyMarkersColor(path, mark, S && (i => S[i]));
export function applyMarkers(path: ISelection, mark: IMark, {stroke: S}: {stroke?: string[]} = {}) {
return applyMarkersColor(path, mark, S && ((i: number) => S[i]));
}

export function applyGroupedMarkers(path, mark, {stroke: S} = {}) {
return applyMarkersColor(path, mark, S && (([i]) => S[i]));
export function applyGroupedMarkers(path: ISelection, mark: IMark, {stroke: S}: {stroke?: string[]} = {}) {
return applyMarkersColor(path, mark, S && (([i]: IndexArray) => S[i]));
}

function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke) {
function applyMarkersColor(
path: ISelection,
{
markerStart,
markerMid,
markerEnd,
stroke
}: IMark,
strokeof: ((i: any) => string | nullish) = (() => stroke) // any is really number or number[]
) {
const iriByMarkerColor = new Map();

function applyMarker(marker) {
return function(i) {
function applyMarker(marker: MarkerFunction) {
return function(this: Element, i: number | [number]) {
const color = strokeof(i);
let iriByColor = iriByMarkerColor.get(marker);
if (!iriByColor) iriByMarkerColor.set(marker, iriByColor = new Map());
let iri = iriByColor.get(color);
if (!iri) {
const context = {document: this.ownerDocument};
const node = this.parentNode.insertBefore(marker(color, context), this);
const node = (this.parentNode as Element).insertBefore(marker(color, context), this) as Element;
const id = `plot-marker-${++nextMarkerId}`;
node.setAttribute("id", id);
iriByColor.set(color, iri = `url(#${id})`);
Expand Down
Loading