From f584c429363b57857faa60fc2f48332a8f34a593 Mon Sep 17 00:00:00 2001 From: Ben Schmidt Date: Tue, 11 Jun 2024 17:46:09 -0400 Subject: [PATCH 1/3] restore linear color scales --- dev/svelte/ColorChange.svelte | 14 +++ package-lock.json | 20 ++-- package.json | 2 +- src/aesthetics/ColorAesthetic.ts | 167 +++++++++++++++--------------- src/aesthetics/ScaledAesthetic.ts | 108 ++++++++++--------- 5 files changed, 160 insertions(+), 151 deletions(-) diff --git a/dev/svelte/ColorChange.svelte b/dev/svelte/ColorChange.svelte index d8fd7d101..8027cd523 100644 --- a/dev/svelte/ColorChange.svelte +++ b/dev/svelte/ColorChange.svelte @@ -15,6 +15,18 @@ }, }); } + function plotViridis() { + scatterplot.plotAPI({ + encoding: { + color: { + field: 'x', + range: 'viridis', + // range: ["red", "yellow", "pink", "purple"], + // domain: ["Apple", "Banana", "Strawberry", "Mulberry"] + }, + }, + }); + } + + diff --git a/package-lock.json b/package-lock.json index 5e65d17e6..b686150a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "d3-timer": "^3.0.1", "d3-transition": "^3.0.1", "d3-zoom": "^3.0.0", - "deepscatter": "^3.0.0-next.6", + "deepscatter": "^3.0.0-next.23", "glsl-easings": "^1.0.0", "glsl-fast-gaussian-blur": "^1.0.2", "glsl-read-float": "^1.1.0", @@ -56,7 +56,7 @@ "vite": "^5.1.4" }, "peerDependencies": { - "apache-arrow": "^15.0.2" + "apache-arrow": ">=11.0.0 <17.0.0" } }, "node_modules/@75lb/deep-merge": { @@ -2418,9 +2418,9 @@ } }, "node_modules/deepscatter": { - "version": "3.0.0-next.6", - "resolved": "https://registry.npmjs.org/deepscatter/-/deepscatter-3.0.0-next.6.tgz", - "integrity": "sha512-IhagIaVUWWex02fL9U+QqrSuSItWzo0aV9fcOuCV3HabUNFIF2Jm+rRnvDg3tEzPr3w4jUhPvoAMATYLlj8XfA==", + "version": "3.0.0-next.23", + "resolved": "https://registry.npmjs.org/deepscatter/-/deepscatter-3.0.0-next.23.tgz", + "integrity": "sha512-AuQ4fPvMZMgeUh1KVJFVUhExkiSYOYm0XyY3bSxCuYO7651mbRY//N0JSvi7D2L8OHfTVa0omJ0KeyNyTflcgQ==", "dependencies": { "d3-array": "^3.2.4", "d3-color": "^3.1.0", @@ -2433,6 +2433,7 @@ "d3-timer": "^3.0.1", "d3-transition": "^3.0.1", "d3-zoom": "^3.0.0", + "deepscatter": "^3.0.0-next.6", "glsl-easings": "^1.0.0", "glsl-fast-gaussian-blur": "^1.0.2", "glsl-read-float": "^1.1.0", @@ -2441,7 +2442,7 @@ "regl": "^2.1.0" }, "peerDependencies": { - "apache-arrow": "^15.0.2" + "apache-arrow": ">=11.0.0 <17.0.0" } }, "node_modules/define-data-property": { @@ -8233,9 +8234,9 @@ "dev": true }, "deepscatter": { - "version": "3.0.0-next.6", - "resolved": "https://registry.npmjs.org/deepscatter/-/deepscatter-3.0.0-next.6.tgz", - "integrity": "sha512-IhagIaVUWWex02fL9U+QqrSuSItWzo0aV9fcOuCV3HabUNFIF2Jm+rRnvDg3tEzPr3w4jUhPvoAMATYLlj8XfA==", + "version": "3.0.0-next.23", + "resolved": "https://registry.npmjs.org/deepscatter/-/deepscatter-3.0.0-next.23.tgz", + "integrity": "sha512-AuQ4fPvMZMgeUh1KVJFVUhExkiSYOYm0XyY3bSxCuYO7651mbRY//N0JSvi7D2L8OHfTVa0omJ0KeyNyTflcgQ==", "requires": { "d3-array": "^3.2.4", "d3-color": "^3.1.0", @@ -8248,6 +8249,7 @@ "d3-timer": "^3.0.1", "d3-transition": "^3.0.1", "d3-zoom": "^3.0.0", + "deepscatter": "^3.0.0-next.6", "glsl-easings": "^1.0.0", "glsl-fast-gaussian-blur": "^1.0.2", "glsl-read-float": "^1.1.0", diff --git a/package.json b/package.json index f1cc1209b..fb53a5241 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "d3-timer": "^3.0.1", "d3-transition": "^3.0.1", "d3-zoom": "^3.0.0", - "deepscatter": "^3.0.0-next.6", + "deepscatter": "^3.0.0-next.23", "glsl-easings": "^1.0.0", "glsl-fast-gaussian-blur": "^1.0.2", "glsl-read-float": "^1.1.0", diff --git a/src/aesthetics/ColorAesthetic.ts b/src/aesthetics/ColorAesthetic.ts index 44f910b7f..51281a2f8 100644 --- a/src/aesthetics/ColorAesthetic.ts +++ b/src/aesthetics/ColorAesthetic.ts @@ -11,10 +11,16 @@ import { ScaledAesthetic } from './ScaledAesthetic'; import { Scatterplot } from '../scatterplot'; import { TextureSet } from './AestheticSet'; import { Datum } from './Aesthetic'; -import { ScaleOrdinal } from 'd3-scale'; +import { + ScaleOrdinal, + scaleSequentialLog, + scaleSequentialSqrt, + scaleSequential, +} from 'd3-scale'; +import { interpolateHsl } from 'd3-interpolate'; function materialize_color_interpolator( - interpolator: (t: number) => string + interpolator: (t: number) => string, ): Uint8Array { const output = new Uint8Array(4 * PALETTE_SIZE); arange(PALETTE_SIZE).forEach((i) => { @@ -136,45 +142,85 @@ for (const interpolator of d3Interpolators) { } } +function getSequentialScale( + range: string | [string, string], + transform: DS.Transform, +) { + let interpolator: typeof d3Chromatic.interpolateViridis; + if (typeof range === 'string') { + // So we have to write `puOr`, `viridis`, etc. instead of `PuOr`, `Viridis` + const k = 'interpolate' + range.charAt(0).toUpperCase() + range.slice(1); + interpolator = d3Chromatic[k] as typeof d3Chromatic.interpolateViridis; + if (interpolator === undefined) { + throw new Error(`Unknown interpolator ${k}`); + } + } else { + interpolator = interpolateHsl(...range); + } + // Try it in lowercase too + + if (transform === 'sqrt') { + return scaleSequentialSqrt(interpolator); + } else if (transform === 'log') { + return scaleSequentialLog(interpolator); + } else { + return scaleSequential(interpolator); + } +} + export class Color< ChannelType extends DS.ColorScaleChannel = DS.ColorScaleChannel, - Input extends DS.NumberIn | DS.DateIn | DS.CategoryIn = DS.NumberIn | DS.DateIn | DS.CategoryIn -> extends ScaledAesthetic< - ChannelType, - Input, - DS.ColorOut -> { + Input extends DS.NumberIn | DS.DateIn | DS.CategoryIn = + | DS.NumberIn + | DS.DateIn + | DS.CategoryIn, +> extends ScaledAesthetic { protected _func?: (d: Input['domainType']) => string; public _texture_buffer: Uint8Array | null = null; public texture_type = 'uint8'; public default_constant = '#CC5500'; default_transform: DS.Transform = 'linear'; - - constructor(encoding: ChannelType | null, scatterplot: Scatterplot, map: TextureSet, id:string) { + constructor( + encoding: ChannelType | null, + scatterplot: Scatterplot, + map: TextureSet, + id: string, + ) { super(encoding, scatterplot, map, id); + + if (this.categorical) { + this.populateCategoricalScale(); + } else { + this._scale = getSequentialScale(this.range, this.transform); + this._scale.domain(this.domain as [number, number] | [Date, Date]); + } + this.encoding = encoding; if (encoding) { if (isConstantChannel(encoding)) { - return + return; } if (encoding['range']) { this.encode_for_textures(encoding['range']); this.post_to_regl_buffer(); } else { - throw new Error("Unexpected color encoding -- must have range." + JSON.stringify(encoding)) + throw new Error( + 'Unexpected color encoding -- must have range.' + + JSON.stringify(encoding), + ); } } if (this.scale.range().length === 0) { - throw new Error("Color scale has no range.") + throw new Error('Color scale has no range.'); } } - + get default_range(): [string, string] { - return ["white", "blue"]; + return ['white', 'blue']; } - protected categoricalRange() : string[] { + protected categoricalRange(): string[] { if (this.encoding && this.encoding['range']) { if (typeof this.encoding['range'] === 'string') { return [...schemes[this.encoding['range']]]; @@ -192,6 +238,14 @@ export class Color< return color_palettes.viridis; } + get range(): string | [string, string] { + if (this.encoding && this.encoding['range']) { + return this.encoding['range'] as [string, string] | string; + } else { + return this.default_range; + } + } + get use_map_on_regl() { // Always use a map for colors. return 1 as const; @@ -204,87 +258,24 @@ export class Color< } } - apply(v: Datum) : string { + apply(v: Datum): string { if (this.encoding === null) { - return this.default_constant + return this.default_constant; } if (isConstantChannel(this.encoding)) { - return this.encoding.constant + return this.encoding.constant; } - const scale = this.scale as ScaleOrdinal - return scale(v[this.field] as Input['domainType']) + const scale = this.scale as ScaleOrdinal; + return scale(v[this.field] as Input['domainType']); } - // get mmscale() { - // if (this._scale) { - // return this._scale; - // } - - // const range = this.range; - - // function capitalize(r: string) { - // // TODO: this can't be right for RdBu, etc. and - // // aso for ylorrd. - // if (r === 'ylorrd') { - // return 'YlOrRd'; - // } - // return r.charAt(0).toUpperCase() + r.slice(1); - // } - - // if (this.is_dictionary()) { - // const scale = scaleOrdinal().domain(this.domain); - // if (typeof range === 'string' && schemes[range]) { - // const dictionary = this.arrow_column().data[0] - // .dictionary as Vector; - // if (dictionary === null) { - // throw new Error('Dictionary is null'); - // } - // const keys = dictionary.toArray() as unknown as string[]; - // return (this._scale = scaleOrdinal() - // .range(schemes[range]) - // .domain(keys)); - // } else { - // return (this._scale = scale.range(this.range)); - // } - // } - - // // If not a dictionary, it's a different type. - // if (typeof range == 'string') { - // // Convert range from 'viridis' to InterpolateViridis. - // const k = ('interpolate' + capitalize(range)) as keyof d3Chromatic; - // const interpolator = d3Chromatic[ - // k - // ] as typeof d3Chromatic.interpolateViridis; - // if (interpolator !== undefined) { - // // linear maps to nothing, but. - // // scaleLinear, and scaleLog but - // // scaleSequential and scaleSequentialLog. - // if (this.transform === 'sqrt') { - // return (this._scale = scaleSequentialPow(interpolator) - // .exponent(0.5) - // .domain(this.domain)); - // } else if (this.transform === 'log') { - // return (this._scale = scaleSequentialLog(interpolator).domain( - // this.domain - // )); - // } else { - // return (this._scale = scaleSequential(interpolator).domain( - // this.domain - // )); - // } - // } - // } - // } - - toGLType(color: string) { const { r, g, b } = rgb(color); return [r / 255, g / 255, b / 255] as [number, number, number]; } encode_for_textures(range: string | [string, string] | string[]): void { - if (Array.isArray(range)) { const key = range.join('/'); if (color_palettes[key]) { @@ -298,7 +289,9 @@ export class Color< // We need to find the integer identifiers for each of // the values in the domain. const vec = (this.column as Vector>).data[0]; - const data_values = (vec.dictionary as Vector).toArray() as unknown as Input['domainType'][];; + const data_values = ( + vec.dictionary as Vector + ).toArray() as unknown as Input['domainType'][]; const dict_values: Map = new Map(); let i = 0; for (const val of data_values) { @@ -326,6 +319,8 @@ export class Color< this.texture_buffer.set(color_palettes[range]); return; } - throw new Error(`request range of ${range} for color ${this.field} unknown`); + throw new Error( + `request range of ${range} for color ${this.field} unknown`, + ); } } diff --git a/src/aesthetics/ScaledAesthetic.ts b/src/aesthetics/ScaledAesthetic.ts index a0fe097a5..51139ebd7 100644 --- a/src/aesthetics/ScaledAesthetic.ts +++ b/src/aesthetics/ScaledAesthetic.ts @@ -12,8 +12,9 @@ import { scaleOrdinal, ScaleOrdinal, scaleBand, + ScaleSequential, } from 'd3-scale'; -import { isConstantChannel, isTransform } from '../typing'; +import { isConstantChannel } from '../typing'; import { Dictionary, Int32, Type, Utf8, Vector } from 'apache-arrow'; export const scales = { @@ -33,6 +34,7 @@ export abstract class ScaledAesthetic< protected _scale: | ScaleContinuousNumeric | ScaleOrdinal + | ScaleSequential | null = null; public default_transform: DS.Transform = 'linear'; abstract default_range: [Output['rangeType'], Output['rangeType']]; @@ -45,58 +47,7 @@ export abstract class ScaledAesthetic< id: string, ) { super(encoding, scatterplot, aesthetic_map, id); - let scaleType: DS.Transform = this.default_transform; - if ( - encoding && - encoding['transform'] && - isTransform(encoding['transform']) - ) { - scaleType = encoding['transform']; - } - - this.categorical = false; - - if (this.is_dictionary()) { - this.categorical = true; - this.populateCategoricalScale(); - } else { - if (scaleType === 'linear') { - this._scale = scaleLinear() as ScaleContinuousNumeric< - Input['domainType'], - Output['rangeType'] - >; - } else if (scaleType === 'sqrt') { - this._scale = scaleSqrt() as ScaleContinuousNumeric< - Input['domainType'], - Output['rangeType'] - >; - } else if (scaleType === 'log') { - this._scale = scaleLog() as ScaleContinuousNumeric< - Input['domainType'], - Output['rangeType'] - >; - } else if (scaleType === 'literal') { - this._scale = scaleIdentity() as unknown as ScaleContinuousNumeric< - Input['domainType'], - Output['rangeType'] - >; - } - const domain = this.domain; - if (typeof domain[0] !== 'number') { - throw new Error( - "Domain expected to be 'number', but was" + typeof domain[0], - ); - } - this._scale = ( - this._scale as ScaleContinuousNumeric< - Input['domainType'], - Output['rangeType'], - never - > - ) - .domain(this.domain as [number, number]) - .range(this.range); - } + this.categorical = this.is_dictionary(); } protected categoricalRange(): Output['rangeType'][] { @@ -170,7 +121,9 @@ export abstract class ScaledAesthetic< return this.default_transform; } - get scale() { + get scale(): + | ScaleOrdinal + | ScaleContinuousNumeric { if (this.categorical) { return this._scale as ScaleOrdinal< Input['domainType'], @@ -254,7 +207,10 @@ export abstract class ScaledAesthetic< } } - get range(): [Output['rangeType'], Output['rangeType']] { + /** + * Returns either the inner, outer bounds OR (for color scales only) a string represented the scheme. + */ + get range(): [Output['rangeType'], Output['rangeType']] | string { if (this.encoding && this.encoding['range']) { return this.encoding['range'] as [ Output['rangeType'], @@ -277,6 +233,48 @@ abstract class OneDAesthetic< id: string, ) { super(encoding, scatterplot, aesthetic_map, id); + const scaleType = this.transform; + if (this.categorical) { + this.populateCategoricalScale(); + } else { + if (scaleType === 'linear') { + this._scale = scaleLinear() as ScaleContinuousNumeric< + Input['domainType'], + number + >; + } else if (scaleType === 'sqrt') { + this._scale = scaleSqrt() as ScaleContinuousNumeric< + Input['domainType'], + number + >; + } else if (scaleType === 'log') { + this._scale = scaleLog() as ScaleContinuousNumeric< + Input['domainType'], + number + >; + } else if (scaleType === 'literal') { + this._scale = scaleIdentity() as unknown as ScaleContinuousNumeric< + Input['domainType'], + number + >; + } + const domain = this.domain; + if (typeof domain[0] !== 'number') { + throw new Error( + "Domain expected to be 'number', but was" + typeof domain[0], + ); + } + + this._scale = ( + this._scale as ScaleContinuousNumeric< + Input['domainType'], + number, + never + > + ) + .domain(this.domain as [number, number]) + .range(this.range); + } } protected _func?: (d: Input['domainType']) => number; From 5d7a32d0acc0088209018c4fb4c19716f167bf22 Mon Sep 17 00:00:00 2001 From: Ben Schmidt Date: Tue, 11 Jun 2024 17:52:59 -0400 Subject: [PATCH 2/3] add observable10 --- dev/svelte/ColorChange.svelte | 2 +- package-lock.json | 18 ++++++++++++------ package.json | 2 +- src/aesthetics/ColorAesthetic.ts | 1 + 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/dev/svelte/ColorChange.svelte b/dev/svelte/ColorChange.svelte index 8027cd523..74c2ad5fb 100644 --- a/dev/svelte/ColorChange.svelte +++ b/dev/svelte/ColorChange.svelte @@ -2,7 +2,7 @@ export let scatterplot; let value = 'category10'; - const schemes = ['okabe', 'category10', 'dark2', 'pastel2']; + const schemes = ['okabe', 'category10', 'dark2', 'pastel2', 'observable10']; function changeColor() { scatterplot.plotAPI({ encoding: { diff --git a/package-lock.json b/package-lock.json index b686150a9..481e3d82b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "d3-interpolate": "^3.0.1", "d3-random": "^3.0.1", "d3-scale": "^4.0.2", - "d3-scale-chromatic": "^3.0.0", + "d3-scale-chromatic": "^3.1.0", "d3-selection": "^3.0.0", "d3-timer": "^3.0.1", "d3-transition": "^3.0.1", @@ -2234,7 +2234,8 @@ }, "node_modules/d3-color": { "version": "3.1.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "engines": { "node": ">=12" } @@ -2303,8 +2304,9 @@ } }, "node_modules/d3-scale-chromatic": { - "version": "3.0.0", - "license": "ISC", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" @@ -8128,7 +8130,9 @@ } }, "d3-color": { - "version": "3.1.0" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" }, "d3-dispatch": { "version": "3.0.1" @@ -8166,7 +8170,9 @@ } }, "d3-scale-chromatic": { - "version": "3.0.0", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "requires": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" diff --git a/package.json b/package.json index fb53a5241..c06a98500 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "d3-interpolate": "^3.0.1", "d3-random": "^3.0.1", "d3-scale": "^4.0.2", - "d3-scale-chromatic": "^3.0.0", + "d3-scale-chromatic": "^3.1.0", "d3-selection": "^3.0.0", "d3-timer": "^3.0.1", "d3-transition": "^3.0.1", diff --git a/src/aesthetics/ColorAesthetic.ts b/src/aesthetics/ColorAesthetic.ts index 51281a2f8..84a623a58 100644 --- a/src/aesthetics/ColorAesthetic.ts +++ b/src/aesthetics/ColorAesthetic.ts @@ -69,6 +69,7 @@ for (const schemename of [ 'schemeSet2', 'schemeSet3', 'schemeTableau10', + 'schemeObservable10', ] as const) { const colors = d3Chromatic[schemename]; From 119fc55884be39953b311b6a97ab5d59d0dbe4ea Mon Sep 17 00:00:00 2001 From: Benjamin Schmidt Date: Wed, 12 Jun 2024 21:13:30 -0400 Subject: [PATCH 3/3] Update package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index c06a98500..3372ce4e7 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "d3-timer": "^3.0.1", "d3-transition": "^3.0.1", "d3-zoom": "^3.0.0", - "deepscatter": "^3.0.0-next.23", "glsl-easings": "^1.0.0", "glsl-fast-gaussian-blur": "^1.0.2", "glsl-read-float": "^1.1.0",