Handling Color Overflow in Brewer Palettes¶

ColorBrewer palettes have a limited number of colors (typically 8–12).
When your data has more categories than the palette provides, a larger palette is created either by interpolating the original colors, or by cycling, or by generating additional colors.

By default, qualitative palettes use "generate" (creating new colors with varied luminance),
while sequential and diverging palettes use "interpolate".

The overflow parameter controls the way the extended palette is generated: "interpolate", "cycle", or "generate".

In [1]:
%useLatestDescriptors
%use lets-plot
In [2]:
val letters = ('A'..'Z').map { it.toString() }
val data = mapOf(
    "letter" to letters,
    "x" to (0 until letters.size).toList()
)

val p = letsPlot(data) { x = "x"; color = "letter" } +
    geomPoint(size = 7) +
    guides(color = guideLegend(nrow = 9))

Default: "generate"¶

Additional colors are created based on the original palette colors.

In [3]:
p + scaleColorBrewer(palette = "Set1")
Out[3]:
0 5 10 15 20 25 -0.4 -0.2 0 0.2 0.4 y x letter A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Cycle Palette Colors¶

In [4]:
p + scaleColorBrewer(palette = "Set1", overflow = "cycle")
Out[4]:
0 5 10 15 20 25 -0.4 -0.2 0 0.2 0.4 y x letter A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Interpolate Palette Colors¶

In [5]:
p + scaleColorBrewer(palette = "Set1", overflow = "interpolate")
Out[5]:
0 5 10 15 20 25 -0.4 -0.2 0 0.2 0.4 y x letter A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Choosing the Right Palette for Many Categories¶

When displaying a large number of categories (like countries on a world map), not all Brewer palettes work equally well with the generate overflow.
Palettes with high contrast colors like Set1 can produce jarring results when extended.
Softer palettes like Pastel1 work better, while Paired and Set3 tend to produce the most visually balanced results for large categorical datasets.

In [6]:
@file:Repository("https://repo.osgeo.org/repository/release/")
@file:DependsOn("org.geotools:gt-shapefile:33.2")
@file:DependsOn("org.geotools:gt-epsg-hsql:33.2")
In [7]:
%use lets-plot-gt
%use dataframe
In [8]:
import org.geotools.data.shapefile.ShapefileDataStoreFactory
import java.net.URL

val factory = ShapefileDataStoreFactory()

fun loadShp(name: String) =
    factory.createDataStore(
        URL("https://raw.githubusercontent.com/JetBrains/lets-plot-kotlin/master/docs/examples/shp/$name/$name.shp")
    ).featureSource.features.toSpatialDataset()

val worldSds = loadShp("naturalearth_lowres")

val df = worldSds.toDataFrame()
df.take(5)
Out[8]:

DataFrame: rowsCount = 5, columnsCount = 6

continentpop_estgdp_md_estnameiso_a3geometry
Oceania920938,0000008374,000000FijiFJI{"type":"MultiPolygon","coordinates":...
Africa53950935,000000150600,000000TanzaniaTZA{"type":"MultiPolygon","coordinates":...
Africa603253,000000906,500000W. SaharaESH{"type":"MultiPolygon","coordinates":...
North America35623680,0000001674000,000000CanadaCAN{"type":"MultiPolygon","coordinates":...
North America326625791,00000018560000,000000United States of AmericaUSA{"type":"MultiPolygon","coordinates":...
In [9]:
val world = letsPlot() +
    geomMap(map = worldSds, color = "white", size = 0.5) { fill = "name" } +
    coordMap(ylim = -60 to null) +
    ggsize(900, 500) +
    theme().legendPositionNone() +
    ggtb()

The Default Qualitative Palette (Set1)¶

Works, but not ideal for many categories.

In [10]:
world + scaleFillBrewer()
Out[10]:
-150 -100 -50 0 50 100 150 -60 -40 -20 0 20 40 60 80 lat lon

Pastel1¶

A softer palette that works better with many categories.

In [11]:
world + scaleFillBrewer(palette = "Pastel1")
Out[11]:
-150 -100 -50 0 50 100 150 -60 -40 -20 0 20 40 60 80 lat lon

Paired and Set3¶

These palettes produce the most balanced results for large categorical datasets.

In [12]:
// sort countries to ensure a consistent color palette order
val sortedByPop = df.sortBy("pop_est")["name"].toList().filterNotNull()
In [13]:
world + scaleFillBrewer(palette = "Paired", limits = sortedByPop)
Out[13]:
-150 -100 -50 0 50 100 150 -60 -40 -20 0 20 40 60 80 lat lon
In [14]:
world + scaleFillBrewer(palette = "Set3", limits = sortedByPop)
Out[14]:
-150 -100 -50 0 50 100 150 -60 -40 -20 0 20 40 60 80 lat lon