Created simple plotting API for Vega Lite in TypeScript

plot() .mark('line') .x({ field: 'date', type: 'temporal' }) .y('price') .color('stock_symbol') .data(price_data) .inspect()

and Crystal

plot .mark(:line) .x(field: "date", type: :temporal) .y("price") .color("stock_symbol") .data(price_data) .inspect()

Full sources at the end of the page.

Informal comparison

I know Ruby and JavaScript / TypeScrpt quite well and decided to try Crystal. It's very good, there are very few things that in my opinion need to be improved.

Working with Crystal - flexible and nicely designed standard library and runtime with clean error messages feels really good - compared to node.js or Browser with TypeScript.

Enums - very bad decision, it should be deprecated and killed as soon as possible, replaced with literal types. Enums are ancient way to achieve type safety by restricting values to some set. It should be handled by type system.

Data manipulation could be better. Crystal has Maps, Tuples, NamedTuples, Records, Struct, yet it feels less flexible and powerfull than Array and Object (including nested Objects) constructors and destructors in TypeScript.

TypeScript advantages

Excellent type safety , all the flexible stuff is checked and validated.

, all the flexible stuff is checked and validated. Excellent type inference and autocasting helps keep code clean from type noise.

First-class functions , without proper overloading or multiple dispatch, but still good enough.

, without proper overloading or multiple dispatch, but still good enough. Flexible and simple data manipulations with array and map constructors and destructors.

with array and map constructors and destructors. Flexible and safe metaprogramming with data and functions.

Very flexible type system.

type system. Clean and compact code, not best, but good enough.

Flexibility to work in Object Oriented or Functional style.

Disadvantages:

No pattern matching.

No easy function or method overloading.

async functions, you always need to know if function is ordinary or async.

Bad error messages.

Escaping async errors.

Weak runtime without multicore and huge memory consumption.

Weak and fundamentally broken standard library.

Crystal advantages

Excellent type safety , all the flexible stuff is checked and validated.

, all the flexible stuff is checked and validated. Good enough type inference and autocasting, helps keep code clean from type noise (not as good as in TS).

Pattern matching.

Flexible and safe metaprogramming with macros.

Nice and compact code .

. Method overloading based on types.

Good error messages.

Good, clean and rich standard library .

. Good runtime with small memory footprint and parallelism.

Disadvantages:

No first class functions, there's distinction between methods and proc.

No literal types, instead Enums are used, which are harder, inconvenient and broken in some cases.

are used, which are harder, inconvenient and broken in some cases. Can't be used in functional style, Object Oriented style only.

Data manipulation could be better, no flexible constructors and destructors for data manipulation.

Sources

TypeScript

type something = any export type Mark = 'point' | 'circle' | 'line' | 'bar' | 'tick' export type FieldType = 'nominal' | 'ordinal' | 'quantitative' | 'temporal' export type Channel = 'x' | 'y' | 'size' | 'color' export interface Axis { title?: string | boolean labels?: boolean } export interface Encoding { field: string type: FieldType axis?: Axis } export interface VegaSpec { mark?: Mark encoding?: { [key in Channel]?: Encoding } } function build_encoding_helper(channel: Channel) { return function encoding_helper(this: Plot, arg: { field: string, type?: FieldType, title?: string | boolean, labels?: boolean } | string) { const { field, type, title, labels } = typeof arg == 'string' ? { field: arg, type: undefined, title: undefined, labels: undefined, } : arg const axis: Axis = { title: false } if (title !== undefined) axis.title = title if (labels !== undefined) axis.labels = labels this.spec.encoding = this.spec.encoding || {} this.spec.encoding[channel] = { field, type: type || 'quantitative', axis } return this } } export type RenderFn = (spec: VegaSpec, data: something) => Promise<void> export class Plot { spec: VegaSpec = {} _data?: something constructor( protected readonly render?: RenderFn ) {} mark(mark: Mark) { this.spec.mark = mark return this } x = build_encoding_helper('x') y = build_encoding_helper('y') color = build_encoding_helper('color') size = build_encoding_helper('size') data(data : something) { this._data = data return this } inspect(): Promise<void> { if (!this.render) throw new Error("Render not defined") return this.render(this.spec, this._data) } } const plot = () => new Plot(async (spec, data) => console.log({ spec, data })) const price_data = "price data" // Usage -------------------------------------------------------------------------- plot() .mark('line') .x({ field: 'date', type: 'temporal' }) .y('price') .color('stock_symbol') .data(price_data) .inspect()

Crystal