Skip to content

Commit 6e1371c

Browse files
authored
Add a new function to easy up creation of choropleths. (#1894)
1 parent 484686c commit 6e1371c

6 files changed

Lines changed: 197 additions & 21 deletions

File tree

src/choropleth.jl

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
export choropleth
2+
3+
4+
"""
5+
choropleth(D, ids, vals; attrib="CODE", cmap="turbo", outline=true, kw...)
6+
choropleth(D, att=""; attrib="", cmap="turbo", outline=true, kw...)
7+
choropleth(D, data::GMTdataset, att=""; attrib="", cmap="turbo", outline=true, kw...)
8+
choropleth(D, data::Dict; outline=true, kw...)
9+
choropleth(D, data::NamedTuple; outline=true, kw...)
10+
choropleth(D, vals::Vector{<:Real}; attrib="CODE", outline=true, kw...)
11+
12+
Create a choropleth map visualization from geographic data with color-coded regions.
13+
14+
### Arguments
15+
- `D`: Geographic data - either a file path (String) or Vector{GMTdataset} containing polygons.
16+
- `ids::Vector{<:AbstractString}`: Region identifiers to match with the attribute values.
17+
- `vals::Vector{<:Real}`: Numeric values to map to colors for each region.
18+
- `att`: Attribute column name (positional argument) for coloring when values are in the dataset.
19+
- `data::GMTdataset`: Dataset containing attributes to join with geographic features (must have text column).
20+
- `data::Dict`: Dictionary mapping region identifiers to values.
21+
- `data::NamedTuple`: Named tuple with region identifiers as keys and values.
22+
23+
### Keywords
24+
- `attrib`: Attribute name to match identifiers (default: "CODE"). Takes precedence over positional `att`.
25+
- `cmap`: A colormap name (default: "turbo") or a GMTcpt colormap.
26+
- `outline`: Draw polygon outlines. `true` (default), `false`, or a pen specification string.
27+
- `kw...`: Additional arguments passed to the plotting function (e.g., `proj`, `region`, `title`).
28+
29+
### Returns
30+
Nothing or a GMTps type (when saving to file).
31+
32+
### Methods
33+
1. **With IDs and values**: `choropleth(D, ids, vals)` - Explicit region IDs and corresponding values.
34+
2. **From attribute**: `choropleth(D, att)` - Extract values from a numeric attribute column in D.
35+
3. **With GMTdataset**: `choropleth(D, data, att)` - Join D with data using text column for matching.
36+
4. **With Dict**: `choropleth(D, Dict("PT"=>10, "ES"=>20))` - Map from region codes to values.
37+
5. **With NamedTuple**: `choropleth(D, (PT=10, ES=20))` - Same as Dict but with named tuple syntax.
38+
6. **Values only**: `choropleth(D, vals)` - Values in order of unique IDs extracted from D.
39+
40+
### Examples
41+
```julia
42+
# Using explicit IDs and values
43+
D = getdcw("PT,ES,FR")
44+
choropleth(D, ["PT","ES","FR"], [10.0, 20.0, 30.0])
45+
46+
# Using a Dict
47+
choropleth(D, Dict("PT"=>10, "ES"=>20, "FR"=>30))
48+
49+
# From attribute in dataset (values stored in polygon attributes)
50+
choropleth("countries.shp", "GDP_PER_CAPITA")
51+
52+
# With custom colormap and no outlines
53+
choropleth(D, ["PT","ES","FR"], [1.0, 2.0, 3.0], cmap="bamako", outline=false)
54+
55+
# The example in "Tutorials"
56+
D = getdcw("US", states=true, file=:ODS);
57+
Df = filter(D, _region=(-125,-66,24,50), _unique=true);
58+
pop = gmtread(TESTSDIR * "assets/uspop.csv");
59+
choropleth(Df, pop, "NAME", show=true)
60+
```
61+
"""
62+
choropleth(D::String, ids::Vector{<:AbstractString}, vals::Vector{<:Real}; attrib::StrSymb="CODE", cmap="turbo", outline=true, kw...) =
63+
choropleth(gmtread(D), ids, vals; attrib=attrib, cmap=cmap, outline=outline, kw...)
64+
function choropleth(D, ids::Vector{<:AbstractString}, vals::Vector{<:Real}; attrib::StrSymb="CODE",
65+
cmap="turbo", outline=true, kw...)
66+
67+
helper_plot_choropleth(D, ids, Float64.(vals), string(attrib), cmap, outline; kw...)
68+
end
69+
70+
71+
# ------------------------------------------------------------------------------------------------------
72+
choropleth(D::String, att::StrSymb=""; attrib::StrSymb="", cmap="turbo", outline=true, kw...) =
73+
choropleth(gmtread(D), att; attrib=attrib, cmap=cmap, outline=outline, kw...)
74+
function choropleth(D::Vector{<:GMTdataset}, att::StrSymb=""; attrib::StrSymb="", cmap="turbo", outline=true, kw...)
75+
76+
_att = (attrib !== "") ? attrib : att # May use either positional or keyword argument. Later takes precedence
77+
ids, vals = extract_ids_vals(D, string(_att))
78+
helper_plot_choropleth(D, ids, vals, string(_att), cmap, outline; kw...)
79+
end
80+
81+
# ------------------------------------------------------------------------------------------------------
82+
choropleth(D::String, data::GMTdataset, att::StrSymb=""; attrib::StrSymb="", cmap="turbo", outline=true, kw...) =
83+
choropleth(gmtread(D), data, att; attrib=attrib, cmap=cmap, outline=outline, kw...)
84+
function choropleth(D::Vector{<:GMTdataset}, data::GMTdataset, att::StrSymb=""; attrib::StrSymb="", cmap="turbo", outline=true, kw...)
85+
isempty(data.text) && error("The 'data' GMTdataset has no text column to guid the joining operation.")
86+
_att = (attrib !== "") ? attrib : att # May use either positional or keyword argument. Later takes precedence
87+
(get(D[1].attrib, _att, "") === "") && error("The 'D' GMTdatasets has no attribute $(_att).")
88+
zvals = polygonlevels(D, data, att=_att)
89+
helper_plot_choropleth(D, String[], view(data, :,1), string(_att), cmap, outline, zvals; kw...)
90+
end
91+
92+
# ------------------------------------------------------------------------------------------------------
93+
choropleth(D::String, data::Dict; outline=true, kw...) = choropleth(gmtread(D), data; outline=outline, kw...)
94+
function choropleth(D::Vector{<:GMTdataset}, data::Dict; outline=true, kw...)
95+
ids = collect(keys(data))
96+
vals = [data[k] for k in ids]
97+
choropleth(D, string.(ids), Float64.(vals); outline=outline, kw...)
98+
end
99+
100+
# ------------------------------------------------------------------------------------------------------
101+
choropleth(D::String, data::NamedTuple; outline=true, kw...) = choropleth(gmtread(D), data; outline=outline, kw...)
102+
choropleth(D::Vector{<:GMTdataset}, data::NamedTuple; outline=true, kw...) = choropleth(D, Dict(pairs(data)); outline=outline, kw...)
103+
104+
# ------------------------------------------------------------------------------------------------------
105+
choropleth(D::String, vals::Vector{<:Real}; attrib::StrSymb="CODE", outline=true, kw...) =
106+
choropleth(gmtread(D), vals; attrib=attrib, outline=outline, kw...)
107+
function choropleth(D::Vector{<:GMTdataset}, vals::Vector{<:Real}; attrib::StrSymb="CODE", outline=true, kw...)
108+
# Extract unique IDs in order of first appearance
109+
att = string(attrib)
110+
ids = String[]
111+
last_id = ""
112+
for d in D
113+
id = get(d.attrib, att, "")
114+
(id == last_id) && continue # skip obvious duplicates
115+
!(id in ids) && push!(ids, id)
116+
last_id = id
117+
end
118+
119+
length(vals) != length(ids) && error("Values length ($(length(vals))) != unique IDs ($(length(ids))): $(join(ids, ", "))")
120+
choropleth(D, ids, vals; attrib=att, outline=outline, kw...)
121+
end
122+
123+
# ------------------------------------------------------------------------------------------------------
124+
function extract_ids_vals(D, attrib::String)
125+
ids, vals = String[], String[]
126+
last_id = ""
127+
for d in D
128+
id = get(d.attrib, attrib, "")
129+
(id == last_id) && continue # skip obvious duplicates
130+
!(id in ids) && (push!(ids, id); push!(vals, d.attrib[attrib]))
131+
last_id = id
132+
end
133+
ind = findall(x -> x === "", vals) # Shit, can't use tryparse() because it f. returns Nothing's instaed of NaN's for ""
134+
if (!isempty(ind))
135+
vals = deleteat!(vals, ind)
136+
ids = deleteat!(ids, ind)
137+
end
138+
ids, parse.(Float64, vals)
139+
end
140+
141+
# ------------------------------------------------------------------------------------------------------
142+
function helper_plot_choropleth(D, ids, vals, attrib::String, cmap, outline, _zvals=Float64[]; kw...)
143+
zvals = (isempty(_zvals)) ? polygonlevels(D, ids, vals, att=attrib) : _zvals
144+
pen_ouline = isa(outline, Bool) ? "0" : outline
145+
C = isa(cmap, GMTcpt) ? cmap : makecpt(T=(minimum(vals), maximum(vals)), C=string(cmap))
146+
plot_outline = (outline != false)
147+
(!plot_outline) && return plot(D; level=zvals, cmap=C, kw...)
148+
d = KW(kw)
149+
do_show, fmt, savefig = get_show_fmt_savefig(d, false)
150+
plot(D; level=zvals, cmap=C)
151+
#plot!(D; pen=pen_ouline, fmt=fmt, savefig=savefig, show=do_show, d...)
152+
d[:fmt]=fmt; d[:savefig]=savefig; d[:show]=do_show; d[:pen]=pen_ouline
153+
common_plot_xyz("", D, "plot", false, false, d)
154+
end
155+
156+
# ------------------------------------------------------------------------------------------------------
157+
choropleth!(args...; kw...) = choropleth(args...; first=false, kw...)

src/choropleth_utils.jl

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -159,21 +159,3 @@ function mk_codes_values(codes::Vector{<:AbstractString}, vals; region::StrSymb=
159159
end
160160
return ky, vl
161161
end
162-
163-
#= --------------------------------------------------------------------------------------------------
164-
function choropleth(polygs::Vector{<:GMTdataset}, colorval::Vector{<:Real}; kwargs...)
165-
d = KW(kwargs)
166-
data_ids, ind = get_segment_ids(polygs)
167-
zvals = make_zvals_vec(polygs, data_ids, colorval)
168-
C::GMTcpt = makecpt(T=(1,6,1)) # <==================================================== ERRADO
169-
((val = find_in_dict(d, [:fmt])[1]) !== nothing) && (fmt = arg2str(val))
170-
fmt::String = ((val = find_in_dict(d, [:fmt])[1]) !== nothing) ? arg2str(val) : "ps"
171-
see = (find_in_dict(d, [:show])[1] !== nothing) ? true : false
172-
val, symb = find_in_dict(d, [:savefig :name])
173-
if (val === nothing)
174-
plot(polygs, Z=zvals, L=true, G="+z", fmt=fmt, show=see, colorbar=true)
175-
else
176-
plot(polygs, Z=zvals, L=true, G="+z", name=string(d[symb]), fmt=fmt, show=see, colorbar=true)
177-
end
178-
end
179-
=#

src/spatial_funs.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ The elements of `zvals` are made up from the `vals`.
1717
The idea here is to match two conditions: `att[1] == ids[n,1] && att[2] == ids[n,2]`
1818
- `vals`: is a vector with the numbers to be used in plot `level` to color the polygons.
1919
- `idvals`: is a GMTdataset with the `text` field containing the ids to match against the `ids` strings.
20-
The first column of `id_vals` must contain the values to be used in `vals`. This is a comodity
21-
function when both the `ids` and `vals` are store in a GMTdataset.
20+
The first column of `idvals` must contain the values to be used in `vals`. This is a comodity
21+
function when both the `ids` and `vals` are stored in a GMTdataset.
2222
2323
### Kwargs
2424
- `attrib` or `att`: Select which attribute to use when matching with contents of the `ids` strings.

src/utils.jl

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1751,4 +1751,21 @@ function cut_icons(; nome="", size=350)
17511751
end
17521752
end
17531753
=#
1754-
include("getdcw.jl")
1754+
1755+
# ------------------------------------------------------------------------------------------------------
1756+
"""
1757+
do_show, fmt, savefig = get_show_fmt_savefig(d; show=false)
1758+
1759+
Many extension/composed plotting functions need to know these but they can only apply them
1760+
in the last plotting comand. Centralise those options fetching in this function.
1761+
"""
1762+
function get_show_fmt_savefig(d, show::Bool=false)
1763+
# 'show' carries the default of the caller for the show-or-not-show
1764+
do_show = ((val = find_in_dict(d, [:show])[1]) === nothing) ? show : !show
1765+
fmt::String = ((val = find_in_dict(d, [:fmt])[1]) !== nothing) ? arg2str(val)::String : FMT[]::String
1766+
savefig = ((val = find_in_dict(d, [:savefig :figname :name])[1]) !== nothing) ? arg2str(val)::String : nothing
1767+
return do_show, fmt, savefig
1768+
end
1769+
1770+
include("getdcw.jl")
1771+
include("choropleth.jl")

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ using InteractiveUtils
6767
@test mapsize2region(proj=(name=:tmerc, center=-177), scale="1:10000000", clon=-177, clat=-21, width=15, height=10)[2] == "t-177/1:10000000"
6868

6969
include("test_fourcolors.jl")
70+
include("test_choroplets.jl")
7071
include("test_isoutlier.jl")
7172
include("test_okadas.jl")
7273
include("test_findpeaks.jl")

test/test_choroplets.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@testset "CHOROPLETHS" begin
2+
3+
println(" CHOROPLETHS")
4+
5+
D = getdcw("PT,ES,FR", file=:ODS);
6+
choropleth(D, ["PT","ES","FR"], [10.0, 20.0, 30.0])
7+
8+
# Using a Dict
9+
choropleth(D, Dict("PT"=>10, "ES"=>20, "FR"=>30))
10+
11+
# With custom colormap and no outlines
12+
choropleth(D, ["PT","ES","FR"], [1.0, 2.0, 3.0], cmap="bamako", outline=false)
13+
14+
# The example in "Tutorials"
15+
D = getdcw("US", states=true, file=:ODS);
16+
Df = filter(D, _region=(-125,-66,24,50), _unique=true);
17+
pop = gmtread(TESTSDIR * "assets/uspop.csv");
18+
choropleth(Df, pop, "NAME", show=false)
19+
end

0 commit comments

Comments
 (0)