# Dot mark ​

The dot mark draws circles or other symbols positioned in x and y as in a scatterplot. For example, the chart below shows the roughly-inverse relationship between car horsepower in y↑ and fuel efficiency in miles per gallon in x→.

Fork
js
``Plot.dot(cars, {x: "economy (mpg)", y: "power (hp)"}).plot({grid: true})``
``Plot.dot(cars, {x: "economy (mpg)", y: "power (hp)"}).plot({grid: true})``

Using a function for x, we can instead plot the roughly-linear relationship when fuel efficiency is represented as gallons per 100 miles. (For fans of the metric system, 1 gallon per 100 miles is roughly 2.4 liters per 100 km.)

Fork
js
``````Plot.plot({
grid: true,
inset: 10,
x: {label: "Fuel consumption (gallons per 100 miles)"},
y: {label: "Horsepower"},
marks: [
Plot.dot(cars, {x: (d) => 100 / d["economy (mpg)"], y: "power (hp)"})
]
})``````
``````Plot.plot({
grid: true,
inset: 10,
x: {label: "Fuel consumption (gallons per 100 miles)"},
y: {label: "Horsepower"},
marks: [
Plot.dot(cars, {x: (d) => 100 / d["economy (mpg)"], y: "power (hp)"})
]
})``````

Dots support stroke and fill channels in addition to position along x and y. Below, color is used as a redundant encoding to emphasize the rising trend in average global surface temperatures. A diverging color scale encodes values below zero blue and above zero red.

Fork
js
``````Plot.plot({
y: {
grid: true,
tickFormat: "+f",
label: "Surface temperature anomaly (°F)"
},
color: {
scheme: "BuRd"
},
marks: [
Plot.ruleY([0]),
Plot.dot(gistemp, {x: "Date", y: "Anomaly", stroke: "Anomaly"})
]
})``````
``````Plot.plot({
y: {
grid: true,
tickFormat: "+f",
label: "Surface temperature anomaly (°F)"
},
color: {
scheme: "BuRd"
},
marks: [
Plot.ruleY([0]),
Plot.dot(gistemp, {x: "Date", y: "Anomaly", stroke: "Anomaly"})
]
})``````

Dots also support an r channel allowing dot size to represent quantitative value. Below, each dot represents a day of trading; the x-position represents the day’s change, while the y-position and area (r) represent the day’s trading volume. As you might expect, days with higher volatility have higher trading volume.

Fork
js
``````Plot.plot({
grid: true,
x: {
label: "Daily change (%)",
tickFormat: "+f",
percent: true
},
y: {
type: "log",
},
marks: [
Plot.ruleX([0]),
Plot.dot(aapl, {x: (d) => (d.Close - d.Open) / d.Open, y: "Volume", r: "Volume"})
]
})``````
``````Plot.plot({
grid: true,
x: {
label: "Daily change (%)",
tickFormat: "+f",
percent: true
},
y: {
type: "log",
},
marks: [
Plot.ruleX([0]),
Plot.dot(aapl, {x: (d) => (d.Close - d.Open) / d.Open, y: "Volume", r: "Volume"})
]
})``````

With the bin transform, sized dots can also be used as an alternative to a rect-based heatmap to show a two-dimensional distribution.

Fork
js
``````Plot.plot({
height: 640,
marginLeft: 60,
grid: true,
x: {label: "Carats"},
y: {label: "Price (\$)"},
r: {range: [0, 20]},
marks: [
Plot.dot(diamonds, Plot.bin({r: "count"}, {x: "carat", y: "price", thresholds: 100}))
]
})``````
``````Plot.plot({
height: 640,
marginLeft: 60,
grid: true,
x: {label: "Carats"},
y: {label: "Price (\$)"},
r: {range: [0, 20]},
marks: [
Plot.dot(diamonds, Plot.bin({r: "count"}, {x: "carat", y: "price", thresholds: 100}))
]
})``````

TIP

For hexagonal binning, use the hexbin transform instead of the bin transform.

While dots are typically positioned in two dimensions (x and y), one-dimensional dots (only x or only y) are also supported. Below, dot area is used to represent the frequency of letters in the English language as a compact alternative to a bar chart.

Fork
js
``Plot.dot(alphabet, {x: "letter", r: "frequency"}).plot()``
``Plot.dot(alphabet, {x: "letter", r: "frequency"}).plot()``

Dots, together with rules, can be used as a stylistic alternative to bars to produce a lollipop 🍭 chart. (Sadly these lollipops cannot be eaten.)

Fork
js
``````Plot.plot({
x: {label: null, tickPadding: 6, tickSize: 0},
y: {percent: true},
marks: [
Plot.ruleX(alphabet, {x: "letter", y: "frequency", strokeWidth: 2}),
Plot.dot(alphabet, {x: "letter", y: "frequency", fill: "currentColor", r: 4})
]
})``````
``````Plot.plot({
x: {label: null, tickPadding: 6, tickSize: 0},
y: {percent: true},
marks: [
Plot.ruleX(alphabet, {x: "letter", y: "frequency", strokeWidth: 2}),
Plot.dot(alphabet, {x: "letter", y: "frequency", fill: "currentColor", r: 4})
]
})``````

A dot may have an ordinal dimension on either x and y, as in the plot below comparing the demographics of states: color represents age group, y represents the state, and x represents the proportion of the state’s population in that age group. The normalize transform is used to compute the relative proportion of each age group within each state, while the group transform is used to pull out the min and max values for each state for a horizontal rule.

Fork
js
``````Plot.plot({
height: 660,
axis: null,
grid: true,
x: {
axis: "top",
label: "Population (%)",
percent: true
},
color: {
scheme: "spectral",
domain: stateage.ages, // in age order
legend: true
},
marks: [
Plot.ruleX([0]),
Plot.ruleY(stateage, Plot.groupY({x1: "min", x2: "max"}, {...xy, sort: {y: "x1"}})),
Plot.dot(stateage, {...xy, fill: "age", title: "age"}),
Plot.text(stateage, Plot.selectMinX({...xy, textAnchor: "end", dx: -6, text: "state"}))
]
})``````
``````Plot.plot({
height: 660,
axis: null,
grid: true,
x: {
axis: "top",
label: "Population (%)",
percent: true
},
color: {
scheme: "spectral",
domain: stateage.ages, // in age order
legend: true
},
marks: [
Plot.ruleX([0]),
Plot.ruleY(stateage, Plot.groupY({x1: "min", x2: "max"}, {...xy, sort: {y: "x1"}})),
Plot.dot(stateage, {...xy, fill: "age", title: "age"}),
Plot.text(stateage, Plot.selectMinX({...xy, textAnchor: "end", dx: -6, text: "state"}))
]
})``````
js
``xy = Plot.normalizeX("sum", {x: "population", y: "state", z: "state"})``
``xy = Plot.normalizeX("sum", {x: "population", y: "state", z: "state"})``

TIP

To reduce code duplication, pull shared options out into an object (here `xy`) and then merge them into each mark’s options using the spread operator (`...`).

To improve accessibility, particularly for readers with color vision deficiency, the symbol channel can be used in addition to color (or instead of it) to represent ordinal data.

Fork
js
``````Plot.plot({
grid: true,
x: {label: "Body mass (g)"},
y: {label: "Flipper length (mm)"},
symbol: {legend: true},
marks: [
Plot.dot(penguins, {x: "body_mass_g", y: "flipper_length_mm", stroke: "species", symbol: "species"})
]
})``````
``````Plot.plot({
grid: true,
x: {label: "Body mass (g)"},
y: {label: "Flipper length (mm)"},
symbol: {legend: true},
marks: [
Plot.dot(penguins, {x: "body_mass_g", y: "flipper_length_mm", stroke: "species", symbol: "species"})
]
})``````

Plot uses the following default symbols for filled dots:

js
``````Plot.dotX([
"circle",
"cross",
"diamond",
"square",
"star",
"triangle",
"wye"
], {fill: "currentColor", symbol: Plot.identity}).plot()``````
``````Plot.dotX([
"circle",
"cross",
"diamond",
"square",
"star",
"triangle",
"wye"
], {fill: "currentColor", symbol: Plot.identity}).plot()``````

There is a separate set of default symbols for stroked dots:

js
``````Plot.dotX([
"circle",
"plus",
"times",
"triangle2",
"asterisk",
"square2",
"diamond2",
], {stroke: "currentColor", symbol: Plot.identity}).plot()``````
``````Plot.dotX([
"circle",
"plus",
"times",
"triangle2",
"asterisk",
"square2",
"diamond2",
], {stroke: "currentColor", symbol: Plot.identity}).plot()``````

INFO

The stroked symbols are based on Heman Robinson’s research. There is also a hexagon symbol; it is primarily intended for the hexbin transform. You can even specify a D3 or custom symbol type as an object that implements the symbol.draw(context, size) method.

The dot mark can be combined with the stack transform. The diverging stacked dot plot below shows the age and gender distribution of the U.S. Congress in 2023.

Fork
js
``````Plot.plot({
aspectRatio: 1,
x: {label: "Age (years)"},
y: {
grid: true,
label: "← Women · Men →",
labelAnchor: "center",
tickFormat: Math.abs
},
marks: [
Plot.dot(
congress,
Plot.stackY2({
x: (d) => 2023 - d.birthday.getUTCFullYear(),
y: (d) => d.gender === "M" ? 1 : -1,
fill: "gender",
title: "full_name"
})
),
Plot.ruleY([0])
]
})``````
``````Plot.plot({
aspectRatio: 1,
x: {label: "Age (years)"},
y: {
grid: true,
label: "← Women · Men →",
labelAnchor: "center",
tickFormat: Math.abs
},
marks: [
Plot.dot(
congress,
Plot.stackY2({
x: (d) => 2023 - d.birthday.getUTCFullYear(),
y: (d) => d.gender === "M" ? 1 : -1,
fill: "gender",
title: "full_name"
})
),
Plot.ruleY([0])
]
})``````

INFO

The stackY2 transform places each dot at the upper bound of the associated stacked interval, rather than the middle of the interval as when using stackY. Hence, the first male dot is placed at y = 1, and the first female dot is placed at y = -1.

TIP

The dodge transform can also be used to produce beeswarm plots; this is particularly effective when dots have varying radius.

Dots are sorted by descending radius by default ^0.5.0 to mitigate occlusion; the smallest dots are drawn on top. Set the sort option to null to draw them in input order. Use the checkbox below to see the effect of sorting on a bubble map of U.S. county population.

Fork
js
``````Plot.plot({
projection: "albers-usa",
marks: [
Plot.geo(statemesh, {strokeOpacity: 0.4}),
Plot.dot(counties, Plot.geoCentroid({
r: (d) => d.properties.population,
fill: "currentColor",
stroke: "white",
strokeWidth: 1,
sort: sorted ? undefined : null
}))
]
})``````
``````Plot.plot({
projection: "albers-usa",
marks: [
Plot.geo(statemesh, {strokeOpacity: 0.4}),
Plot.dot(counties, Plot.geoCentroid({
r: (d) => d.properties.population,
fill: "currentColor",
stroke: "white",
strokeWidth: 1,
sort: sorted ? undefined : null
}))
]
})``````

The dot mark can also be used to construct a quantile-quantile (QQ) plot for comparing two univariate distributions.

## Dot options ​

In addition to the standard mark options, the following optional channels are supported:

• x - the horizontal position; bound to the x scale
• y - the vertical position; bound to the y scale
• r - the radius (area); bound to the r (radius) scale, which defaults to sqrt
• rotate - the rotation angle in degrees clockwise
• symbol - the categorical symbol; bound to the symbol scale ^0.4.0

If either of the x or y channels are not specified, the corresponding position is controlled by the frameAnchor option.

The following dot-specific constant options are also supported:

• r - the effective radius (length); a number in pixels
• rotate - the rotation angle in degrees clockwise; defaults to 0
• symbol - the categorical symbol; defaults to circle ^0.4.0
• frameAnchor - how to position the dot within the frame; defaults to middle

The r option can be specified as either a channel or constant. When the radius is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. The radius defaults to 4.5 pixels when using the symbol channel, and otherwise 3 pixels. Dots with a nonpositive radius are not drawn.

The stroke defaults to none. The fill defaults to currentColor if the stroke is none, and to none otherwise. The strokeWidth defaults to 1.5. The rotate and symbol options can be specified as either channels or constants. When rotate is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. When symbol is a valid symbol name or symbol object (implementing the draw method), it is interpreted as a constant; otherwise it is interpreted as a channel. If the symbol channel’s values are all symbols, symbol names, or nullish, the channel is unscaled (values are interpreted literally); otherwise, the channel is bound to the symbol scale.

## dot(data, options) ​

js
``Plot.dot(sales, {x: "units", y: "fruit"})``
``Plot.dot(sales, {x: "units", y: "fruit"})``

Returns a new dot with the given data and options. If neither the x nor y nor frameAnchor options are specified, data is assumed to be an array of pairs [[x₀, y₀], [x₁, y₁], [x₂, y₂], …] such that x = [x₀, x₁, x₂, …] and y = [y₀, y₁, y₂, …].

## dotX(data, options) ​

js
``Plot.dotX(cars.map((d) => d["economy (mpg)"]))``
``Plot.dotX(cars.map((d) => d["economy (mpg)"]))``

Equivalent to dot except that if the x option is not specified, it defaults to the identity function and assumes that data = [x₀, x₁, x₂, …].

If an interval is specified, such as d3.utcDay, y is transformed to (interval.floor(y) + interval.offset(interval.floor(y))) / 2. If the interval is specified as a number n, y will be the midpoint of two consecutive multiples of n that bracket y. Named UTC intervals such as day are also supported; see scale options.

## dotY(data, options) ​

js
``Plot.dotY(cars.map((d) => d["economy (mpg)"]))``
``Plot.dotY(cars.map((d) => d["economy (mpg)"]))``

Equivalent to dot except that if the y option is not specified, it defaults to the identity function and assumes that data = [y₀, y₁, y₂, …].

If an interval is specified, such as d3.utcDay, x is transformed to (interval.floor(x) + interval.offset(interval.floor(x))) / 2. If the interval is specified as a number n, x will be the midpoint of two consecutive multiples of n that bracket x. Named UTC intervals such as day are also supported; see scale options.

## circle(data, options) ^0.5.0​

Equivalent to dot except that the symbol option is set to circle.

## hexagon(data, options) ^0.5.0​

Equivalent to dot except that the symbol option is set to hexagon.

Resources
Observable