Chongbo NingDesign Engineer, London

Logistics infrastructure company

Components for Any Data

One component library that draws any customer's data, from the stream to the chart.

Role
Design Engineer
  • Data model, interface, front end and server design on my own, with Claude Code and Cursor
Outcome
One library serves every customer. Each component declares the shape of data it needs, and customers connect their own streams by choosing fields.

What this shows

  • Front-end architecture
  • Data contracts
  • Data-binding UX

The preview panel, rebuilt for this case study

The product is confidential, so this uses mock data and is for demonstration only.

Bar chart
CategoryValue
Leeds364
Bristol190
Glasgow160
Cardiff122
Norwich41

Try north_depot.scans: same meaning, different field names.

Rows as they arrive

First 3 of 1,840

categorytownvalueparcelsdatedelivered_ontextdriver_id
Leeds1282026-09-28D-104
Bristol1022026-09-28D-221
Glasgow872026-09-28D-318

Part 1: The model

Different shapes · A shape, not field names · One pipeline

01.1

Every customer's data has a different shape

Our platform carries customers' operational data as live streams, and customers build their own products on it, picking components from our component store. But the components only showed fixed values in their settings, so no customer could see what their own data would look like in one. One set of sample data couldn't cover every case either, as each customer names and formats the same data differently.

So I started from two questions: Can we connect live streams to the store's components? What does a component need to know about its data?

  • category
  • value
  • no role

courier_app.deliveries

1,840 rows

town (category)
Leeds
parcels (value)
128
delivered_on
2026-09-28
driver_id
D-104

north_depot.scans

26,708 rows

scan_id
S-88120
site (category)
York
qty (value)
34
eta_ts
1790583600

partner_carrier.feed

412 rows

Ref
PC-2231
City (category)
Dundee
ArrDate
30/09/2026
Status
Arrived
no value
no number field

01.2

A component promises a shape, not field names

I based the model on ECharts' dataset and encode: a chart type has fixed dimensions it draws, and encode maps the data's columns onto them. A contract adds what ECharts leaves out: each role's type, and which are required.

Each component declares one contract, and a stream can feed it once its fields map onto that contract's roles. Components that draw the same shape share one, so a bar chart, a donut, a treemap and a table all declare the same contract. Sample data fits it too, so a component explains itself before anything is connected.

Contracttotals-by-category

  • categorytextrequired
  • valuenumberrequired

Declared by

  • Bar
  • Donut
  • Treemap
  • Table

Sample datafits the same contract

contracts/totals-by-category.ts
// A contract names the roles a
// component draws, not the fields a
// stream happens to have.
export const totalsByCategory =
  defineContract({
    id: "totals-by-category",
    roles: {
      category: {
        type: "text",
        required: true,
      },
      value: {
        type: "number",
        required: true,
      },
    },
  });

// Bar, donut, treemap and table all
// declare the same contract.
export const BarChart = component({
  contract: totalsByCategory,
  settings: { sort: "desc" },
});

01.3

One pipeline from stream to chart

An adapter turns each source system's format into flat rows, a binding maps the stream's fields onto the contract's roles, the contract checks their types, and the component draws the roles. Components never subscribe to streams, and settings hold configuration only, never data.

Charts and tables read a stream, filters and search read the page, and forms wait for the user.

  1. Sourceits own format

    {"evt":"delivered",
     "payload":{
      "town":"Leeds",
      "parcels":"128"}}
  2. Adapterflat rows

    townLeeds

    parcels128

  3. Bindingfields onto roles

    category← town

    value← parcels

  4. Contractroles and types

    categorytext · required

    valuenumber · required

  5. Componentdraws the roles

  • Charts, tablesread a stream
  • Filters, searchread the page
  • Formswait for the user

Part 2: The interface

Grouped by fit · Fields like chart axes

02.1

Streams grouped by fit, judged on the server

The first answer listed every stream by row count. A test organisation already had 74, and most didn't fit: users only found out after reading the rows.

I grouped the list by fit instead. The browser can't judge fit, as a stream record carries no field names, so I designed one endpoint on the server to judge it.

First answer · by row count

74 streams, one list

  1. telematics.pings412,880 rows
  2. gate_cameras.events96,210 rows
  3. north_depot.scans26,708 rows
  4. finance.invoices9,120 rows
  5. driver_shifts.log4,455 rows
  6. 69 more

Grouped by fit

Fits

  1. courier_app.deliveriesFits
  2. north_depot.scansFits

Partly fits

  1. partner_carrier.feed1 of 2 fields
  2. driver_shifts.log1 of 2 fields

Other

  1. PickerAsks which streams fit
  2. One endpointFits · Partly fits · Other
  3. Phase 1The newest entryIts field names, from a read the platform already makes.
  4. Phase 2A stored field profileIndexed by field: the same cost for 50 streams or 5,000.

02.2

Each field picked like a chart axis

The people connecting data aren't always engineers, so connecting fields can't feel like configuration. Each role has a dropdown of the stream's fields, the way you pick a chart's axes.

The store suggests a field for each role from aliases and industry terms. The stream list and the field suggestions share one matcher, so a stream marked "Fits" always fills its fields.

Bar chart

Bar chart
CategoryValue
Wakefield89
Leeds North88
York63
Harrogate22
Selby12

Part 3: Outcome

Takeaway

03.1

Takeaway

  1. The store couldn't preview a customer's data, which looked like an interface problem. The cause was in the data model, so I defined the shape each component needs before designing the panel.
  2. Judging fit on the server lets users see which streams fit before they pick, however many there are.
  3. Every layer has one job, so a new source system needs a new adapter, and nothing else changes.