Docs / Build Workflow

Visualization — line

When to use line

Line is the right choice when the x-axis is ordered and the question is about trend — typically a time series (daily revenue, monthly orders, hourly errors) or any other naturally ordered category. The same renderer covers three shapes:

  • Single line — one measure plotted across the x-axis. The default.
  • Dual-axis — two measures on different scales sharing one chart. Add mapping.y2; the second measure renders against a right-hand y-axis.
  • Multi-series — one line per category value, all sharing the same y-axis. Add mapping.series with the categorical field.

y2 and series are mutually exclusive in intent: dual-axis is for two measures on the same x; multi-series is for one measure split by a category. Do not combine them in one viz.

Use bar instead when the x-axis is unordered and the question is about comparison. Use scatter when the relationship is between two continuous measures rather than a measure across an ordered axis.

Mapping

  • mapping.x — required. The ordered axis field (date, month, integer, ordinal category).
  • mapping.y — required. Numeric field for the (left) y-axis values.
  • mapping.y2 — optional. Numeric field. When present, the chart switches to dual-axis mode.
  • mapping.series — optional. Categorical field. When present, the chart switches to multi-series mode.
  • mapping.series_label — legend / tooltip label for the primary y series. Default "value".
  • mapping.series_label_2 — legend / tooltip label for the y2 series in dual-axis mode. Default "y2".
# single line
mapping:
  x: order_month
  y: revenue
  series_label: Revenue

# dual-axis
mapping:
  x: order_month
  y: revenue
  y2: order_count
  series_label: Revenue
  series_label_2: Orders

# multi-series (one line per channel)
mapping:
  x: order_month
  y: revenue
  series: channel

chart shortcuts

The chart block is typed and closed — anything not listed in this page is rejected at validation time.

  • chart.show_value_labels — boolean. Turns on data-point labels for every series. Style them with chart.value_label below.
  • chart.cross_filter — boolean, default true. Set false to suppress click-to-filter for this viz.
  • chart.height — pixel height of the viz container.

Line intentionally exposes a small option surface — most styling decisions are taken from the platform theme. Series-level customisation goes through mapping, not chart.

Value labels

chart.value_label is an object applied to every series when chart.show_value_labels is on:

  • position — placement of the label relative to the point. Common values: "top", "bottom", "right", "insideTop", "insideBottom".
  • rotate — degrees, between -90 and 90.
  • color, font_size, font_weight.
  • formatter — string template (no callbacks). Use {c} for the value.
  • distance, align, vertical_align, clip.

Value labels on a line are noisy for dense series; consider showing them only on the last point by post-processing the data, or rely on the hover tooltip instead.

Legend & tooltip

chart.legend:

  • show — boolean.
  • position — shortcut: "top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right".
  • orient"horizontal" | "vertical".
  • top / bottom / left / right — pixel number or percent string.
  • text_stylecolor, font_style, font_weight, font_family, font_size, line_height.
  • item_width, item_height, item_gap.

chart.tooltip:

  • show — boolean.
  • trigger"item", "axis" (recommended for line — shows all series at the hovered x), or "none".
  • confine, formatter, background_color, border_color, border_width, padding, text_style.
  • axis_pointer.type"line" (recommended for line), "shadow", "none", "cross".

Axes

chart.x_axis and chart.y_axis share the same shape (with one extra on x_axis):

  • name — axis title.
  • name_location"start" | "middle" | "center" | "end".
  • name_gap — pixels between axis line and name.
  • axis_label.show — boolean.
  • axis_label.rotate — degrees, -90 to 90.
  • axis_label.interval — integer or "auto".
  • axis_label.color, axis_label.font_size, axis_label.font_weight.
  • axis_label.formatter — string template.
  • axis_label.max_chars — integer ≥ 1, ellipsizes long labels.
  • x_axis.visible_window — integer ≥ 1. Restricts the visible range and enables a horizontal range slider; useful for long time series.

format

  • format.y or format[<y_field_name>] — pattern for the left y-axis labels and tooltip values.
  • format.y2 or format[<y2_field_name>] — pattern for the right y-axis (dual-axis mode).
  • format at the root — fallback.
format:
  revenue: "$#,##0"
  order_count: "#,##0"

Cross-filter behavior

Inside a dashboard, clicking a line cross-filters the rest of the dashboard. Full mechanism at Cross-filtering. Line specifics:

  • In single / dual-axis mode, clicking a point cross-filters by the clicked x value.
  • In multi-series mode, clicking a line cross-filters by the series name.
  • The clicked field must be declared as a parameter in at least one model used by the dashboard, otherwise the click is silently ignored.
  • Disable per viz with chart.cross_filter: false.

Worked examples

Single line, with a rotated x-axis label for monthly periods:

id: ec_revenue_over_time_line
title: Revenue Over Time
query: "models/ec_revenue.malloy::over_time"
type: line
mapping:
  x: order_month
  y: revenue
  series_label: Revenue
chart:
  height: 320
  x_axis:
    axis_label:
      rotate: -30
  y_axis:
    name: Revenue
  tooltip:
    trigger: axis
    axis_pointer:
      type: line
format:
  revenue: "$#,##0"
published: true

Dual-axis (revenue on the left, order count on the right):

id: ec_revenue_orders_line
title: Revenue and Orders
query: "models/ec_revenue.malloy::over_time"
type: line
mapping:
  x: order_month
  y: revenue
  y2: order_count
  series_label: Revenue
  series_label_2: Orders
chart:
  height: 320
  legend:
    show: true
    position: top
format:
  revenue: "$#,##0"
  order_count: "#,##0"
published: true

Multi-series (one line per channel):

id: ec_revenue_by_channel_line
title: Revenue by Channel
query: "models/ec_revenue.malloy::by_channel_over_time"
type: line
mapping:
  x: order_month
  y: revenue
  series: channel
chart:
  legend:
    show: true
    position: top
  tooltip:
    trigger: axis
format:
  revenue: "$#,##0"
published: true

Long time series with a horizontal data-zoom slider:

chart:
  x_axis:
    visible_window: 30

Common pitfalls

  • Mixing dual-axis and multi-series. Setting both y2 and series in the same viz produces undefined behavior — pick one.
  • Dual-axis labels collide. Two value scales need room. Set explicit name_gap on both axes, reduce data density, or split into two charts.
  • Multi-series legend is too wide. If the categorical field has many distinct values, the legend can dominate the chart. Move it to position: bottom, or filter to top-N in the underlying Malloy query.
  • Date formatting on the x-axis is wrong. Format the x value with format[<x_field_name>] or pre-format in the Malloy query (e.g. derive an order_month_label string).
  • Time-zone shifts in the x values. Time-series x values are interpreted in the user's session timezone. Pre-bucket dates to days or months in the Malloy query if you need consistent groupings across users.
  • Cross-filter clicks have no effect. The clicked field must be declared as a parameter in at least one model used by the dashboard. Without that, clicks are silently ignored.