← Bridge lesson
Lab

Bridge

// What every surface can draw: one bar, and a figure around the bars.
interface Surface {
  bar(label: string, value: number, largest: number): string;
  figure(title: string, bars: string[], note: string): string;
}

const escapeXml = (text: string) =>
  text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');

// Plain text for the weekly digest: a bar is up to 20 blocks.
class TextSurface implements Surface {
  bar(label: string, value: number, largest: number): string {
    const blocks = '█'.repeat(Math.floor((value * 20) / largest));
    return `${label} ${blocks} ${value}`;
  }
  figure(title: string, bars: string[], note: string): string {
    return [title, ...bars, note].join('\n');
  }
}

// SVG for the dashboard: a bar is up to 200 units wide.
class SvgSurface implements Surface {
  bar(label: string, value: number, largest: number): string {
    const width = Math.floor((value * 200) / largest);
    const tip = `${escapeXml(label)}: ${value}`;
    return `<rect width="${width}"><title>${tip}</title></rect>`;
  }
  figure(title: string, bars: string[], note: string): string {
    const head = `<title>${escapeXml(title)}</title>`;
    return `<svg>${head}${bars.join('')}<text>${escapeXml(note)}</text></svg>`;
  }
}

type Point = { label: string; value: number };

// Top categories decides what to show: the three biggest, largest first.
class TopCategories {
  private readonly surface: Surface;
  constructor(surface: Surface) {
    this.surface = surface;
  }
  draw(title: string, points: Point[]): string {
    const top = [...points].sort((a, b) => b.value - a.value).slice(0, 3);
    const largest = top[0].value;
    const bars = top.map((p) => this.surface.bar(p.label, p.value, largest));
    const note = `${top.length} of ${points.length} shown`;
    return this.surface.figure(`Top categories: ${title}`, bars, note);
  }
}

// Weekly trend decides what to show: every week in order, and the change.
class WeeklyTrend {
  private readonly surface: Surface;
  constructor(surface: Surface) {
    this.surface = surface;
  }
  draw(title: string, points: Point[]): string {
    const largest = Math.max(...points.map((p) => p.value));
    const bars = points.map((p) => this.surface.bar(p.label, p.value, largest));
    const change = points[points.length - 1].value - points[0].value;
    const note = `Change: ${change >= 0 ? '+' : ''}${change}`;
    return this.surface.figure(`Weekly trend: ${title}`, bars, note);
  }
}

// The caller connects one chart to one surface.
const categories: Point[] = [
  { label: 'Billing & refunds', value: 12 },
  { label: 'Login', value: 30 },
  { label: 'Exports', value: 7 },
  { label: 'Mobile', value: 18 },
];
const weeks: Point[] = [
  { label: 'W1', value: 20 },
  { label: 'W2', value: 26 },
  { label: 'W3', value: 21 },
];
const svg = new SvgSurface();
const text = new TextSurface();
const title = 'Support tickets';
const dashboard = new TopCategories(svg).draw(title, categories);
const digest = new TopCategories(text).draw(title, categories);
const trend = new WeeklyTrend(text).draw(title, weeks);
console.log(dashboard);
console.log(digest);
console.log(trend);
Inspect recorded facts and calls
Which data appears?
Top categoriesTopCategories
Weekly trendWeeklyTrend
plain factsSurface
How is it drawn?
SVG dashboardSvgSurface
Text digestTextSurface
Recorded dataawaiting values

No complete input collection observed yet.

Received across the boundary bar(label, value, largest) Awaiting a display call
Observed representationNo result yet

The display’s returned value appears when it is recorded.

Play or step to follow the recorded selection and display calls.

Role cards explain the design. Highlighted roles come from the recording; “called” cells show observed delegation, not all possible combinations.