Skip to content

Commit 5c43e0b

Browse files
committed
Add method to assign colors in order to maximize the distance between
features assigned to same color This is the most cartographically pleasing color arrangement in my opinion as it creates a nicely distributed set of colors.
1 parent 2fd78b8 commit 5c43e0b

File tree

3 files changed

+44
-25
lines changed

3 files changed

+44
-25
lines changed

python/plugins/processing/algs/help/qgis.yaml

+7-3
Original file line numberDiff line numberDiff line change
@@ -545,11 +545,15 @@ qgis:texttofloat: >
545545
This algorithm modifies the type of a given attribute in a vector layer, converting a text attribute containing numeric strings into a numeric attribute.
546546

547547
qgis:topologicalcoloring: >
548-
This algorithm assigns a color index to polygon features in such a way that no adjacent polygons share the same color index.
548+
This algorithm assigns a color index to polygon features in such a way that no adjacent polygons share the same color index, whilst minimizing the number of colors required.
549549

550-
The algorithm attempts to assign colors so that the total number of colors required is minimized, whilst keeping the count of features assigned to each individual color index balanced. A minimum number of colors can be specified if desired.
550+
The algorithm allows choice of method to use when assigning colors. The default method attempts to assign colors so that the count of features assigned to each individual color index is balanced.
551551

552-
The color index is saved to a new attribute named color_id.
552+
The 'by assigned area' mode instead assigns colors so that the total area of features assigned to each color is balanced. This mode can be useful to help avoid large features resulting in one of the colors appearing more dominant on a colored map.
553+
554+
The 'by distance between colors' mode will assign colors in order to maximize the distance between features of the same color. This mode helps to create a more uniform distribution of colors across a map.
555+
556+
A minimum number of colors can be specified if desired. The color index is saved to a new attribute named color_id.
553557

554558
qgis:translate: >
555559
This algorithm moves the geometries within a layer, by offsetting them with a specified x and y displacement.

python/plugins/processing/algs/qgis/TopoColors.py

+35-22
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@
2727

2828
import os
2929
import operator
30-
from enum import Enum
30+
import sys
3131

3232
from collections import defaultdict, deque
3333

3434
from qgis.core import (QgsField,
3535
QgsGeometry,
3636
QgsSpatialIndex,
37+
QgsPointV2,
3738
NULL)
3839

3940
from qgis.PyQt.QtCore import (QVariant)
@@ -47,18 +48,13 @@
4748

4849
pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
4950

50-
class BalanceMethod(Enum):
51-
BY_COUNT = 0
52-
BY_AREA = 1
53-
BY_DISTANCE = 2
5451

5552
class TopoColor(GeoAlgorithm):
5653
INPUT_LAYER = 'INPUT_LAYER'
57-
MIN_COLORS='MIN_COLORS'
58-
BALANCE='BALANCE'
54+
MIN_COLORS = 'MIN_COLORS'
55+
BALANCE = 'BALANCE'
5956
OUTPUT_LAYER = 'OUTPUT_LAYER'
6057

61-
6258
def defineCharacteristics(self):
6359
self.name, self.i18n_name = self.trAlgorithm('Topological coloring')
6460
self.group, self.i18n_group = self.trAlgorithm('Cartographic tools')
@@ -69,20 +65,20 @@ def defineCharacteristics(self):
6965
self.addParameter(ParameterNumber(self.MIN_COLORS,
7066
self.tr('Minimum number of colors'), 1, 1000, 4))
7167
balance_by = [self.tr('By feature count'),
72-
self.tr('By assigned area'),
73-
self.tr('By distance between colors')]
68+
self.tr('By assigned area'),
69+
self.tr('By distance between colors')]
7470
self.addParameter(ParameterSelection(
7571
self.BALANCE,
7672
self.tr('Balance color assignment'),
77-
balance_by))
73+
balance_by, default=0))
7874

7975
self.addOutput(OutputVector(self.OUTPUT_LAYER, self.tr('Colored'), datatype=[dataobjects.TYPE_VECTOR_POLYGON]))
8076

8177
def processAlgorithm(self, feedback):
8278
layer = dataobjects.getObjectFromUri(
8379
self.getParameterValue(self.INPUT_LAYER))
8480
min_colors = self.getParameterValue(self.MIN_COLORS)
85-
balance_by = BalanceMethod(self.getParameterValue(self.BALANCE))
81+
balance_by = self.getParameterValue(self.BALANCE)
8682

8783
fields = layer.fields()
8884
fields.append(QgsField('color_id', QVariant.Int))
@@ -93,7 +89,7 @@ def processAlgorithm(self, feedback):
9389
layer.wkbType(),
9490
layer.crs())
9591

96-
features = {f.id():f for f in vector.features(layer)}
92+
features = {f.id(): f for f in vector.features(layer)}
9793

9894
topology, id_graph = self.compute_graph(features, feedback)
9995
feature_colors = ColoringAlgorithm.balanced(features,
@@ -131,7 +127,7 @@ def compute_graph(features, feedback, create_id_graph=False):
131127
id_graph = Graph(sort_graph=True)
132128

133129
# skip features without geometry
134-
features_with_geometry = { f_id: f for (f_id, f) in features.items() if f.hasGeometry() }
130+
features_with_geometry = {f_id: f for (f_id, f) in features.items() if f.hasGeometry()}
135131

136132
total = 70.0 / len(features_with_geometry)
137133
index = QgsSpatialIndex()
@@ -167,10 +163,10 @@ def compute_graph(features, feedback, create_id_graph=False):
167163
class ColoringAlgorithm:
168164

169165
@staticmethod
170-
def balanced(features, graph, feedback, balance=BalanceMethod.BY_COUNT, min_colors = 4):
166+
def balanced(features, graph, feedback, balance=0, min_colors=4):
171167
feature_colors = {}
172168
# start with minimum number of colors in pool
173-
color_pool = set(range(1, min_colors+1))
169+
color_pool = set(range(1, min_colors + 1))
174170

175171
# calculate count of neighbours
176172
neighbour_count = defaultdict(int)
@@ -201,26 +197,43 @@ def balanced(features, graph, feedback, balance=BalanceMethod.BY_COUNT, min_colo
201197
# from the existing colors, work out which are available (ie non-adjacent)
202198
available_colors = color_pool.difference(adjacent_colors)
203199

204-
feature_color=-1
200+
feature_color = -1
205201
if len(available_colors) == 0:
206202
# no existing colors available for this feature, so add new color to pool and repeat
207203
min_colors += 1
208-
return ColoringAlgorithm.balanced(features,graph,feedback,balance,min_colors)
204+
return ColoringAlgorithm.balanced(features, graph, feedback, balance, min_colors)
209205
else:
210-
if balance==BalanceMethod.BY_COUNT:
206+
if balance == 0:
211207
# choose least used available color
212208
counts = [(c, v) for c, v in color_counts.items() if c in available_colors]
213209
feature_color = sorted(counts, key=operator.itemgetter(1))[0][0]
214210
color_counts[feature_color] += 1
215-
elif balance==BalanceMethod.BY_AREA:
211+
elif balance == 1:
216212
areas = [(c, v) for c, v in color_areas.items() if c in available_colors]
217213
feature_color = sorted(areas, key=operator.itemgetter(1))[0][0]
218214
color_areas[feature_color] += features[feature_id].geometry().area()
219-
#elif balance==BalanceMethod.BY_DISTANCE:
215+
elif balance == 2:
216+
min_distances = {c: sys.float_info.max for c in available_colors}
217+
this_feature_centroid = QgsPointV2(features[feature_id].geometry().centroid().geometry())
220218

219+
# find features for all available colors
220+
other_features = {f_id: c for (f_id, c) in feature_colors.items() if c in available_colors}
221221

222-
feature_colors[feature_id] = feature_color
222+
# loop through these, and calculate the minimum distance from this feature to the nearest
223+
# feature with each assigned color
224+
for other_feature_id, c in other_features.items():
225+
other_geometry = features[other_feature_id].geometry()
226+
other_centroid = QgsPointV2(other_geometry.centroid().geometry())
227+
228+
distance = this_feature_centroid.distanceSquared(other_centroid)
229+
if distance < min_distances[c]:
230+
min_distances[c] = distance
223231

232+
# choose color such that minimum distance is maximised! ie we want MAXIMAL separation between
233+
# features with the same color
234+
feature_color = sorted(min_distances, key=min_distances.__getitem__, reverse=True)[0]
235+
236+
feature_colors[feature_id] = feature_color
224237

225238
i += 1
226239
feedback.setProgress(70 + int(i * total))

python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml

100644100755
+2
Original file line numberDiff line numberDiff line change
@@ -2379,7 +2379,9 @@ tests:
23792379
INPUT_LAYER:
23802380
name: custom/adjacent_polys.gml
23812381
type: vector
2382+
MIN_COLORS: 4
23822383
results:
23832384
OUTPUT_LAYER:
23842385
name: expected/topocolor_polys.gml
23852386
type: vector
2387+

0 commit comments

Comments
 (0)