# R lines() Function: How to Add Lines to a Plot

The R `lines()` function adds line segments to a plot that already exists. You call `plot()` first to create the axes and the first series, then call `lines()` to overlay one or more additional series on the same coordinate system. This is the standard base R way to compare two or more trends on one chart.

## Quick Answer

- `lines(x, y, ...)` draws connected line segments through the points `(x, y)` on the current plot.
- It never creates a new plot. If no plot is open, the call fails or draws nothing useful.
- The first series usually comes from `plot()`, and every later series comes from `lines()`.
- Arguments such as `col`, `lty`, `lwd`, and `type` control appearance and are passed through to the underlying drawing code.
- `lines()` is generic, so it also works on special objects such as step functions [1].

## Syntax

`lines(x, y = NULL, type = "l", ...)`

| Argument | Required? | Meaning |
|---|---|---|
| `x` | Yes | Numeric vector of x coordinates, or an object with a `lines` method |
| `y` | No | Numeric vector of y coordinates, same length as `x`. If omitted, `x` is plotted against its index |
| `type` | No | Line style: `"l"` for lines, `"b"` for points joined by lines, `"o"` for overplotted points and lines, `"h"` for vertical lines, `"s"` and `"S"` for steps |
| `col` | No | Line color, as a name or hex code |
| `lty` | No | Line type, such as `1` solid, `2` dashed, `3` dotted |
| `lwd` | No | Line width, a positive number |
| `...` | No | Further graphical parameters passed to the drawing functions |

The `type` default is `"l"`, so plain `lines(x, y)` draws a connected line. When `x` is a time series or another structured object, `y` can be omitted and the method extracts both coordinates.

## How It Works

Base R graphics are drawn on a device with a current coordinate system. `plot()` sets up that system from the data ranges you pass, then draws. `lines()` reuses the same system and adds segments to it. Nothing about the axes, labels, or limits changes.

That behavior has one direct consequence. The y range of the first `plot()` call must cover every series you plan to add. If the second series sits outside the range, `lines()` still draws it, but the segments fall outside the visible plot area and you see nothing. Setting `ylim` explicitly on the first call is the reliable fix.

Because `lines()` is a generic function, it dispatches to class-specific methods. For a `stepfun` object, for example, the method accepts `xval` to control where the function is evaluated and `col.hor` to color the horizontal segments [1]. The same call pattern works for other classes that define a `lines` method.

The order of drawing matters for readability. Later calls paint over earlier ones, so draw the series you want visible on top last. Color and line type are the main tools for telling series apart, and a matching `legend()` call makes the chart self-explanatory.

## Worked Example

The dataset holds daily high temperatures in Celsius for two cities over 10 days.

| day | riverside | hillcrest |
|---|---|---|
| 1 | 12.0 | 9.5 |
| 2 | 14.5 | 11.0 |
| 3 | 13.2 | 10.2 |
| 4 | 15.8 | 12.8 |
| 5 | 17.1 | 13.5 |
| 6 | 16.4 | 12.9 |
| 7 | 18.0 | 14.1 |
| 8 | 19.2 | 15.0 |
| 9 | 17.5 | 13.8 |
| 10 | 16.0 | 12.4 |

The steps are straightforward. Riverside's mean high is

$$\frac{12.0 + 14.5 + 13.2 + 15.8 + 17.1 + 16.4 + 18.0 + 19.2 + 17.5 + 16.0}{10} = 15.9700$$

Hillcrest's mean high is

$$\frac{9.5 + 11.0 + 10.2 + 12.8 + 13.5 + 12.9 + 14.1 + 15.0 + 13.8 + 12.4}{10} = 12.5200$$

Riverside peaks at 19.2000 C on day 8. Hillcrest peaks at 15.0000 C on day 8. On day 10 the gap is 16.0000 - 12.4000 = 3.6000 C.

The code builds the vectors, draws Riverside with `plot()`, then overlays Hillcrest with `lines()`. Both series use `type = "b"` so the points and the connecting lines are both visible.

```r
day <- 1:10
riverside <- c(12.0, 14.5, 13.2, 15.8, 17.1, 16.4, 18.0, 19.2, 17.5, 16.0)
hillcrest <- c(9.5, 11.0, 10.2, 12.8, 13.5, 12.9, 14.1, 15.0, 13.8, 12.4)
plot(day, riverside, type = "b", col = "#1d4ed8", pch = 19,
     ylim = c(8, 20), xlab = "Day", ylab = "High temp (C)",
     main = "Daily high temperatures: Riverside vs Hillcrest")
lines(day, hillcrest, type = "b", col = "#ea580c", pch = 17)
legend("topleft", legend = c("Riverside", "Hillcrest"),
       col = c("#1d4ed8", "#ea580c"), pch = c(19, 17))
```

Output: Riverside mean 15.9700 C, Hillcrest mean 12.5200 C. Peaks are 19.2000 C and 15.0000 C, both on day 8. The day-10 gap is 3.6000 C.

The `ylim = c(8, 20)` argument is doing real work here. Riverside reaches 19.2 and Hillcrest drops to 9.5, so a range of 8 to 20 contains both series with a little breathing room.

## More Examples

**Add a horizontal reference line.** Use `abline()` for straight reference lines, since it takes an intercept and slope directly. Use `lines()` when the reference is a computed series.

```r
plot(day, riverside, type = "l", ylim = c(8, 20))
lines(day, rep(15.97, 10), col = "gray40", lty = 2)
```

**Add a smoothed trend.** Fit a model, predict over the same x values, and draw the fitted values on top of the raw points.

```r
fit <- lm(riverside ~ day)
plot(day, riverside, pch = 19, ylim = c(8, 20))
lines(day, fitted(fit), col = "red", lwd = 2)
```

**Draw several series in a loop.** Build the vectors first, then loop over them so each one is added with its own color.

```r
series <- list(riverside, hillcrest)
cols <- c("#1d4ed8", "#ea580c")
plot(day, riverside, type = "l", ylim = c(8, 20), col = cols[1])
for (i in 2:length(series)) lines(day, series[[i]], col = cols[i])
```

If you need to assemble the coordinate vectors from separate values, the article on [c() in R: How to Create Vectors (With Examples)](/blog/data-analysis/c-function-in-r-create-vectors) covers the concatenation step. For repeated or constant baselines, [R ones: How to Create a Vector of Ones in R](/blog/data-analysis/r-ones-vector) shows how to build a constant vector of the right length.

## Errors and How to Fix Them

**"plot.new has not been called yet."** You called `lines()` before any plot existed. Call `plot()` first, or open a device with `plot.new()` and set the coordinate system with `plot.window()`.

**"Error in xy.coords(x, y): 'x' and 'y' lengths differ."** The two vectors have different lengths. Check with `length(x)` and `length(y)` and trim or pad the shorter one.

**"Error in plot.xy(xy.coords(x, y), type = type, ...): invalid plotting symbol."** A `pch` value is out of range or not a valid character. Use a number from 0 to 25 or a single character.

**Nothing appears on the plot.** The added series lies outside the current axis limits. Reissue the first `plot()` call with a wider `ylim` or `xlim` that covers every series.

**Lines appear but the legend is wrong.** The legend colors and point characters must match the values used in `plot()` and `lines()`. Keep them in the same order in both places.

## Common Mistakes

- **Forgetting `ylim` on the first plot.** The default range covers only the first series, so later series get clipped. Fix: compute the range across all series and pass it explicitly.
- **Calling `lines()` with only one vector when the x values are not 1, 2, 3.** A single numeric vector is drawn against its index, so it will not line up with a plot whose x values differ. Fix: supply both `x` and `y`.
- **Using `lines()` to start a chart.** It adds to an existing plot and cannot create one. Fix: start with `plot()` or `plot.new()` plus `plot.window()`.
- **Mixing up `lines()` and `abline()`.** `abline()` takes intercept and slope for straight reference lines. Fix: use `lines()` for data-driven series and `abline()` for simple reference lines.
- **Reusing the same color for every series.** Overlapping lines become unreadable. Fix: vary `col` and `lty`, and add a legend.
- **Assuming `type = "b"` and `type = "o"` are identical.** In `"b"` the line stops short of the points, in `"o"` the line runs through them. Fix: pick the one that matches the look you want.

## Limitations

`lines()` only draws on the current plot and the current device. It cannot change axis limits, add a second y axis, or rescale existing data. If a new series needs a different scale, you have to transform the values yourself and label the axis accordingly, which is easy to misread.

Base R graphics redraw from scratch. There is no retained object you can edit later, so changing a line means rerunning the drawing code. For charts that need many layers, interactive updates, or precise theme control, a grammar-of-graphics package is usually a better fit. `lines()` remains the fastest option for quick overlays and for scripts where base graphics are already in use.

## Frequently Asked Questions

### What is the difference between lines() and plot() in R?

`plot()` creates a new plot, including the axes, labels, and the first series. `lines()` adds line segments to a plot that already exists and leaves the axes untouched. In practice you call `plot()` once and `lines()` for every additional series.

### How do I add multiple lines to the same plot?

Call `plot()` for the first series, then call `lines()` once per additional series. Give each call its own `col` and `lty` so the series are distinguishable, and set `ylim` on the first call to cover all of them. Finish with a `legend()` call that matches the colors and line types.

### Why does my lines() call produce no visible line?

The most common cause is that the series falls outside the current axis limits. Reissue the first `plot()` call with a `ylim` that spans every series. A second cause is a `type` value that draws nothing visible, or a line width or color that blends into the background.

### Can lines() draw vertical or stepped lines?

Yes. `type = "h"` draws vertical lines from each point down to the x axis. `type = "s"` and `type = "S"` draw step functions, and the `stepfun` method accepts `xval` to control where the function is evaluated [1].

### Does lines() work with time series and other objects?

Yes. `lines()` is generic, so it dispatches to a method when the input has a class with one defined. For a time series object you can often omit `y` and let the method extract the coordinates. For classes without a method, pass plain numeric vectors instead.

## References

1. [R: Plot Step Functions](https://stat.ethz.ch/R-manual/R-devel/library/stats/html/plot.stepfun.html)

## Further Reading

- [Wickham H (2014). Tidy Data. Journal of Statistical Software](https://doi.org/10.18637/jss.v059.i10)
- [Wickham H, Averick M, Bryan J et al. (2019). Welcome to the Tidyverse. Journal of Open Source Software](https://doi.org/10.21105/joss.01686)
- [An Introduction to R (R Core Team)](https://cran.r-project.org/doc/manuals/r-release/R-intro.html)
- [Wickham H, Cetinkaya-Rundel M, Grolemund G. R for Data Science (2e)](https://r4ds.hadley.nz/)
- [ggplot2 Reference](https://ggplot2.tidyverse.org/reference/index.html)

## Related Articles

- [c() in R: How to Create Vectors (With Examples)](/blog/data-analysis/c-function-in-r-create-vectors)
- [R ones: How to Create a Vector of Ones in R](/blog/data-analysis/r-ones-vector)
- [R letters Function: Generate Lowercase Letter Sequences](/blog/data-analysis/r-letters-function)
- [How to Collapse Data in R (Step by Step)](/blog/data-analysis/how-to-collapse-data-in-r)
- [F-Test in R: How to Compare Variances (With Example)](/blog/data-analysis/f-test-in-r)