LocusZoom

repository·develop·Indexed 41 days ago

https://github.com/statgen/locuszoom

A low-level JavaScript/D3 plugin for creating interactive, embeddable visualizations of statistical genetic data. Version 0.14.0 provides tools for developers to define data sources via adapters, manage layouts, and render genomic plots using a pull-based data retrieval approach.

Tokens
20.4K
Snippets
60
Records
81
Agent score
43%

What's inside LocusZoom

  1. Combine multiple data sources using data operations

    develop

    LocusZoom allows you to combine different data layers (e.g., association data and LD information) within a single data layer layout using data_operations. This is essential when a single plot element depends on information from multiple sources.

    Data operations consist of two main types:

    1. Fetch operations: Used to retrieve data. You can specify dependencies using the syntax some_request(dependency_name). For example, ld(assoc) tells LocusZoom to wait until the assoc data is retrieved before requesting ld data. LocusZoom automatically parallelizes requests that have no dependencies.
    2. Joins (Data functions): Used to merge multiple sets of records into a single list. You can use built-in joins like left_match, inner_match, and full_outer_match, or register custom functions in LocusZoom.DataFunctions.

    Note: Everything listed in data_layer.namespace automatically triggers a retrieval request. You only need to explicitly modify fetch.from if you need to define dependencies (e.g., ld(assoc)).

    {
        namespace: { 'assoc': 'mystudy', 'ld': 'some_ld' },
        data_operations: [
            {
                type: 'fetch',
                from: ['assoc', 'ld(assoc)'],
            },
            {
                type: 'left_match',
                name: 'assoc_plus_ld',
                requires: ['assoc', 'ld'],
                // Specify field names for the join
                params: ['assoc:position', 'ld:position2'], 
            },
        ]
    }
  2. Match similar elements across panels

    develop

    LocusZoom allows different data layers to communicate so that selecting an element in one panel highlights related elements in another. This is achieved through a match directive.

    1. Opt-in: Add a match object to your layer configuration.
    2. Broadcasting: A layer with match.send will broadcast the value of a specific field when an element is selected.
    3. Receiving: A layer with match.receive will examine its data points. If a point's specified field matches the broadcast value, it is tagged with the internal field lz_is_match.
    4. Rendering: Use the lz_is_match field in your layout directives (e.g., color, size, shape) to visually distinguish matched points.

    Note: For performance, only one field can be broadcast at a time, and only a single value is broadcast across all layers.

    To trigger matching from outside the plot, you can manually update the plot state using plot.applyState({lz_match_value: your_value_here}).

    {
      ...options,
      // A layer can send and receive to respond to its own events
      match: { send: '{{namespace[access]}}target', receive: '{{namespace[access]}}target' },
      color: [
        {
          field: 'lz_is_match', // Use the internal tag to trigger custom rendering
          scale_function: 'if',
          parameters: {
            field_value: true,
            then: '#ff0000',
          },
        },
      ],
    }
  3. Handle numerical underflow for p-values

    develop

    JavaScript cannot accurately represent extremely small p-values (typically smaller than ~ 5e-324), which will cause them to be truncated to 0. This can break calculations like -log10(p-value) on the client side.

    Best Practice: Transform your p-values to -log10 format on your server before sending them to the web page. If you must do it on the client, perform the transformation within a custom adapter's _annotateRecords method immediately upon receipt of the data.

  4. How layouts and the central registry work

    develop

    LocusZoom uses a configuration-driven model where layouts act as the 'glue' connecting data (content) to presentation (data layers).

    Key concepts:

    • JSON-serializable: Layouts must be simple JSON-serializable objects (strings, numbers, arrays, objects). They cannot contain functions.
    • The Registry: Features like plots, panels, and data layers are managed in a central registry (e.g., LocusZoom.Layouts, LocusZoom.DataLayers).
    • Decoupling: Layouts do not contain code; they request features by name (as strings). For example, a layout specifies a data layer type by its string name, and LocusZoom looks up the corresponding implementation in the registry at runtime.
    • Composition: Plots are composed of panels (arranged adjacent to each other), and panels are composed of data layers (stacked on top of each other).
  5. How LocusZoom.js handles data and customization

    develop

    LocusZoom.js uses a decoupled architecture where visualizations are responsible for display, while data operations and custom behaviors are handled by specialized elements. This allows you to mix and match components without affecting their internal logic.

    Key components for data and behavior:

    • Adapters: Coordinate the formatting and retrieval of data.
    • Matching functions: Coordinate filter and match operations that affect which items are displayed across different panels (used for interactivity).
    • Scale functions: Control the size, shape, or color of plot elements. These are used in any layout directives marked as ScalableParameter.
    • Transformation Functions: Control the display or content of field values in tooltips, axis labels, etc. They are used via the template syntax {{fieldname|functionname}}.
  6. Understand the data request lifecycle in an adapter

    develop

    When an adapter requests data, it follows a specific sequence of methods to construct the query, handle caching, and transform the raw response into a format suitable for rendering. Developers can override these methods to customize how data is fetched and processed.

    The execution order is roughly:

    1. getData(plot_state, ...dependent_data): The entry point.
    2. _buildRequestOptions(plot_state, ...dependent_data): Prepares request parameters.
    3. _getCacheKey(request_options): Determines the cache identifier.
    4. _performRequest(request_options): Executes the network request.
    5. _getURL(request_options): Generates the target URL.
    6. _normalizeResponse(raw_response, options): Parses and transforms raw data (e.g., converting columnar to row-based formats).
    7. _annotateRecords(records, options): Performs user-specified cleanup like field renaming, filtering, or adding calculated fields.
    8. _postProcessResponse(records, options): Final transformation, typically used for adding field prefixes (e.g., assoc:id).
    getData(plot_state, ...dependent_data)
        _buildRequestOptions(plot_state, ...dependent_data)
        _getCacheKey(request_options)
        _performRequest(request_options)
            _getURL(request_options)
        // Parse JSON, convert columnar data to rows, etc
        _normalizeResponse(raw_response, options)
            cache.add(records)
        // User-specified cleanup of the parsed response fields.
        _annotateRecords(records, options)
        // Usually not overridden: applies prefixes and simplifies fields
        _postProcessResponse(records, options)
  7. Map data layers to global namespaces

    develop

    Each data layer in a LocusZoom plot is independent and uses a namespace mapping to connect its local fields to the global data sources defined in LocusZoom.DataSources.

    In a layout object, the namespace property is a key-value pair where:

    • Key: The local_name used within that specific layer's configuration.
    • Value: The global_data_source_name (the namespace used in DataSources.add()).

    This indirection allows you to reuse the same layout for different datasets simply by changing the namespace mapping.

    Example Layout Mapping:

    {
      namespace: {'assoc': 'mystudy'},
      id_field: 'assoc:variant'
    }

    In this example, whenever the layer refers to a field starting with assoc:, it is actually requesting data from the global source named 'mystudy'.

    {
      namespace: {'assoc': 'mystudy'},
      id_field: 'assoc:variant'
    }
  8. How elements communicate via events

    develop

    LocusZoom.js plots are interactive and can communicate with elements outside the plot (like external tables) or form connections between decoupled data layers. This is achieved through an event-driven system.

    Common user actions—such as clicking a data point, dragging to change the genomic region, or receiving new data—emit events containing metadata about the trigger. These events allow for internal synchronization (e.g., matching data elements across different panels) and external integration.

  9. Transform data using _normalizeResponse and _annotateRecords

    develop

    Once data is retrieved, it often needs to be transformed into the row-based format ([{field: value}]) expected by LocusZoom data layers.

    _normalizeResponse(raw_response, options)

    Use this to convert non-standard responses into a standard format.

    • Columnar to Row conversion: If your source provides data in a column-oriented format (e.g., {a_field: [1], other_field: [1]}), this method can transform it into the required row-based format ([{a_field: 1, other_field: 1}]). Note that the default adapter attempts to do this automatically for some sources.

    _annotateRecords(records, options)

    Use this for high-level data manipulation after parsing. Common use cases include:

    • Renaming fields: e.g., changing pValue to log_pvalue.
    • Filtering: Removing records based on specific criteria.
    • Calculated fields: Generating new fields on the fly, such as creating a variant marker field by concatenating chromosome:position_ref/alt if the API doesn't provide it.
    • Toggling datasets: Using plot.state to decide which subset of data to return from a single large response (useful for creating cheap toggles via the set_state toolbar widget).
  10. How LocusZoom handles data: Pull vs Push

    develop

    LocusZoom uses a pull-oriented data retrieval model. Instead of being 'pushed' data from an external source, the plot asks for the data it needs based on its current state (e.g., current zoom level or region).

    Key implications for developers:

    • Avoid direct mutation: Do not try to force display changes by mutating data_layer.data directly; these changes will be overwritten on the next re-render.
    • State is the source of truth: To change what is shown, update the plot's state via applyState(). The data adapters will then 'pull' the correct data based on that state.
    • Data logic belongs in adapters: Changes to data (like calculated fields) should be implemented in the data adapter, not the rendering layer, to ensure consistency across re-renders.
  11. Transform values before filtering

    develop

    You can apply transformations to a field value before the filter operator is evaluated. This is useful for filtering by transformed metrics, such as -log10(pvalue) instead of the raw pvalue.

    {
      ...options,
      filters: [
        { field: '{{namespace[assoc]}}pvalue|neglog10', operator: '>=', value: 7.301 },
      ],
    }
  12. Set the initial plot view via State or data-region

    develop

    You can control the initial genomic region displayed by the plot using two methods:

    1. Using the State object in a Layout Include a state object within your layout definition. This is useful when using predefined layouts.

    const layout = LocusZoom.Layouts.get('plot', 'standard_association', { 
      state: { chr: 6, start: 20379709, end: 20979709 } 
    });

    2. Using the data-region attribute Add a data-region attribute to the HTML element that will contain the plot. LocusZoom.populate() will automatically parse this.

    <div id="lz-plot" data-region="10:114550452-115067678"></div>