create a custom vega.transform in vega-lite

I’d like to extend Vega’s transform map with a new transform class, but I’m running into some difficulties. Here is what I have so far. I’ve sketched out my intended roadmap in the comments, but I’m having some difficulty even loading the Vega transform into Vega-lite. I’ve found Vega-lite’s typescript interface, but I’m not sure how to register my transform.

<!doctype html>
<html>
  <head>
    <title>Too Much Data</title>
    <meta charset="utf-8" />

    <!--
    <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
    -->

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/vega.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/vega-lite.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/vega-embed.js"></script>

    <style media="screen">
    h1 {
        text-align: center;
        font-family: Georgia, serif
    }
    #vis {
        width: 100%;
    }
    </style>
  </head>
  <body>
    <h1>Too Much Data</h1>
    <!-- Container for the visualization -->
    <div id="vis"></div>

    <script>
      // Assign the specification to a local variable vlSpec.
      var vlSpec =
        { $schema: "https://vega.github.io/schema/vega-lite/v5.json"
        , data:
          {"values":
            [{"time": "2023-08-31 15:12:40", "data": 265.1037232391961, "category": 1}, {"time": "2023-08-31 15:15:26", "data": 989.2391954577464, "category": 1}, {"time": "2023-08-31 15:15:29", "data": 426.3748533977788, "category": 0}, {"time": "2023-08-31 15:18:11", "data": 563.7725296786067, "category": 1}, {"time": "2023-08-31 15:21:25", "data": 322.4493083566362, "category": 0}, {"time": "2023-08-31 15:24:54", "data": 822.6771646740021, "category": 1}, {"time": "2023-08-31 15:28:10", "data": 294.37404484299054, "category": 1}, {"time": "2023-08-31 15:28:22", "data": 838.7462086185608, "category": 0}, {"time": "2023-08-31 15:35:58", "data": 961.3893188770259, "category": 0}, {"time": "2023-08-31 15:36:07", "data": 241.45631836625802, "category": 0}, {"time": "2023-08-31 15:49:33", "data": 191.96326506124362, "category": 0}, {"time": "2023-08-31 15:51:15", "data": 450.50733664623965, "category": 1}, {"time": "2023-08-31 15:56:35", "data": 390.5921971631632, "category": 1}, {"time": "2023-08-31 15:57:52", "data": 829.8364876130439, "category": 1}, {"time": "2023-08-31 16:01:06", "data": 996.0349700996576, "category": 0}, {"time": "2023-08-31 16:02:36", "data": 78.24722444300802, "category": 0}, {"time": "2023-08-31 16:14:05", "data": 942.3350040849994, "category": 0}, {"time": "2023-08-31 16:14:56", "data": 860.58714895142, "category": 1}, {"time": "2023-08-31 16:15:08", "data": 515.199102407516, "category": 1}, {"time": "2023-08-31 16:23:20", "data": 166.05721829849873, "category": 1}, {"time": "2023-08-31 16:30:05", "data": 439.73137493646266, "category": 0}, {"time": "2023-08-31 16:32:31", "data": 869.245076742056, "category": 0}, {"time": "2023-08-31 16:35:48", "data": 480.50968063008304, "category": 1}, {"time": "2023-08-31 16:37:50", "data": 476.877035209344, "category": 1}, {"time": "2023-08-31 16:39:36", "data": 733.3017448826324, "category": 0}, {"time": "2023-08-31 16:44:17", "data": 636.686519092496, "category": 1}, {"time": "2023-08-31 16:45:08", "data": 694.5261775005811, "category": 0}, {"time": "2023-08-31 16:51:36", "data": 695.7401884245502, "category": 0}, {"time": "2023-08-31 16:55:29", "data": 570.0935946720598, "category": 1}, {"time": "2023-08-31 16:57:05", "data": 277.22052647262717, "category": 0}, {"time": "2023-08-31 16:58:27", "data": 480.36926264607274, "category": 1}, {"time": "2023-08-31 17:02:34", "data": 893.3698570026319, "category": 1}, {"time": "2023-08-31 17:05:32", "data": 236.71895124154685, "category": 1}, {"time": "2023-08-31 17:08:46", "data": 573.0841835923452, "category": 0}, {"time": "2023-08-31 17:14:37", "data": 191.7254918774728, "category": 0}, {"time": "2023-08-31 17:16:43", "data": 94.93763899240804, "category": 1}, {"time": "2023-08-31 17:24:40", "data": 936.4038465823089, "category": 0}, {"time": "2023-08-31 17:31:09", "data": 390.84825100994567, "category": 1}, {"time": "2023-08-31 17:35:35", "data": 14.48187309843274, "category": 0}, {"time": "2023-08-31 17:35:47", "data": 443.05398617944593, "category": 1}, {"time": "2023-08-31 17:40:44", "data": 30.0828399028229, "category": 0}, {"time": "2023-08-31 17:48:33", "data": 768.0549896500464, "category": 1}, {"time": "2023-08-31 17:53:29", "data": 71.57068127924227, "category": 0}, {"time": "2023-08-31 18:04:56", "data": 594.7138236213322, "category": 0}, {"time": "2023-08-31 18:06:44", "data": 29.21370270526036, "category": 0}, {"time": "2023-08-31 18:28:22", "data": 852.7093808483378, "category": 1}, {"time": "2023-08-31 18:30:01", "data": 576.9728506525139, "category": 1}, {"time": "2023-08-31 18:31:41", "data": 968.1882202042807, "category": 1}, {"time": "2023-08-31 18:31:51", "data": 185.6873327854428, "category": 1}, {"time": "2023-08-31 18:33:31", "data": 258.211113709635, "category": 0}, {"time": "2023-08-31 18:36:36", "data": 641.264570256715, "category": 1}, {"time": "2023-08-31 18:39:52", "data": 717.6143367808544, "category": 1}, {"time": "2023-08-31 18:39:52", "data": 191.4611806426172, "category": 1}, {"time": "2023-08-31 18:41:38", "data": 136.9116350629923, "category": 0}, {"time": "2023-08-31 18:57:48", "data": 62.11343548023751, "category": 1}, {"time": "2023-08-31 18:58:26", "data": 529.5089127094398, "category": 0}, {"time": "2023-08-31 19:07:54", "data": 153.13269404700824, "category": 1}, {"time": "2023-08-31 19:09:17", "data": 705.4049459845114, "category": 0}, {"time": "2023-08-31 19:11:07", "data": 300.90132125121005, "category": 1}, {"time": "2023-08-31 19:20:25", "data": 946.4725291504993, "category": 1}, {"time": "2023-08-31 19:23:48", "data": 319.04133613813724, "category": 1}, {"time": "2023-08-31 19:24:25", "data": 464.2923297748929, "category": 1}, {"time": "2023-08-31 19:28:02", "data": 836.436678063193, "category": 1}, {"time": "2023-08-31 19:28:08", "data": 5.992853044164859, "category": 1}, {"time": "2023-08-31 19:40:01", "data": 873.6847072580948, "category": 1}, {"time": "2023-08-31 19:43:41", "data": 431.0286183407737, "category": 1}, {"time": "2023-08-31 19:51:22", "data": 396.43260404732825, "category": 0}, {"time": "2023-08-31 19:54:08", "data": 575.9715221353141, "category": 0}, {"time": "2023-08-31 19:55:53", "data": 44.016217670442614, "category": 0}, {"time": "2023-08-31 19:58:14", "data": 988.9639046666363, "category": 1}, {"time": "2023-08-31 20:05:53", "data": 742.2798696276691, "category": 0}, {"time": "2023-08-31 20:07:13", "data": 982.7119961613008, "category": 0}, {"time": "2023-08-31 20:15:20", "data": 976.3381077100345, "category": 1}, {"time": "2023-08-31 20:20:15", "data": 498.5276910780252, "category": 0}, {"time": "2023-08-31 20:22:29", "data": 301.4863894174468, "category": 1}, {"time": "2023-08-31 20:31:03", "data": 232.56452406666895, "category": 1}, {"time": "2023-08-31 20:33:52", "data": 694.171014904713, "category": 1}, {"time": "2023-08-31 20:35:45", "data": 102.79567934930212, "category": 1}, {"time": "2023-08-31 20:47:32", "data": 431.64822699883376, "category": 1}, {"time": "2023-08-31 20:55:19", "data": 683.217576875891, "category": 0}, {"time": "2023-08-31 20:55:36", "data": 879.5945045918183, "category": 1}, {"time": "2023-08-31 21:04:28", "data": 164.6834561802648, "category": 1}, {"time": "2023-08-31 21:06:04", "data": 22.588620229922583, "category": 1}, {"time": "2023-08-31 21:07:10", "data": 757.0796861192514, "category": 1}, {"time": "2023-08-31 21:23:43", "data": 848.456892794343, "category": 1}, {"time": "2023-08-31 21:34:38", "data": 447.89147371830785, "category": 1}, {"time": "2023-08-31 21:45:30", "data": 862.3116375036777, "category": 1}, {"time": "2023-08-31 21:47:00", "data": 967.0312319533795, "category": 0}, {"time": "2023-08-31 21:47:56", "data": 966.4938018703745, "category": 1}, {"time": "2023-08-31 21:49:45", "data": 890.2567189914545, "category": 0}, {"time": "2023-08-31 21:55:40", "data": 362.80312104639677, "category": 1}, {"time": "2023-08-31 21:58:55", "data": 834.7469369912607, "category": 1}, {"time": "2023-08-31 22:01:02", "data": 584.1447613550432, "category": 1}, {"time": "2023-08-31 22:01:06", "data": 82.66592460479994, "category": 1}, {"time": "2023-08-31 22:02:00", "data": 332.67959271479384, "category": 0}, {"time": "2023-08-31 22:02:32", "data": 316.51081491367347, "category": 0}, {"time": "2023-08-31 22:08:31", "data": 336.10098602094985, "category": 1}, {"time": "2023-08-31 22:18:52", "data": 873.7313013506864, "category": 0}, {"time": "2023-08-31 22:19:24", "data": 312.42947148514776, "category": 1}, {"time": "2023-08-31 22:28:48", "data": 582.8096654568776, "category": 1}]
          }
        , params:
          [ {name: "test", expr: "span(grid_time)/1000"}
          ]
        , transform:
          [ {filter: "datum.data > 0"}
          ]
        , title: "Too Much Data"
        , config: { font: "monospace" }
        , width: "container"
        , layer:
          [ { params:
              [ { name: "grid"
                , bind: "scales"
                , select:
                  { type: "interval"
                  , encodings: ["x"]
                  , on: "[mousedown[!event.shiftKey], mouseup] > mousemove"
                  , translate: "[mousedown[!event.shiftKey], mouseup] > mousemove!"
                  }
                }
              , { name: "brush"
                , select:
                  { type: "interval"
                  , encodings: ["x"]
                  , on: "[mousedown[event.shiftKey], mouseup] > mousemove"
                  , translate: "[mousedown[event.shiftKey], mouseup] > mousemove!"
                  }
                }
              ]
            , mark: "point"
            , encoding:
              { x: {field: "time", type: "temporal"}
              , y: {field: "data", type: "quantitative"}
              , color: {field: "category", type: "nominal"}
              }
            }
          , { data: {values: [{}]}
            , mark:
              { type: "rect"
              , x: 10
              , y: 10
              , x2: 182
              , y2: 100
              , fillOpacity: 0.05
              , stroke: "darkgrey"
              , strokeWidth: 2
              , fill: "azure"
              }
            }
          , { data: {values: [{}]}
            , mark:
              { type: "rect"
              , fillOpacity: 0.05
              , stroke: "darkgrey"
              , strokeWidth: 2
              , fill: "azure"
              }
            , encoding:
              { "x": {"value": 200}
              , "y": {"value": 120}
              , "x2": {"value": 240}
              , "y2": {"value": 160}
              }
            }
          , { //data: {values: [{time: 1693521211000}]}
              transform:
              [ { filter: {param: "brush", empty: true}}
              , { window: [{op: "row_number", as: "row_number"}]}
              , { extent: "time", param: "time_extent"}
              , { aggregate:
                  [ {op: "count", as: "count"}
                  //, {op: "values", field: "time", as: "values"}
                  //, {op: "argmax", field: "time", as: "argmax"}
                  //, {op: "argmin", field: "time", as: "argmin"}
                  ]
                }
              //, { type: "default", values: []}
              , { default: []}
              ]
            , mark:
              { type: "text"
              , x: 15
              , y: 15
              , align: "left"
              , baseline: "top"
              , text: {expr: "warn(true ? 'foo' : 'bar')"}
              }
            //, encoding:
            //  { text: {field: "data", type: "nominal"}
            //  , y: {field: "row_number", type: "ordinal", axis: null}
            //  }
            }
          ]
        }

      // Insert a predetermined row only when the dataset is empty.
      // This can happen because aggregate does not understand empty sets,
      // i.e. "count" goes from 3,2,1,<empty dataset> rather than 3,2,1,0.
      class Default extends vega.Transform {
        Definition =
          { type: "Filter"
          , metadata: {changes: true}
          , params:
            [ { name: "values", type: "any", array: true }
            ]
          }

        constructor(params) {
          // todo: provide initial value
          // { count: 0, cache: null } ??
          super(null, params)
        }

        transform(params, pulse) {
          // rough draft (probably wrong):
          // 1. For each add, increment this.value.count
          // 2. For each rem, decrement this.value.count
          // 3. if this.value.count == 0 {
          //      if this.value.cache == null {
          //        // need to lookup null/undefined/==/===
          //        // need to console.log some tuples to learn the
          //        // expected ingest format
          //        this.value.cache = ingest(params.values)
          //      }
          //      pulse.add.push(this.value.cache)
          //    } else {
          //      // pretty sure I have to remove the default, i.e. will
          //      // persist during subsequent pulses
          //      pulse.rem.push(this.value.cache)
          //    }
          // 4. Handle params.modified() ...
          //    reset count to 0 (??) - before step 3, then after step 3:
          //    pulse.visit(pulse.REFLOW, ...) // anything except ADD/REM/MOD
          //    For each, increment this.value.count
          // Will these count strategies actually work? No idea. Need to
          // console.log all changes to count.
          // ???
          const out = pulse.fork(pulse.ALL)
          pulse.visit(pulse.ADD, t => {
            console.log(t)
          })

          return out
        }
      }

      // problem: need to convert the vega interface to the vega-lite interface.
      vega.transforms["default"] = Default

      // Embed the visualization in the container with id `vis`
      vegaEmbed('#vis', vlSpec).then(function(result) {
      // Access the Vega view instance as result.view
      // (https://vega.github.io/vega/docs/api/view/)
      }).catch(console.error);
    </script>
  </body>
</html>