Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nyalldawson committed Feb 22, 2017
1 parent 2fd78b8 commit 5c43e0b
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 25 deletions.
10 changes: 7 additions & 3 deletions python/plugins/processing/algs/help/qgis.yaml
Expand Up @@ -545,11 +545,15 @@ qgis:texttofloat: >
This algorithm modifies the type of a given attribute in a vector layer, converting a text attribute containing numeric strings into a numeric attribute.

qgis:topologicalcoloring: >
This algorithm assigns a color index to polygon features in such a way that no adjacent polygons share the same color index.
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.

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.
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.

The color index is saved to a new attribute named color_id.
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.

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.

A minimum number of colors can be specified if desired. The color index is saved to a new attribute named color_id.

qgis:translate: >
This algorithm moves the geometries within a layer, by offsetting them with a specified x and y displacement.
Expand Down
57 changes: 35 additions & 22 deletions python/plugins/processing/algs/qgis/TopoColors.py
Expand Up @@ -27,13 +27,14 @@

import os
import operator
from enum import Enum
import sys

from collections import defaultdict, deque

from qgis.core import (QgsField,
QgsGeometry,
QgsSpatialIndex,
QgsPointV2,
NULL)

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

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

class BalanceMethod(Enum):
BY_COUNT = 0
BY_AREA = 1
BY_DISTANCE = 2

class TopoColor(GeoAlgorithm):
INPUT_LAYER = 'INPUT_LAYER'
MIN_COLORS='MIN_COLORS'
BALANCE='BALANCE'
MIN_COLORS = 'MIN_COLORS'
BALANCE = 'BALANCE'
OUTPUT_LAYER = 'OUTPUT_LAYER'


def defineCharacteristics(self):
self.name, self.i18n_name = self.trAlgorithm('Topological coloring')
self.group, self.i18n_group = self.trAlgorithm('Cartographic tools')
Expand All @@ -69,20 +65,20 @@ def defineCharacteristics(self):
self.addParameter(ParameterNumber(self.MIN_COLORS,
self.tr('Minimum number of colors'), 1, 1000, 4))
balance_by = [self.tr('By feature count'),
self.tr('By assigned area'),
self.tr('By distance between colors')]
self.tr('By assigned area'),
self.tr('By distance between colors')]
self.addParameter(ParameterSelection(
self.BALANCE,
self.tr('Balance color assignment'),
balance_by))
balance_by, default=0))

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

def processAlgorithm(self, feedback):
layer = dataobjects.getObjectFromUri(
self.getParameterValue(self.INPUT_LAYER))
min_colors = self.getParameterValue(self.MIN_COLORS)
balance_by = BalanceMethod(self.getParameterValue(self.BALANCE))
balance_by = self.getParameterValue(self.BALANCE)

fields = layer.fields()
fields.append(QgsField('color_id', QVariant.Int))
Expand All @@ -93,7 +89,7 @@ def processAlgorithm(self, feedback):
layer.wkbType(),
layer.crs())

features = {f.id():f for f in vector.features(layer)}
features = {f.id(): f for f in vector.features(layer)}

topology, id_graph = self.compute_graph(features, feedback)
feature_colors = ColoringAlgorithm.balanced(features,
Expand Down Expand Up @@ -131,7 +127,7 @@ def compute_graph(features, feedback, create_id_graph=False):
id_graph = Graph(sort_graph=True)

# skip features without geometry
features_with_geometry = { f_id: f for (f_id, f) in features.items() if f.hasGeometry() }
features_with_geometry = {f_id: f for (f_id, f) in features.items() if f.hasGeometry()}

total = 70.0 / len(features_with_geometry)
index = QgsSpatialIndex()
Expand Down Expand Up @@ -167,10 +163,10 @@ def compute_graph(features, feedback, create_id_graph=False):
class ColoringAlgorithm:

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

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

feature_color=-1
feature_color = -1
if len(available_colors) == 0:
# no existing colors available for this feature, so add new color to pool and repeat
min_colors += 1
return ColoringAlgorithm.balanced(features,graph,feedback,balance,min_colors)
return ColoringAlgorithm.balanced(features, graph, feedback, balance, min_colors)
else:
if balance==BalanceMethod.BY_COUNT:
if balance == 0:
# choose least used available color
counts = [(c, v) for c, v in color_counts.items() if c in available_colors]
feature_color = sorted(counts, key=operator.itemgetter(1))[0][0]
color_counts[feature_color] += 1
elif balance==BalanceMethod.BY_AREA:
elif balance == 1:
areas = [(c, v) for c, v in color_areas.items() if c in available_colors]
feature_color = sorted(areas, key=operator.itemgetter(1))[0][0]
color_areas[feature_color] += features[feature_id].geometry().area()
#elif balance==BalanceMethod.BY_DISTANCE:
elif balance == 2:
min_distances = {c: sys.float_info.max for c in available_colors}
this_feature_centroid = QgsPointV2(features[feature_id].geometry().centroid().geometry())

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

feature_colors[feature_id] = feature_color
# loop through these, and calculate the minimum distance from this feature to the nearest
# feature with each assigned color
for other_feature_id, c in other_features.items():
other_geometry = features[other_feature_id].geometry()
other_centroid = QgsPointV2(other_geometry.centroid().geometry())

distance = this_feature_centroid.distanceSquared(other_centroid)
if distance < min_distances[c]:
min_distances[c] = distance

# choose color such that minimum distance is maximised! ie we want MAXIMAL separation between
# features with the same color
feature_color = sorted(min_distances, key=min_distances.__getitem__, reverse=True)[0]

feature_colors[feature_id] = feature_color

i += 1
feedback.setProgress(70 + int(i * total))
Expand Down
2 changes: 2 additions & 0 deletions python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml 100644 → 100755
Expand Up @@ -2379,7 +2379,9 @@ tests:
INPUT_LAYER:
name: custom/adjacent_polys.gml
type: vector
MIN_COLORS: 4
results:
OUTPUT_LAYER:
name: expected/topocolor_polys.gml
type: vector

0 comments on commit 5c43e0b

Please sign in to comment.