> ## Documentation index
> Fetch the complete documentation index at: https://docs.odysseysignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Run map

> Everything needed to draw one of the DataSet's maps

API versions: v1 (current) https://docs.odysseysignal.com/api/reference/run-map.md

`POST /v1/signals/{signal_id}/datasources/{datasource_id}/datasets/{dataset_id}/map`

A brand position, the demand space sizes or a brand profile: the same figures `query` answers, with the rules to draw them as the portal draws them — the sectors and their names, the radial scale, each point's drawing position (`draw.rho`, `draw.bearing_deg`), colours and label placement. Charged as a query of as many rows as it has points.

Authentication: an API key in `X-Api-Key`, or `Authorization: Bearer <token>`.

## Parameters

### Path parameters

- `signal_id` (string, required)
- `datasource_id` (string, required)
- `dataset_id` (string, required)

## Request body

`application/json`, required. A map of one DataSet: which map, and the question behind it.

- `type` (string, required): `brand_position`: brands placed on the demand space map. `demand_space_sizes`: how large each demand space is. `brand_profile`: a spider of each brand's strength per demand space. One of `brand_position`, `demand_space_sizes`, `brand_profile`.
- `metric` (string or array of string, nullable): The catalogue's metric to draw: a position (`position`/`position2`, or `position_di` at its map level) for a brand position, a demand-space index (or a `position_di`/`double_index_net` per need area) for a brand profile. A brand profile takes a list too (only when the user asks for several metrics), each metric a line per brand in the same map. Omitted: the DataSet's first such metric.
- `drivers` (boolean or string or array of string, nullable): Brand position only, and only when the user asks for them (one series per map is the default): the category's drivers, a layer per driver position, each driver sized by its share. `true` takes every driver position in the DataSet's catalogue (a study asks the drivers' relation and relevance as positions of their own, and the portal draws them together); a string or a list names them.
- `sizes` (boolean, default false): Brand position only, and only when the user asks for it: each demand space's size, written in its sector.
- `period` (object): The time window. A dimension, but with first-class syntax. ``rolling`` is the moving-average window in periods. It is part of the question, not a display setting: a smoothed value and an unsmoothed one are different numbers, and a base read beside a smoothed value covers the same window.
  - `granularity` (string): Omit for the Signal's standard unit where the DataSet has it, else the data project's own; the answer's `meta.period` says which was used. One of `month`, `quarter`, `halfyear`, `year`.
  - `last` (integer, ≥ 1, ≤ 120, default 1): How many periods, counting back from `ending`. Above 1, a row that stands for one period of the window (a moving average is a series, one row per period) carries that period as a dimension, and the answer declares its column, even when `group_by` names no period; a window pooled into one figure carries none. Group by the period for one row per period every time. A driver's position has one place rather than one per period: it is answered for the last period only (as `last: 1`), and `meta.warnings` says so.
  - `ending` (string, nullable): Last period to include, named by any day in it: a period id as the API writes one (2026-09, 2026-Q3, 2026-H2, 2026) or a date. Omit for the latest available.
  - `rolling` (integer, ≥ 1, ≤ 12): Moving-average window in periods (1 = none); the windows allowed per unit are in discovery. Omit for the portal's preset for the unit (discovery `period.defaults_by_granularity`).
  - `complete_only` (boolean): Exclude a trailing period that has not finished; an open question then ends at the last finished period, as the portal does. Omit for the portal's preset for the unit.
- `filters` (map of array of string or integer or boolean): As `dimensions.filters` on a query.
- `entities` (array of integer or string, nullable): As on a query: the brands to draw. Omitted: the Signal's standard set.
- `focus` (array of integer or string, nullable): As on a query: brands to add and mark as the caller's own.
- `theme` (string, default "light") One of `light`, `dark`.

## Example request

```bash
curl -X POST "https://api.odysseysignal.app/v1/signals/{signal_id}/datasources/{datasource_id}/datasets/{dataset_id}/map" \
  -H "X-Api-Key: $ODYSSEY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "brand_position"
  }'
```

## Response 200

Content: `application/json`.

- `type` (string, required) One of `brand_position`, `demand_space_sizes`, `brand_profile`.
- `theme` (string, required) One of `light`, `dark`.
- `frame` (object, required)
  - `orientation` (string, default "Bearing 0° is straight up and bearings run clockwise. A point is drawn at (cx + rho·R·sin(bearing), cy − rho·R·cos(bearing)) for a map of radius R; rho 1 is the outer circle.")
  - `outer_circle` (number, default 1)
  - `rings` (array of number)
  - `reference_radius_px` (number, default 300): Every px value in the answer (dots, fonts, offsets) is meant for a map of this radius; scale them with yours.
  - `sectors` (array of objects, required)
    - `id` (integer, required)
    - `label` (string, required)
    - `description` (string, nullable)
    - `start_deg` (number, required)
    - `end_deg` (number, required)
    - `mid_deg` (number, required)
    - `colour` (string, required)
    - `fill` (string, required)
    - `fill_alpha` (number, required)
    - `stroke` (string, required)
  - `sector_labels` (string, default "Each sector's label along its arc, 12 px outside the outer circle, bold 13 px, in `labels.colour` (the theme's text colour), never in the sector's colour.")
  - `ribbon` (object, nullable): A band from `inner` to `outer` of the radius in each sector's `colour`. Absent: draw no sector colour at all.
    - `inner` (number, default 0.95)
    - `outer` (number, default 1)
- `radial` (object, nullable): How a radius became ``draw.rho``: the values the answer was computed with.
  - `scale` (string, required) One of `linear`, `log`, `sigmoid`.
  - `map_limit` (number, required)
  - `log_k` (number, nullable)
  - `sigmoid_midpoint` (number, nullable)
  - `sigmoid_steepness` (number, nullable)
  - `formula` (string, required)
  - `map_limit_auto` (boolean, nullable)
  - `rings` (array of number, nullable)
- `profile_scale` (object, nullable)
  - `min` (number, required)
  - `max` (number, required)
  - `average` (number, default 100)
  - `formula` (string, default "rho = (value − min) / (max − min); the average is the competitors' 100.")
- `layers` (array of objects, required)
  - `kind` (string, required) One of `brands`, `drivers`, `sizes`, `profile`, `average`.
  - `label` (string, required)
  - `colour` (string, required)
  - `metric` (string, nullable)
  - `line_px` (number, nullable)
  - `dash` ([number, number], nullable, at least 2 items, at most 2 items)
  - `points` (array of objects, required)
    - `id` (integer or string, required)
    - `label` (string, required)
    - `type` (string, required) One of `brand`, `ghost`, `driver`, `size`, `profile`, `average`.
    - `demand_space` (integer, nullable)
    - `value` (number or object, nullable)
      - `x` (number, required)
      - `y` (number, required)
    - `polar` (object, nullable): The same point in polar form, as the map draws it. Derived from ``x``/``y``, so it is context rather than a measurement: ``r`` is the distance from the centre, ``theta`` the angle from the x-axis counter-clockwise and ``phi`` the angle from the top clockwise — the one the demand spaces are cut by — all in radians. ``radius_true`` and ``outside_map`` come only from a method that keeps the map's edge as a boundary rather than a position (``position2``): a brand beyond the edge is drawn on it, ``r`` is then the edge, and ``radius_true`` how far out it really is. Absent, not null, otherwise.
      - `r` (number, nullable)
      - `theta` (number, nullable)
      - `phi` (number, nullable)
      - `radius_true` (number, nullable)
      - `outside_map` (boolean, nullable)
    - `draw` (object, required)
      - `rho` (number, required)
      - `bearing_deg` (number, required)
    - `outside_map` (boolean, default false)
    - `radius_true` (number, nullable)
    - `hint` (string, nullable)
    - `size` (number, nullable)
    - `base` (integer, nullable)
    - `own_brand` (boolean, default false)
    - `colour` (string, nullable)
    - `dot_px` (number, nullable)
    - `marker` (string, default "dot") One of `dot`, `boundary`, `text`, `vertex`.
    - `label_side` (string, nullable) One of `above`, `below`.
    - `markets` (integer, nullable)
    - `markets_total` (integer, nullable)
    - `in_net` (boolean, nullable)
    - `low_base` (boolean, nullable)
- `labels` (object, required)
  - `font_px` (integer, default 9)
  - `colour` (string, required)
  - `offset_px` (string, default "dot radius + 2")
  - `side` (string, default "`label_side` on each point: in the top half of the map a point further out than r 1.4 is labelled below, any other above; the bottom half the other way round.")
  - `collision` (string, default "Keep a label next to its dot. When it would cover another label, a dot or a size, try its other side, then right and left of the dot, a little further out at each round; never hide one. A label that ended up more than about 12 px from its dot gets a thin line to it.")
  - `sizes` (string, default "A size is written bold at 16 px, as a whole percentage (rounded), centred on its point.")
  - `boundary_fill` (string, default "#22293C")
  - `boundary` (string, default "A `boundary` marker is a hollow ring 1.5× the dot, filled with `boundary_fill` (the map ground), with a 2 px edge in the layer's colour and, inside it, a chevron in that colour pointing outwards: nothing of it reaches past the ring. `hint` is its tooltip.")
- `legend` (array of objects, required)
  - `label` (string, required)
  - `colour` (string, required)
  - `marker` (string, required) One of `dot`, `line`, `text`.
  - `dash` ([number, number], nullable, at least 2 items, at most 2 items)
- `meta` (object, required)
  - `figures_rule` (string, default "Use only the figures in this answer, exactly as given, each with its label, metric, market and period. Never compute a new figure from them: no ratio, no share of a share (such as consideration among those aware), no conversion rate, no difference or change between two figures (give both and say which is higher), no sum, no average, no index. Never estimate, round differently or fill a gap: a figure that is not in an answer is not available, so say so. Every claim, explanation and conclusion must rest on the answers; do not invent reasons the data does not give. Before presenting, check every figure again against the answer it came from and correct or drop any that does not match."): How the figures may be used: as given, never as the input to a new one. The same text on every answer.
  - `period` (object, nullable): The window the answer covers, every field resolved. Echoed because a question may leave any of it open and the defaults depend on the Signal and the DataSet, so a client presenting the figures does not have to work them out again.
    - `granularity` (string, required) One of `month`, `quarter`, `halfyear`, `year`.
    - `last` (integer, required)
    - `ending` (string, required, nullable): The period counted back from, as a period id at `granularity`: the one asked for, or the latest with data.
    - `rolling` (integer, required): Moving-average window in periods; 1 means no smoothing.
    - `complete_only` (boolean, required): False when a last period that has not finished is included.
    - `pooled` (boolean, default false): Present, and true, only when the window spans several periods and each figure covers all of it as one span (the portal's "whole period pooled"), so no row names a period: unsmoothed, and not broken down by period. Group by `period` for a figure per period.
    - `last_complete` (boolean, nullable): Whether the last period the answer holds (`ending`) had finished by the latest data the Signal may read: false means its figures cover only the months so far. Left out when the waves are not dated.
  - `warnings` (array of string)
  - `query_ids` (array of string)
  - `applied_filters` (array of objects)
    - `dimension` (string, required)
    - `label` (string, required)
    - `origin` (string, required): `request` when the question named these members; `signal_default` when it left the filter open and the Signal's default filled it in (its standard market, its default competitors: see the DataSet's `defaults`); `default` when it left the filter open and the answer covers every member available to it. One of `request`, `signal_default`, `default`.
    - `members` (array of objects, required)
      - `id` (string or integer or boolean, required)
      - `label` (string, required, nullable): Null when this DataSet has no such member; the answer then carries a warning saying so.
      - `role` (string, nullable): On a brand: `focus` when the question named it in `focus`, `standard_brand` when it is the Signal's standard brand, added because the question named no focus. Absent otherwise. One of `focus`, `standard_brand`.
      - `has_rows` (boolean, default true): Present, and false, only when the rows break the answer down by this dimension and none of them is this member: the answer has no data for it, and a warning names it.
  - `environment` (string, nullable): The data environment the answer was read in, `prod` or `test`: the one Odyssey set on the Signal, or the one an Odyssey person picked for this call.

## Errors

Errors are `application/problem+json` (RFC 9457): `type`, `title`, `status`, `detail` and, for some, more fields that say how to fix the call. The `type` is `https://api.odysseysignal.app/problems/<code>`.

| Status | Code | When |
|---|---|---|
| 400 | `invalid-request` | The request is not valid for this DataSet; `detail` says why. |
| 401 | `unauthorized` | No credential, or one that is not accepted. |
| 403 | `forbidden` | The credential is valid but does not cover what was asked. |
| 404 | `not-found` | Nothing there, or nothing this credential may read. |
| 422 | `validation-failed` | The request does not match the contract; `errors` lists each field. |
| 429 | `rate_limited` | Too many requests, or the monthly quota is used up. |
| 503 | `unavailable` | A service behind the API did not answer; try again. |
