Skip to content

GSoC 2026 ‐ Sachin Pangal

Sachin Pangal edited this page Aug 22, 2026 · 3 revisions

About me

Hey! I’m Sachin Pangal, a Computer Science undergraduate at Walchand Institute of Technology, Solapur, India, graduating in 2027. I like building things that other people get to build with, like libraries, tooling, and anything where the output is a good API rather than a finished app. Outside of stdlib, I write a fair amount of code in the JavaScript ecosystem, mess around with C++ and Python, and have shipped a couple of side projects that real people actually use.

Project overview

Vega is a visualization grammar for describing charts through data, marks, scales, and encodings. stdlib’s plot namespace uses Vega as a backend to bring that capability into JavaScript, letting you build visualizations programmatically. When I started working on it, stdlib already supported parts of the Vega grammar. My work was about extending that foundation so common charts could be built end-to-end through stdlib APIs, without having to hand-write a Vega specification.

The question I kept coming back to throughout the summer was: can someone hand stdlib an ndarray and get a chart back?

Objectives

  • Finish the value-reference system so data fields, colors, and gradients can be mapped onto visual properties.
  • Implement the remaining mark typesrect, symbol, line, rule, text, area, arc, path, shape, image, trail, group — each with its own encoding, encoding set, and assertions.
  • Build a complete legend system, including type/direction/orientation enumerations.
  • Add the data transforms that charts actually depend on: extent, stack, collect, filter, formula, sample, sequence, loess, label.
  • Add layout support for composing group marks.
  • Wire it all together into end-to-end chart constructors (scatter).

Approach

The whole thing was built bottom-up, in layers, because every layer above depends on the one below being correct:

  1. Enumerations first. Every Vega property with a fixed set of legal values (interpolation methods, symbol shapes, stroke joins, legend orientations, format types, grid alignments, …) got its own package plus a matching plot/vega/base/assert/* predicate. Boring, but it means every constructor above gets validation for free.
  2. Marks next. For each mark type, four packages: the ctor, an encoding-set (the per-mark set of visual channels), an encoding (the enter/update/exit lifecycle), and assertions for both.
  3. Then the supporting grammar — legends, scales, transforms, layouts.
  4. Then charts. A user-facing chart constructor that consumes all of the above and hands back a valid Vega spec.

Each layer shipped as its own PR, which is why there are a lot of them.

Project recap

What Vega actually is

Vega is a visualization grammar. Instead of calling functions to draw things, you write a JSON document describing what the chart is, and Vega figures out how to draw it. A chart is built from a few pieces: data, transforms that reshape it, scales that turn data values into visual values, marks that get drawn, encodings that connect data fields to mark properties, and guides (axes and legends).

The nice part is that these snap together in different ways. Change a symbol mark to a rect mark and a scatter plot becomes a bar chart. Add a bin transform and it becomes a histogram. So you don't build thirty chart types — you build the pieces and combine them.

Why is this important?

Because writing that JSON by hand is painful. A full chart is hundreds of lines of nested objects, and JSON doesn't help you: type "symbal" instead of "symbol" and nothing complains, the chart just comes out wrong. Want five series? Copy the block five times.

So stdlib gives every piece of the grammar its own JavaScript constructor. You build objects, get errors when you make a mistake, and call toJSON() at the end. My project was writing the constructors that didn't exist yet — which was most of them.

Marks

What I built: ten of Vega's eleven mark types — rect, symbol, path, text, arc, area, trail, image, and rule — each with four packages: the constructor, an encoding set, an encoding, and assertions for both. group is still in review.

A mark is a shape drawn once for each row of data. Marks are the only thing that actually puts pixels on screen; everything else exists to tell marks where to go and what to look like. Between them, Vega's eleven types cover most of what charts need:

Mark What it draws Used for
rect rectangles bar, column, histogram, heatmap
symbol points scatter, bubble
line a connected path line charts
area a filled band area charts
arc pie slices pie, donut
rule straight lines error bars, reference lines
text text labels
path / shape custom SVG shapes maps, custom glyphs
image images icon plots
trail a line that changes width tapered lines
group a container for other marks small multiples

Enter, update, and exit

Each mark's encoding is split three ways, because Vega charts can change while you're looking at them. When the data updates, Vega compares old to new and sorts marks into three groups: enter (new marks), update (marks that are staying), and exit (marks whose data is gone). Each group gets its own styling, which is how you get smooth animations — fade in on enter, slide on update, fade out on exit. A static chart only really uses update, but the model has to support all three.

  • Example
    var Value = require( '@stdlib/plot/vega/value/ctor' );
    var SymbolEncodingSet = require( '@stdlib/plot/vega/mark/symbol/encoding-set' );
    var SymbolEncoding = require( '@stdlib/plot/vega/mark/symbol/encoding' );
    var SymbolMark = require( '@stdlib/plot/vega/mark/symbol/ctor' );

    var mark = new SymbolMark({
        'encode': new SymbolEncoding({
            'enter': new SymbolEncodingSet({
                'stroke': new Value({
                    'value': '#000'
                })
            }),
            'update': new SymbolEncodingSet({
                'stroke': new Value({
                    'value': 'steelblue'
                })
            }),
            'exit': new SymbolEncodingSet({
                'stroke': new Value({
                    'value': '#fff'
                })
            })
        })
    });

    console.log( mark.toJSON() );
    // => {...}

Why does each mark need its own encoding set?

Different marks have different properties. shape and size only make sense on a symbol. cornerRadius only on a rect. startAngle only on an arc.

With one shared encoding set, you could set cornerRadius on a symbol mark and nothing would stop you — Vega would just ignore it, and you'd get a chart that looks fine but isn't what you asked for. Giving each mark its own encoding set means you get an error right where you made the mistake. That's why it's four packages per mark instead of one.

Encodings and value references

What I built: the remaining properties on value/ctor, plus value/field-descriptor and its assertion.

An encoding says which data field controls which visual property — "x comes from the year field", "color comes from the category field". A value reference is the small object underneath that answers where a value comes from. Three cases:

  • a fixed value{ 'value': 'red' }, the same for every point
  • a field with a scale{ 'scale': 'colorScale', 'field': 'species' }
  • a signal — a value that can change while the chart runs

Why is this important?

Everything depends on it. Every property of every mark — position, color, size, opacity, shape — is a value reference underneath, so this had to be finished before any mark work could start.

It's also where you decide whether a chart is readable. People are good at comparing positions, decent at comparing lengths, and pretty bad at comparing colors or shapes. So the important numbers belong on x and y, and category belongs on color or shape.

Enumerations and assertions

What I built: paired packages for every fixed list of values I needed — interpolation methods, area orientations, symbol shapes, text alignments, format types, grid alignments, stroke joins, legend types/directions/orientations, stack offsets, label transform options, and layout bounds. One package returns the valid values, the other checks against them.

Why is this important?

Because Vega doesn't tell you when you get one wrong. Write "orient": "lft" instead of "left" and Vega doesn't throw, doesn't warn, doesn't log. It silently ignores the property and uses the default. Your legend shows up on the right, you assume you misread the docs, and you waste half an hour. With these, a typo throws an error on the line where you typed it.

  • Example
    var isSymbolMarkShape = require( '@stdlib/plot/vega/base/assert/is-symbol-mark-shape' );

    var bool = isSymbolMarkShape( 'circle' );
    // returns true

    bool = isSymbolMarkShape( 'beep' );
    // returns false

Legends

What I built: the legend constructor, plus the type, direction, and orientation enumerations it needs (and their assertions).

A scale turns data into visuals — it turns 'setosa' into the color #1f77b4. A legend is that same mapping drawn on screen, so the reader can go backwards: they see blue, and the legend tells them it means 'setosa'.

Why is this important?

It explains why the API looks the way it does. You don't hand a legend a list of labels and colors — you hand it the name of a scale:

var Legend = require( '@stdlib/plot/vega/legend/ctor' );

var legend = new Legend({
    'fill': 'colorScale',
    'orient': 'left'
});

console.log( legend.toJSON() );
// => {...}

The labels come from the scale's domain and the colors from its range, so there's nothing to list. It also means the legend can never disagree with the chart — the usual problem with hand-written legends, where you change a color and forget to update the key. Axes work the same way: an axis is a legend for a position scale.

Symbol utilities

What I built: twelve packages in plot/base — lists of symbol aliases and shorthands, converters between all three representations (single and array versions), and four assertions. This wasn't in my proposal; it came up while building the scatter plot.

Why is this important?

Vega wants a symbol shape as either a built-in name like 'circle' or a raw SVG path string. Nobody wants to type an SVG path.

Meanwhile, people who plot things already know the short forms from MATLAB — 'o' for circle, '+' for plus, 'x' for cross, '*' for asterisk, 's' for square. matplotlib, R, and gnuplot all use the same ones, so stdlib should accept them too.

So the layer handles three ways of naming the same thing: shorthand ('o', what people type), alias ('circle', the readable name), and shape (what Vega needs — a built-in name if one exists, otherwise an SVG path).

  • Example
    var symbol2shape = require( '@stdlib/plot/base/symbol2shape' );

    var v = symbol2shape( 'o' );
    // returns 'circle'

    v = symbol2shape( '*' );
    // returns 'M0.2,-1L-0.2,-1L-0.2,-0.34641L...Z'

    v = symbol2shape( 'beep' );
    // returns null

Transforms

What I built: the stack and label transform enumerations (merged), and constructors for formula, sequence, loess, extent, filter, sample, and label (all still in review).

A transform is a step that changes data before it gets drawn — binning, grouping, filtering, stacking.

Why is this important?

Transforms are what let chart types be combinations instead of special cases. A histogram isn't its own chart type; it's a rect mark with bin and aggregate in front of it. A stacked bar chart is a rect mark with aggregate and stack. A trend line is a line mark with loess.

Write each of those as a separate chart implementation and you write the same code over and over, and users can only make the charts you thought of. Build the transforms instead and users can combine them however they like.

Scales

What I built: band and point scales (both in review).

A scale maps a domain (your data) to a range (pixels, colors, sizes). Three flavours: numbers in and numbers out (linear, log, time), categories in and numbers out (band, point), and categories in and categories out (ordinal).

Why is this important?

The scatter plot only needed linear and ordinal (mapping each series to a color, shape, size, and opacity). Bar charts need band, and that's worth explaining.

A bar needs two numbers: where it starts and how wide it is. A band scale splits the range into equal slices with padding between them and gives you back the width of one slice — which is exactly the bar width. Without it you'd calculate bar widths by hand every time and get the padding wrong at the edges. point is the same thing with zero width, giving evenly spaced positions instead of slices, which is what categorical line charts need.

Layout

What I built: layout/bounds with its assertion, and the layout constructor (all in review).

A group mark is a container — it holds its own marks, scales, and axes, and can hold other groups. Layout arranges those groups in a grid.

Why is this important?

Together they give you small multiples: a grid of small charts that look the same and share their scales, each showing a different slice of the data. It's one of the best ways to show an extra dimension without cramming everything into one messy plot, and it's the "multidimensional data" part of my project title.

The shared scales are the important bit. If each small chart quietly gets its own y-axis, you can't compare them any more and the whole point is lost — which is why layout has to be built on group marks that can inherit scales, rather than being a grid of separate images.

Charts: the scatter plot

What I built: plot/charts/scatter/ctor — a full chart constructor that takes an ndarray and produces a renderable Vega spec, with per-series symbols, colors, sizes, opacities, edges, and a legend. Still in review.

Why is this important?

There's a mismatch to solve. stdlib stores data in ndarrays — a block of numbers, say 100 rows by 5 columns. Vega wants a list of objects, one per point, with named fields:

ndarray (100 × 5)              Vega data
                               [ { x: 0, y: 12.4 },
[[12.4, 23.1, ...],     →        { x: 1, y: 15.9 },
 [15.9, 21.8, ...],              ... ]
 ...]

Converting between the two, then building the marks, the scales for every channel, and the legend, is what the chart constructor does. It also picks sensible defaults — the category10 color scheme, and 0.6 opacity so overlapping points show density without getting too faint to see.

  • Example
    var array = require( '@stdlib/ndarray/array' );
    var random = require( '@stdlib/random/uniform' );
    var Scatterplot = require( '@stdlib/plot/charts/scatter/ctor' );

    var opts = {
        'shape': [ 1, 5 ]
    };

    var a = array( [ 10.0, 20.0, 30.0, 40.0, 50.0 ], opts );
    var b = array( [ 20.0, 30.0, 40.0, 50.0, 60.0 ], opts );

    // Five series, 100 points each:
    var y = random( [ 100, 5 ], a, b );

    var chart = new Scatterplot( y, {
        'symbols': [ 'o', 'square', '^', 'd', '*' ],
        'symbolOpacity': [ 0.6 ],
        'symbolSize': [ 100 ],
        'legend': true
    });

    chart.view( 'browser' );

Completed work

Marks

Enumerations and assertions

Legends

Symbol utilities (plot/base)

Transforms

Scales and layouts

Charts

  • plot/charts/scatter/ctor#14299

Current state

The mark layer is essentially done. Ten of eleven mark types are merged with full encoding machinery, and group is the only one left in review. Every enumeration I needed exists and is validated. The legend system is merged and working. The symbol utility layer in plot/base is fully merged.

The scatter plot works end-to-end today — you can hand it an ndarray and get a rendered chart in the browser, with legends, per-series symbols, sizes, opacities, and hollow-marker support.

What remains

  • Other chart types — bar, column, line, and histogram. Scatter showed the pattern works, so the rest follow the same shape once transforms and scales land. In short, the building blocks for plot/charts/* are still being laid, and the scope will only keep growing as more chart types get added — so this is continuing work.

Challenges and lessons learned

Learning the Vega grammar took a while. Before I could write a single constructor I had to understand how Vega actually composes a chart — marks, scales, encodings, and how they all reference each other. Reading through the spec and pulling apart example charts took a good chunk of the community bonding period, and I kept coming back to it whenever a new mark or transform behaved in a way I didn't expect.

Building bottom-up feels slow and isn't. For weeks I wrote enumeration packages — lists of strings and a predicate. It felt like nothing. But by the time I reached the marks, all the validation was already written, and I never once had to debug a rejected property.

Small PRs beat big ones. They got reviewed fast, and the feedback was specific enough to act on straight away. The few times I bundled changes together, review took longer and the discussion got muddier.

Scope estimation is hard. I planned eleven marks, legends, transforms, layouts, signals, and five chart types in twelve weeks. The marks alone took a large chunk of the summer. I'd rather have shipped a solid mark layer and one real chart than a shaky version of everything, but it's still a miss against my timeline.

Code reviews taught me a lot. A big part of what I now know about stdlib's conventions came from the feedback maintainers left on my PRs. The contributing guide lays the rules out clearly, but having someone point them out on my own code is what made them stick — and I'm grateful for how patient and detailed that feedback was.

Conclusion

I really enjoyed contributing this summer. The highlight for me was the weekly standups — a mix of genuine feedback and fun time with the mentors and the other contributors. I always came away from them knowing more than I went in with, and often about things well outside my own project.

Huge thanks to @kgryte, @Planeshifter, @batpigandme, and @gururaj1512 for the mentorship, guidance, and review feedback that was consistently more useful than it needed to be. Thanks also to everyone else in the stdlib community who reviewed PRs, answered questions, and generally made this feel like joining a project rather than completing an assignment. This was easily the most I've learned in three months, and the fact that the output is a library other people will build on top of makes it stick.

Clone this wiki locally