Plotting in GeoStats

Hey,

I’ve seen the page here: Visualization · GeoStats.jl

However as a newbie I might be unable to understand how to do this. I have a GeoTable of points and I want to plot the points coloured by a column in the GeoTable.

I can do this using: Makie.viz(data.geometry, colour=data.values). However this doesn’t have equal x and y coordinates which is needed in the UTM CRS I’m using.

I can get the desired result as follows:

fig = Mke.Figure()

ax1 = Mke.Axis(fig[1, 1], aspect = Mke.DataAspect())

x = [coords(pt).x for pt in data.geometry]
y = [coords(pt).y for pt in data.geometry]

Mke.scatter!(ax1, x, y, color = data.values)

fig

However this seems very clunky for something that in R can be done (using the great sf and tmap packages):

tm_shape(data) + tm_dots(col = "values")

I imagine the nicest way to achive this would be some way to just do Mke.geoscatter!(ax1, data.geometry, color = data.values) without the need to extract the x and y values manually.

Thanks so much for any advice you guys can give!

Could you please share the result you are getting with viz? Why is it not matching the x and y coordinates?

Assume a dataframe df with :geometry and :is_trauma_center, typeof IGeometry and Bool. It was read from shapefile with GeoDataTables.read()

using CairoMakie
using GeoMakie
using Geometry Basics
# Function to safely calculate centroids for polygons
function safe_centroid(geom)
    try
        # Try ArchGDAL centroid first
        cent = ArchGDAL.centroid(geom)
        return (ArchGDAL.getx(cent, 0), ArchGDAL.gety(cent, 0))
    catch e
        try
            # Fallback: use GeoInterface to get coordinates and calculate centroid manually
            coords = GeoInterface.coordinates(geom)
            if !isempty(coords) && !isempty(coords[1]) && !isempty(coords[1][1])
                # For polygons, use the first ring (exterior)
                ring_coords = coords[1][1]
                if length(ring_coords) > 0
                    # Calculate centroid as mean of coordinates
                    x_sum = sum(coord[1] for coord in ring_coords)
                    y_sum = sum(coord[2] for coord in ring_coords)
                    n = length(ring_coords)
                    return (x_sum/n, y_sum/n)
                end
            end
        catch e2
            println("Warning: Could not calculate centroid for geometry: $e2")
        end
        return (0.0, 0.0)  # Fallback coordinates
    end
end

# Create the figure
f = Figure(size = (1400, 1000))

# Main map for CONUS
ga = GeoAxis(f[1, 1:3]; dest=conus_crs)

# Plot all counties in a neutral color (light gray)
poly!(ga, conus.geometry, color = :lightgray, strokecolor = :white, strokewidth = 0.5)

# Add dots for counties with Level 1 trauma centers
trauma_counties = subset(conus, :is_trauma_center => ByRow(x -> x === true))
if nrow(trauma_counties) > 0
    # Calculate centroids for trauma center counties
    trauma_centroids = [safe_centroid(geom) for geom in trauma_counties.geometry]
    trauma_x = [coord[1] for coord in trauma_centroids]
    trauma_y = [coord[2] for coord in trauma_centroids]
    
    # Filter out invalid coordinates
    valid_indices = [i for i in 1:length(trauma_x) if trauma_x[i] != 0.0 || trauma_y[i] != 0.0]
    if !isempty(valid_indices)
        valid_x = trauma_x[valid_indices]
        valid_y = trauma_y[valid_indices]
        # Plot dots at trauma center locations
        scatter!(ga, valid_x, valid_y, color = :red, markersize = 8, marker = :circle)
    end
end

hidedecorations!(ga)

With all respect to Dr. Hoffimann, GeoStats may be too heavyweight where its full capabilities aren’t needed.

@beppi if all you need is plot the centroids, avoid all the mess of the previous answer and do:

viz(centroid.(geotable.geometry))

and if you need to display the geometries and their centroids:

viz(geotable.geometry)
viz!(centroid.(geotable.geometry), color="red")

Our philosophy is that you shouldn’t be forced to memorize different plotting functions to visualize the data. You just need to inform the desired geometries to the viz and it will do the job.

Also, I would simply ignore the fallacy that GeoStats.jl is heavyweight.

People don’t realize how heavyweight GDAL and ArchGDAL.jl are. And how problematic their installation can be across platforms.

1 Like

This is the warped version using:

test = viz(
    test.geometry,
    color = test.b1,
    pointsize = 30
)
Mke.save("../test.png", test)

And this is using:

fig = Mke.Figure()

ax1 = Mke.Axis(fig[1, 1], aspect = Mke.DataAspect(), title = "b_1")

x = [coords(pt).x for pt in test.geometry]
y = [coords(pt).y for pt in test.geometry]

# for (dat, ax) in zip([test.b1, test.b2], [ax1, ax2])
for (dat, ax) in zip([test.b1], [ax1])
    Mke.scatter!(ax, x, y, color = dat, markersize = 15)
end

fig
Mke.save("test2.png", fig)

This would be solved in R/ggplot2 using coord_equal.

I believe you just need to pass the Mke.DataAspect() the same way you passed it with the manual Mke.scatter! call.

The viz command is a normal Makie.jl recipe like Mke.scatter, so you can create the Mke.Axis as usual with the data aspect and call viz directly.

Explained in this chapter:

There is an additional issue regarding units: Makie.jl doesn’t have full support for unitful axis across recipes, so we strip out the units of the coordinates before calling the built-in Makie.jl recipe (e.g., scatter) under the hood. At some point in the future, when all Makie.jl recipes become more consistent units-wise, we can review viz to avoid striping units.

Thanks a lot, but I’m sorry I’m not sure how to pass the Makie.DataAspect() or Makie.Axis() instances to viz.

None of its arguments appear to be related to aspect ratio, or axes

Try this (adapted from your code):

ax = Mke.Axis(fig[1, 1], aspect = Mke.DataAspect(), title = "b_1")

viz!(ax, test.geometry, color=test.b1)

Think of viz and viz! as the same thing as Mke.scatter or Mke.scatter!, but more general in the sense that it recognizes the types of geometries it receives.

Highly recommend reading the chapter of the book I shared above.

Ah yeah that makes sense, and works great. Thanks a lot! I’ve read that chapter a few times but I’m still pretty ignorant with this type of plotting (spoiled by ggplot2) so thanks for the guidance.

Once again, thanks a lot for your help and patience!

1 Like

I will consider improving the chapter to include examples where the axis is manually constructed and populated with viz! calls. That might be useful to future readers.

Thanks for raising the question!

1 Like

@beppi added a new section to the chapter with more details about axis customization:

Amazing, thanks so much!

You’ve been so helpful, I’m glad I was able to talk to you about the issues I was having. Feeling great about using Julia going forward since it clearly has a fantastic community

1 Like

Julio, I truly meant no disrespect. I’ve no doubt that the backend of the GeoStats ecosystems improves greatly over GDAL. I had more in mind that among all the wealth I have found much in the way of advanced techniques, but I still haven’t found the geoscience Hello, World, a simple recipe to plot a choropleth map given one of the most widespread formats in existence, shapefiles. I might wish that Makie had a recipe for IGeometry, but it requires conversion to GeometryBasic first. I’m not yet up to rolling my own Makie convert recipe to do it without the routine above. But it may come to that, may the gods protect me.

So far, the only user-lightweight workaround that I have found is a PostGIS back-end to serve WKBs. In fact, it was that which mislead me to miss the conversion from IGeometry issues earlier.

What would cause me to jump feet-first into GeoStats is a short how-to of taking shapefiles and GeoJSON, to start, plot them using vis and embellish them, a bit, in Makie.

I’m old, not to put too fine a point on it. My degrees in geology and regional planning are over 50 years old. But I’m not convinced that flat maps have seen yet their obsolescence. I think GeoStats could find a broad audience for that use case.

As an inducement, I have a book in draft Thematic Mapping with Julia aimed at a general audience of data scientists, journalists and general academic users who are assumed to have neither GIS nor programming backgrounds. My aim is to provide the minimum Julia necessary to produce high-quality thematic maps, such as choropleths. Anything that I can find to ease the learning experience on that is what I will feature.

using GeoStats
using GeoIO


import GLMakie as Mke

geotable = GeoIO.load("file.shp")

viewer(geotable)

?

One of the first things I did in Julia when I started as a complete novice was to make a chloropleth map. I recall the basics being pretty straightforward. I loaded shape files with geoIO, manipulated the GeoTable a bit and used Makie to plot. I had no skill, so it must have been comparatively easy!

I’m travelling, so can’t easily offer an mwe. However, this thread may help: Scaling viz plots in GeoStats - #19 by TimG

2 Likes

Notice that potencial readers coming from Python and R communities have no reason whatsoever to switch to Julia for GIS work if all they get are the same old GDAL+PROJ scripts and their inherent limitations.

Your book could target a modern audience, i.e., it could promote native Julia packages that bring something new to the mapping world. Our treatment of coordinate systems and geospatial domains is quite unique across all programming languages and you might be missing an opportunity here.

Good luck with the writing.

1 Like

julia> @time viewer(geotable)
102.084344 seconds (684.73 M allocations: 47.473 GiB, 5.39% gc time)

from that script in a clean environment on an M3 Ultra

using https://www2.census.gov/geo/tiger/GENZ2024/shp/cb_2024_us_state_500k.zip

I hadn’t found viewer(), but I’m still dissatisfied—it shouldn’t impute a z-axis missing from the data.

With the annoyance of the intermediate step of converting IGeometry to GeometryBasic, I get

with

julia> @time poly!(ga, all_polygons, color= :white, strokecolor= :black, strokewidth=0.5)
  0.088717 seconds (17.18 k allocations: 17.771 MiB)
Poly{Tuple{Vector{Polygon{2, Float32, P, L, V} where {P<:AbstractPoint{2, Float32}, L<:(AbstractVector{<:GeometryBasics.Ngon{2, Float32, 2, P}}), V<:AbstractVector{L}}}}}

I’m sure it can be done, and I’m even sure that I can do it, but I’m not sure that I will clearly be able to explain it to my readers.

If you like, we could take this offline?

If you read the GDSJL book I asked you to read a couple of times you will understand what is happening.

Only after you read all chapters of the book.

Back home now. Here is a quick MWE based on the shapefile you linked. This file includes boundaries for several areas outside the contiguous USA (eg Guam), so I’ve excluded these. The shapefile uses the ‘NAD83’ reference system, and I’ve stuck to this in the MWE.

using GeoIO
using GeoStats

import CairoMakie as Mke

gt = GeoIO.load("cb_2024_us_state_500k.shp") |> Filter(row -> row.NAME ∉ ["Alaska", "Hawaii", "Guam", "Puerto Rico", "American Samoa", "United States Virgin Islands", "Commonwealth of the Northern Mariana Islands"])

fig = Mke.Figure()
ax = Mke.Axis(fig[1, 1], title = "US 'Lower 48' States")
viz!(ax, gt.geometry, color=1:nrow(gt), segmentcolor="black", showsegments=true, segmentsize=0.3f0)
ax.aspect = Mke.DataAspect()
Mke.display(fig)

This produces a simple chloropleth map straight from the shapefile:

This is obviously a much simpler task than the OP, which @juliohm solved, but it shows how easy it is to create a “hello world” map.

2 Likes