Back to Blog
WPFAIdotnetCSharpDataVisualizationXAMLfintechUIDesign

Building an Interactive Cohort Analysis Chart in WPF and What Happens When You Ask AI to Do It Instead

A cohort analysis matrix sounds simple it's just a grid. But keyboard navigation, live cell updates, row/column highlighting, and a buy/sell popup make it one of those UI components that exposes exactly where AI-assisted development hits its limits. Here's how I built it in WPF, and what happened when I asked AI to reproduce it.

5 May 202620 min read

Cohort analysis is a staple of financial and product analytics — you group data by time period and track how each cohort behaves across subsequent periods. The visualization is a matrix: cohorts on the rows, time offsets on the columns, values in the cells. Sounds simple. The implementation is anything but, especially when you add interactivity.

I built this cohort analysis chart for financial trading analytics — a 13×14 interactive matrix with keyboard navigation, live timer cells, row/column highlighting on selection, and a detailed buy/sell order popup. Then, as an experiment, I asked AI to generate the same component from scratch. The results revealed something important about where AI-assisted development genuinely helps and where domain knowledge is irreplaceable. The full source is on GitHub.

The Component Requirements

Before writing any code, the requirements for this chart were specific:

1. A 13×14 matrix grid where each cell represents a cohort/period intersection
2. Mouse click to select a cell — highlights the entire row and column headers
3. Keyboard navigation with Arrow keys to move between cells
4. Enter key to open a detailed popup for the selected cell
5. Some cells contain live updating timer values (countdown/elapsed)
6. The popup shows buy/sell order details for the selected cohort intersection
7. Clean MVVM architecture so the UI and data are properly separated
8. Smooth visual feedback — selected state, hover state, header highlighting

Every one of these requirements interacts with the others in non-obvious ways. The keyboard navigation has to know which cell is selected. The header highlighting has to respond to the same selection. The popup has to receive the right data context. This is the category of UI component where getting the architecture right upfront saves days of rework.

The Data Model — Start with the Cell

Every cell in the matrix is a MatrixElement — a model that holds the cell's data, its position in the grid, its display state, and whether it contains a live timer. Getting this model right is the foundation everything else builds on.

// MatrixElement.cs — the fundamental cell model
public class MatrixElement : INotifyPropertyChanged
{
    private bool   _isSelected;
    private bool   _isRowHighlighted;
    private bool   _isColumnHighlighted;
    private string _displayValue = string.Empty;
    private bool   _isTimerCell;
    private TimeSpan _elapsed;

    public int    Row     { get; set; }
    public int    Column  { get; set; }
    public string Label   { get; set; } = string.Empty;
    public CellType Type  { get; set; }   // Header | Data | Timer | Empty

    public bool IsSelected
    {
        get => _isSelected;
        set { _isSelected = value; OnPropertyChanged(); }
    }

    public bool IsRowHighlighted
    {
        get => _isRowHighlighted;
        set { _isRowHighlighted = value; OnPropertyChanged(); }
    }

    public bool IsColumnHighlighted
    {
        get => _isColumnHighlighted;
        set { _isColumnHighlighted = value; OnPropertyChanged(); }
    }

    public string DisplayValue
    {
        get => _displayValue;
        set { _displayValue = value; OnPropertyChanged(); }
    }

    public bool IsTimerCell
    {
        get => _isTimerCell;
        set { _isTimerCell = value; OnPropertyChanged(); }
    }

    // For live timer cells — updated by a DispatcherTimer
    public TimeSpan Elapsed
    {
        get => _elapsed;
        set
        {
            _elapsed = value;
            // Auto-update display value when timer ticks
            if (IsTimerCell)
                DisplayValue = value.ToString(@"mm\:ss");
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Building the Matrix Grid in XAML

The matrix uses an ItemsControl with a UniformGrid as its items panel. This gives us automatic, even cell sizing without manually calculating widths and heights. The data template for each cell uses a MultiBinding to drive the background color from three independent states: selected, row-highlighted, and column-highlighted.

<!-- MainWindow.xaml — the cohort matrix -->
<ItemsControl x:Name="CohortMatrix"
              ItemsSource="{Binding MatrixElements}"
              KeyDown="Matrix_KeyDown"
              Focusable="True">

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- 14 columns: 1 row header + 13 period columns -->
            <UniformGrid Columns="14" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type local:MatrixElement}">
            <Border x:Name="CellBorder"
                    BorderBrush="#333"
                    BorderThickness="0.5"
                    Cursor="Hand"
                    MinWidth="80"
                    MinHeight="40"
                    MouseLeftButtonDown="Cell_MouseLeftButtonDown"
                    MouseDoubleClick="Cell_MouseDoubleClick">

                <Border.Background>
                    <!-- Priority: Selected > Column highlighted > Row highlighted > Default -->
                    <MultiBinding Converter="{StaticResource CellBackgroundConverter}">
                        <Binding Path="IsSelected" />
                        <Binding Path="IsRowHighlighted" />
                        <Binding Path="IsColumnHighlighted" />
                        <Binding Path="Type" />
                    </MultiBinding>
                </Border.Background>

                <Grid>
                    <TextBlock Text="{Binding DisplayValue}"
                               HorizontalAlignment="Center"
                               VerticalAlignment="Center"
                               FontSize="12"
                               Foreground="{Binding IsSelected,
                                   Converter={StaticResource SelectedForegroundConverter}}" />
                </Grid>
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

The Selection Logic — Highlighting Rows and Columns

When a cell is selected, the entire row and column need to visually indicate which headers correspond to that cell. This is the interaction that makes cohort analysis readable — you always know which cohort (row) and which time period (column) you are looking at. The logic clears all previous highlights, then applies the new ones in a single pass.

// MainWindow.xaml.cs — selection and highlight logic
private MatrixElement? _selectedElement;

private void SelectCell(MatrixElement cell)
{
    if (cell.Type == CellType.Header || cell.Type == CellType.Empty)
        return;

    // Clear previous selection state
    foreach (var el in MatrixElements)
    {
        el.IsSelected         = false;
        el.IsRowHighlighted   = false;
        el.IsColumnHighlighted = false;
    }

    _selectedElement = cell;
    cell.IsSelected = true;

    // Highlight all cells in the same row
    foreach (var el in MatrixElements.Where(e => e.Row == cell.Row))
        el.IsRowHighlighted = true;

    // Highlight all cells in the same column
    foreach (var el in MatrixElements.Where(e => e.Column == cell.Column))
        el.IsColumnHighlighted = true;
}

// Keyboard navigation — Arrow keys move selection
private void Matrix_KeyDown(object sender, KeyEventArgs e)
{
    if (_selectedElement is null) return;

    int currentRow = _selectedElement.Row;
    int currentCol = _selectedElement.Column;

    var (targetRow, targetCol) = e.Key switch
    {
        Key.Up    => (currentRow - 1, currentCol),
        Key.Down  => (currentRow + 1, currentCol),
        Key.Left  => (currentRow, currentCol - 1),
        Key.Right => (currentRow, currentCol + 1),
        _         => (currentRow, currentCol)
    };

    var target = MatrixElements
        .FirstOrDefault(el =>
            el.Row    == targetRow &&
            el.Column == targetCol &&
            el.Type   == CellType.Data);

    if (target is not null)
    {
        SelectCell(target);
        e.Handled = true;  // Prevent default scroll behaviour
    }

    if (e.Key == Key.Enter && _selectedElement is not null)
        OpenDetailPopup(_selectedElement);
}

Live Timer Cells — Real-Time Updates Without Freezing the UI

Some cells in the matrix show live elapsed time — they count up or down in real time. The challenge is updating these cells frequently without causing the entire 13×14 grid to re-render on every tick. The solution is a DispatcherTimer that only updates the specific timer cell models, not the full collection.

private readonly DispatcherTimer _liveTimer;
private readonly List<MatrixElement> _timerCells = [];

private void InitialiseLiveTimers()
{
    // Identify which cells have live timers
    // (e.g. cells in the most recent cohort column)
    _timerCells.AddRange(
        MatrixElements.Where(el => el.IsTimerCell));

    _liveTimer = new DispatcherTimer
    {
        Interval = TimeSpan.FromSeconds(1)
    };

    _liveTimer.Tick += (_, _) =>
    {
        foreach (var cell in _timerCells)
        {
            // Increment elapsed — the property setter auto-updates DisplayValue
            cell.Elapsed = cell.Elapsed.Add(TimeSpan.FromSeconds(1));
        }
        // Only timer cells fire PropertyChanged — the rest of the grid is untouched
    };

    _liveTimer.Start();
}

The Detail Popup — Context-Aware Order View

Double-clicking a cell or pressing Enter opens a popup showing the buy/sell order details for that cohort intersection. The popup receives the selected MatrixElement as its data context and shows the associated Order objects — bid price, ask price, quantity, and time.

// Order.cs — the detail model shown in the popup
public class Order
{
    public string  OrderId    { get; set; } = string.Empty;
    public decimal BidPrice   { get; set; }
    public decimal AskPrice   { get; set; }
    public int     Quantity   { get; set; }
    public string  Side       { get; set; } = "Buy"; // Buy | Sell
    public DateTime Timestamp { get; set; }
    public string  Status     { get; set; } = "Open";

    public decimal Spread => AskPrice - BidPrice;
    public string  DisplayTime => Timestamp.ToString("HH:mm:ss");
}

// Open popup with correct context
private void OpenDetailPopup(MatrixElement cell)
{
    var orders = GetOrdersForCell(cell); // Load from data source

    var popup = new OrderDetailWindow
    {
        DataContext = new OrderDetailViewModel
        {
            Cell          = cell,
            Orders        = new ObservableCollection<Order>(orders),
            CohortLabel   = GetRowLabel(cell.Row),
            PeriodLabel   = GetColumnLabel(cell.Column),
        },
        Owner = this,
        WindowStartupLocation = WindowStartupLocation.CenterOwner
    };

    popup.ShowDialog();
}

What Happened When I Asked AI to Build This

This is where it gets interesting. After building the component manually, I described the same requirements to an AI assistant and asked it to generate the WPF cohort chart. The AI produced code quickly. But here is what it got wrong — and why each problem required domain knowledge to catch.

Problem 1: The selection model was wrong. AI generated a SelectedItem binding on the ItemsControl — but ItemsControl does not have selection built in. Only ListBox does. The AI confused two different WPF controls with similar purposes. The result compiled, but clicking a cell did nothing. A developer knows the distinction; a non-developer would stare at a working build that does not respond to clicks.

Problem 2: Row/column highlighting used a value converter that re-evaluated the entire grid. AI's approach to highlighting involved a converter that checked the selected row and column index against every cell on every property change. With 182 cells and a selected cell changing on every keystroke, this triggered 182 converter evaluations on every arrow key press. On a modern machine it was imperceptible. On the target trading workstation with multiple charts running simultaneously, it caused noticeable lag. The solution — keeping IsRowHighlighted and IsColumnHighlighted as direct properties on each cell — was more code but dramatically better for performance.

Problem 3: The timer cells caused the entire grid to re-render every second. AI's timer implementation raised PropertyChanged on the collection itself rather than on the individual cell models. Every tick caused all 182 cells to re-evaluate their bindings. At 1-second intervals this was visually fine, but at the 100ms update rate some financial matrices need, it produced visible flicker across the entire grid. The fix — targeted property change notifications only on timer cells — required understanding how WPF's binding engine decides what to re-render.

The Pattern: AI Knows the How, Not the Why

After building the cohort chart both manually and with AI assistance, the pattern is consistent with everything I have seen on larger projects. AI knows how to write WPF code — the syntax, the patterns, the common approaches. It does not know why one approach performs better than another in a specific context, or why ItemsControl and ListBox look similar but behave fundamentally differently for selection scenarios.

For a data visualisation component used in financial trading — where performance matters and incorrect data display has real consequences — that distinction is the entire job. The AI gives you a working prototype in minutes. Making it production-ready is still the developer's work.

The full source code for the Cohort Analysis Chart is available on GitHub. If you are building custom data visualisation components in WPF — or need a senior .NET developer who can work across both the performance and correctness dimensions — reach out.

Found this useful?

Share it with your network — it helps others find this too.

https://kathanpatel.vercel.app/blog/wpf-cohort-analysis-chart-interactive-matrix-ai-comparison