6

I try to make a Perpilou's map on QGIS, I managed to get a working code but it is not generic at all, so I am looking for ways to improve it.

A Perpillou map is a (old-fashioned) cartographic method of representing multivariate quantitative data quite effectively. Its general operation is as follows: We start with a set of quantitative variables whose proportions we want to represent and we use these values to generate a filling pattern (see figure 1). These patterns are always similar, we represent each variable by a band whose width is proportional to the value of the variable and we juxtapose all these bands. It is important to note that the total width of the figure (e.g. Band A + B + C) must always be the same size (manualy choosed). It is only the part occupied by each of the different bands that should vary. Figure 1 illustrates what this representation would look like if variable A represented 50% of the total, variable B 10% and variable C 40%.

Patern

This pattern, must then be applied for each polygon (fig 2). As the polygons are not necessarily the same size as the pattern, they are repeated and if they are larger than the pattern, they are cut off (as shown on the right in fig 2). The only constraint is that the strips must be aligned vertically to avoid shifts that are painful to read.

enter image description here

I managed to approach this result on a regular mesh using the geometry generator. In this case the size of pattern is the size of polygon (ei. xmax _xmin). I generate the geometry of each band (A, B or C on the example) with this functions :

Geometry for the first band (A) :

 make_polygon(
    make_line(
        # Point below left
        make_point(
            x_min($geometry), 
            y_min($geometry)),
        # Point Up left
        make_point(
            x_min($geometry), 
            y_max($geometry)),
        # Point Up right
        make_point(
            x_min($geometry) + ("A" / ("A" + "B" + "C"))*(x_max($geometry)-x_min($geometry))), 
            y_max($geometry)),
        # Point Up below
        make_point(
            x_min($geometry) + ("A" / ("A" + "B" + "C"))*(x_max($geometry)-x_min($geometry))), 
            y_min($geometry)),
        # First point to close the polygon
        make_point(
            x_min($geometry), 
            y_min($geometry))
        )
)

Geometry for the second band (B) :

 make_polygon(
    make_line(
        # Point below left
        make_point(
            x_min($geometry) + ("A" / ("A" + "B" + "C")) *(x_max($geometry)-x_min($geometry)), 
            y_min($geometry)),
        # Point Up left
        make_point(
            x_min($geometry) + ("A" / ("A" + "B" + "C")) *(x_max($geometry)-x_min($geometry)), 
            y_max($geometry)),
        # Point Up right
        make_point(
            x_min($geometry) + ("B" / ("A" + "B" + "C"))*(x_max($geometry)-x_min($geometry))), 
            y_max($geometry)),
        # Point Up below
        make_point(
            x_min($geometry) + ("B" / ("A" + "B" + "C"))*(x_max($geometry)-x_min($geometry))), 
            y_min($geometry)),
        # First point to close the polygon
        make_point(
            x_min($geometry) + ("A" / ("A" + "B" + "C")) *(x_max($geometry)-x_min($geometry)), 
            y_min($geometry))
        )
)

Geometry for the last band (C) :

make_polygon(
    make_line(
        # Point below left
        make_point(
            x_min($geometry) + (("A" + "B") / ("A" + "B" + "C")) *(x_max($geometry)-x_min($geometry)), 
            y_min($geometry)),
        # Point Up left
        make_point(
            x_min($geometry) + (("A" + "B")) / ("A" + "B" + "C")) *(x_max($geometry)-x_min($geometry)), 
            y_max($geometry)),
        # Point Up right
        make_point(
            x_min($geometry) + ("C" / ("A" + "B" + "C"))*(x_max($geometry)-x_min($geometry))), 
            y_max($geometry)),
        # Point Up below
        make_point(
            x_min($geometry) + ("C" / ("A" + "B" + "C"))*(x_max($geometry)-x_min($geometry))), 
            y_min($geometry)),
        # First point to close the polygon
        make_point(
            x_min($geometry) + (("A" + "B") / ("A" + "B" + "C")) *(x_max($geometry)-x_min($geometry)), 
            y_min($geometry))
        )
)

This results in the following rendering :

enter image description here

I am quite satisfied with the result, but not with the method to produce it. Indeed :

  1. This code can only works when the polygons have the same size
  2. It's necessary to modify the function as soon as the variables (or their number) change

Is there a way to correct these problems and do better in QGIS using only the symbology tools ?

1
  • And your feature geometries must be grid cells otherwise they can overlap : imagine a two neighboring triangles, one up and one down, the xmin of the right triangle will be at the middle of the left triangle. Commented Oct 8, 2021 at 11:26

2 Answers 2

3

Here is a not perfect solution using a custom SVG.

It would create on the fly a SVG symbol with the 3 bands, and this svg is then rendered as a point pattern.

Annoying aspect: to change the with of the 3 bands altogether, you would have to update the svg code

It has issues: I couldn't keep it systematically starting at the top left corner, so the 1st band could be chopped on the left, and could feel like moving as you pan the map. This can probably be overcome.

enter image description here

so:

  1. do a geometry generator, type polygon, with the expression union($geometry, make_point(-180,-90)) (it will help - not solve - with anchoring the pattern while panning the map)
  2. add a point pattern fill,
  3. sub-type SVG marker
  4. Use the data override for the svg and enter the following expression. It will read the band width from the variables A,B and C. Note that the 2nd and 3rd band are "moved" according to the previous bands values - which are assumed to all sum up to 100
'data:image/svg+xml;utf8,
<svg version="1.1" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg">
    <g fill="#000000" fill-opacity="0.7">
        <rect width="'||(1.0*A/(A+B+C))||'" height="1" />
    </g>
    <g transform="translate('|| (A/100.0) || ' 0)" fill="#ff5733" fill-opacity="0.7">
        <rect width="'||(1.0*B/(A+B+C))||'" height="1" />
    </g>
    <g transform="translate('|| ((A+B)/100.0) || ' 0)" fill="#3349ff" fill-opacity="0.7">
        <rect width="'||(1.0*C/(A+B+C))||'" height="1" />
    </g>
</svg>'
  1. set the symbol size so it fills the geometries
2

I've a more "effortless" solution.

  1. You have to create a Geometry Generator Symbol layer for each of your variable

  2. Create the Geometry Generator base code as :

with_variable('varray', array(
"var_a", "var_b", "var_c"
), -- list of all variables comma separated here
with_variable('serial', generate_series(0, array_length(@varray) - 1),
with_variable('width',  x_max($geometry) - x_min($geometry),
with_variable('var_width', array_foreach(@varray, @element / array_sum(@varray) * @width),
with_variable('x_end',
array_foreach(
    @serial,
    xmin($geometry) +
    CASE WHEN @element = 0 THEN array_get(@var_width, 0)
    ELSE
        array_sum(
            array_slice(@var_width, 0, @element)
        )
    END
),
with_variable('x_start', array_slice(array_prepend(@x_end, xmin($geometry)), 0, array_length(@varray) - 1),
array_foreach(
    @serial,
    make_rectangle_3points(
        make_point(array_get(@x_start, @element), ymax($geometry)),
        make_point(array_get(@x_end, @element), ymax($geometry)),
        make_point(array_get(@x_end, @element), ymin($geometry))
    )
)[
# -- symbol layer id here, starting from 0
]
))))))
  1. Sets your variable list instead of "var_a", "var_b", "var_c" here
  2. Copy the base code and paste it for each Symbol Layer, and instead of the # at the bottom of the code, put for the first symbol layer a 0, for the second a 1, for the third a 2, etc.
  3. Sets a different color for each of your symbol layer, that's it !
2
  • The step 4 is not very clear to me. Can you give me an example ?
    – Atm
    Commented Oct 11, 2021 at 12:39
  • See the before last one row in the code # -- symbol layer id here, starting from 0, for a single symbology, you can add Symbol Layers as you want with the big green +, and for each, choose the geometry generator. So, for the top symbol layer, replace the # by 0, for the next by 1, etc. Is it more clear ? Commented Oct 11, 2021 at 12:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.