Визуализация данных высокой плотности

Данные высокой плотности (High-density data) - это тип набора данных, который имеет большое количество точек данных в определенной пространственной или временной области.

Большие и плотные наборы данных могут быть сложны для эффективной визуализации. Существует несколько методов, которые можно использовать для визуализации данных высокой плотности более содержательным образом. Рассмотрим эти методы в следующих разделах.

Кластеризация

Кластеризация - это метод группировки объектов в слое на основе их близости друг к другу. Обычно размер каждого кластера пропорционален количеству объектов, содержащихся в нем. Это может быть полезно для отображения областей, где многие точки расположены близко друг к другу.

clustering main

Кластеризация настраивается на источнике кластеров (cluster source), связанном с векторным слоем. Источник кластеров должен включать либо вложенный DataVectorSource, либо VectorSource.

Например, для кластеризации объектов из контейнера данных в XML выполните следующие шаги:

  1. Включите векторный слой в компонент geoMap.

  2. Добавьте ClusterSource в этот слой.

  3. Настройте DataVectorSource для кластера следующим образом:

    <maps:geoMap id="geoMap"
                 width="100%"
                 height="100%">
        <maps:layers>
            <maps:tile>
                <maps:osmSource/>
            </maps:tile>
            <maps:vector id="vectorLayer">
                <maps:cluster>
                    <maps:dataVectorSource dataContainer="locationsDc"
                                           property="building"/>
                </maps:cluster>
            </maps:vector>
        </maps:layers>
    </maps:geoMap>

Ниже мы опишем ключевые аспекты кластеризации.

ClusterSource

ClusterSource - это источник, который обеспечивает кластеризацию объектов на карте. Он требует дополнительный источник для поставки объектов, которым может быть либо:

  • DataVectorSource

  • VectorSource

Для кластеризации объектов без привязки к данным используйте VectorSource:

<maps:geoMap id="map"
             width="100%"
             height="100%">
    <maps:layers>
        <maps:tile>
            <maps:osmSource/>
        </maps:tile>
        <maps:vector id="vector">
            <maps:cluster>
                <maps:vectorSource/>
            </maps:cluster>
        </maps:vector>
    </maps:layers>
</maps:geoMap>

Атрибуты кластера

Кластер имеет следующие атрибуты:

  • attributions - параметр, определяющий текстовую информацию об авторстве и источниках данных (credits).

  • disableAtZoom устанавливает уровень масштабирования, при котором кластеризация отключается.

  • distance указывает расстояние в пикселях, на котором объекты группируются в кластер.

  • minDistance обозначает минимальное расстояние в пикселях между кластерами (для получения более подробной информации обратитесь к документации OSM).

  • showSinglePointAsCluster отображает одиночную точку как кластер размера 1.

  • weightProperty - если указан, каждая точка в слое будет иметь целочисленное значение веса на основе свойства weight геообъекта. Это значение помогает рассчитать совокупное значение кластера (по умолчанию используется количество точек).

  • wrapX определяет, должна ли карта бесшовно повторяться по горизонтали.

Свойство веса

По умолчанию кластер вычисляет количество точек внутри себя, которое затем отображается на маркере кластера. weightProperty позволяет нам указать свойство, которое кластер должен использовать для расчета.

Например, проект включает JPA-сущность:

@JmixEntity
@Table(name = "LOCATION")
@Entity
public class Location {

    @Column(name = "BUILDING")
    private Point building; (1)

    @Column(name = "WEIGHT")
    private Integer weight; (2)

    // getters and setters
}
1 Атрибут building содержит координаты, размещающие здание на карте.
2 Атрибут weight содержит числовое значение, которое предоставляет информацию о здании.

В XML-дескрипторе мы настраиваем конфигурацию для кластера:

<data>
    <collection id="locationsDc" class="com.company.mapssample.entity.Location">
        <loader id="locationsDl" readOnly="true">
            <query>
                <![CDATA[select e from Location e]]>
            </query>
        </loader>
        <fetchPlan extends="_base"/>
    </collection>
</data>
<facets>
    <dataLoadCoordinator auto="true"/>
</facets>
<layout>
    <maps:geoMap id="locationsMap"
                 width="100%"
                 height="100%">
        <maps:layers>
            <maps:tile>
                <maps:osmSource/>
            </maps:tile>
            <maps:vector id="vectorL">
                <maps:cluster weightProperty="weight">
                    <maps:dataVectorSource dataContainer="locationsDc"
                                           property="building"/>
                </maps:cluster>
            </maps:vector>
        </maps:layers>
    </maps:geoMap>
</layout>

Стилизация значка кластера

Значки кластеров можно настраивать с помощью Java API. Существует два метода стилизации:

  1. Введение нового стиля, который перекрывает стиль по умолчанию.

    @ViewComponent("locationMap.vectorLr.cluster")
    public ClusterSource cluster;
    
    @Subscribe
    public void onInit(final InitEvent event) {
        cluster.addPointStyles(new Style()
                .withImage(new CircleStyle()
                        .withRadius(15)
                        .withFill(new Fill("#7F70D8"))));
    }
    cluster new style
  2. Удаление стиля по умолчанию и добавление полностью нового стиля.

    @ViewComponent("styledMap.vectLr.clusterS")
    public ClusterSource clusterS;
    
    @Subscribe
    public void onInit(final InitEvent event) {
        clusterS.removeAllPointStyles();
        clusterS.addPointStyles(
                new Style()
                        .withImage(new CircleStyle()
                                .withRadius(20)
                                .withFill(new Fill("rgba(163, 104, 213, 0.6)"))),
                new Style()
                        .withImage(new CircleStyle()
                                .withRadius(15)
                                .withFill(new Fill("rgba(144, 64, 213, 1)"))));
    }
    cluster remove add style

Чтобы изменить стиль текста, используйте метод setPointTextStyle():

@ViewComponent("textStyleMap.vctLayer.clusterSource")
public ClusterSource clusterSource;

@Subscribe
public void onInit(final InitEvent event) {
    clusterSource.setPointTextStyle(new Style()
            .withText(new TextStyle()
                    .withFont("13px sans-serif")
                    .withOffsetY(1)
                    .withFill(new Fill("#FAAF22"))));
}
cluster text style

Установка свойства для объекта

Объекты (Features) имеют возможность определять свойства, которые будут связаны с этими объектами.

Если кластер указывает свойство веса, он попытается получить его из свойств объектов.

Рассмотрим следующий пример:

<maps:geoMap id="featuresMap"
             height="100%"
             width="100%">
    <maps:layers>
        <maps:tile>
            <maps:osmSource/>
        </maps:tile>
        <maps:vector id="pointsLayer">
            <maps:cluster id="pointClusterSource"
                          weightProperty="weight" >
                <maps:vectorSource id="source"/>
            </maps:cluster>
        </maps:vector>
    </maps:layers>
</maps:geoMap>
@ViewComponent("featuresMap.pointsLayer.pointClusterSource.source")
public VectorSource source;

@Subscribe
public void onInit(final InitEvent event) {
    source.addAllFeatures(generatePoints());
}
private List<Feature> generatePoints() {
    List<Feature> features = new ArrayList<>(10000);
    int e = 45;
    for (int i = 0; i < 10000; i++) {
        Point point = GeometryUtils.createPoint(2 * e * Math.random(),
                2 * e * Math.random() - e);
        features.add(
                new PointFeature(point)
                        .withProperty("weight",
                                Double.valueOf(Math.random() * 10).intValue()));
    }
    return Collections.unmodifiableList(features);
}

Тепловая карта

Тепловая карта (Heatmap) отображает точечные объекты как растровую поверхность, выделяя области с более высокой концентрацией точек вдоль непрерывного цветового градиента.

Теплые цвета (такие как красный или оранжевый) указывают на области с более высокими значениями, в то время как холодные цвета (синий или зеленый) указывают на более низкие значения.

heatmap main

Тепловые карты настраиваются на слое HeatmapLayer. Этот слой отображает данные в виде тепловых точек. HeatmapLayer работает с HeatmapDataVectorSource и VectorSource.

  • HeatmapDataVectorSource используется для привязки к контейнерам данных (сущностям или DTO). Атрибут property должен ссылаться на свойство сущности, имеющее тип Point.

  • VectorSource следует использовать, когда нет необходимости в привязке к контейнерам данных.

Например, чтобы добавить тепловую карту, связанную с контейнером данных, выполните следующие шаги:

  1. Включите слой тепловой карты в компонент geoMap: выберите элемент geoMap на панели структуры Jmix UI или в XML-дескрипторе экрана, затем нажмите кнопку Add на панели инспектора. В выпадающем списке выберите Layers → HeatmapLayer.

    add heatmap layer
  2. Добавьте HeatmapDataVectorSource в этот слой. Настройте этот источник следующим образом:

    <data>
        <collection id="locationsDc"
                    class="com.company.mapssample.entity.Location">
            <loader id="locationsDl" readOnly="true">
                <query>
                    <![CDATA[select e from Location e]]>
                </query>
            </loader>
            <fetchPlan extends="_base"/>
        </collection>
    </data>
    <layout>
        <maps:geoMap id="defaultMap"
                     width="100%"
                     height="100%">
            <maps:layers>
                <maps:tile>
                   <maps:osmSource/>
                </maps:tile>
                <maps:heatmap blur="20"
                              radius="15">
                    <maps:heatmapDataVectorSource dataContainer="locationsDc"
                                                  property="building"/> (1)
                </maps:heatmap>
            </maps:layers>
        </maps:geoMap>
    </layout>
    1 Определите свойство из сущности Location, имеющее тип Point.

Ниже мы опишем ключевые аспекты тепловых карт.

Атрибуты тепловой карты

Тепловая карта имеет следующие атрибуты:

  • Атрибут weightProperty указывает свойство сущности, из которого следует извлекать вес (интенсивность), или свойство из Feature.

    Если свойство не указано, будет использовано максимальное доступное значение веса.
  • maxWeight - указывает максимальное значение интенсивности в диапазоне от 0.0 до 1.0.

  • blur задает размер размытия точки в пикселях.

  • radius задает радиус точки в пикселях.

  • gradient - цветовой градиент точки, который должен состоять из CSS-цветов, разделенных запятыми.

Дополнительно включены базовые свойства слоя: className, maxZoom, minZoom, opacity, visible, zIndex

Взгляните на пример:

<maps:heatmap weightProperty="intensity"
              maxWeight="0.5"
              blur="20"
              radius="20"
              gradient="#32CD32, #00FF00, #7CFC00, #7FFF00, #ADFF2F">
    <maps:heatmapDataVectorSource dataContainer="locationsDc"
                                  property="building"/>
</maps:heatmap>

Свойство веса

Свойство веса (weightProperty) - это свойство, определяющее интенсивность или вес точки, отображаемой на тепловой карте. Оно должно быть указано либо в Feature, либо в сущности. Значения этого свойства должны находиться в диапазоне от 0.0 до 1.0 и иметь тип Double.

Если значение weightProperty не задано явно, обычно используется значение по умолчанию 1 для представления максимального уровня интенсивности для точек тепловой карты.

Ознакомьтесь с этим и этим примерами, использующими свойство weightProperty.

Примеры

Тепловая карта без привязки к данным

VectorSource используется, когда нет необходимости в привязке к контейнерам данных.

В примере ниже карта создается и первоначально настраивается с помощью XML.

<maps:geoMap id="map"
             width="100%"
             height="100%">
    <maps:layers>
        <maps:tile>
            <maps:osmSource/>
        </maps:tile>
        <maps:heatmap id="heatmap"
                      blur="10"
                      radius="15"
                      weightProperty="weight"> (1)
            <maps:vectorSource id="vectorSource"/> (2)
        </maps:heatmap>
    </maps:layers>
</maps:geoMap>
1 Установите свойство, которое будет связано с Feature.
2 Укажите id для векторного источника, что позволит инжектировать его в контроллер.

После этого дополнительные свойства задаются программно в Java-контроллере:

@ViewComponent("map.heatmap.vectorSource") (1)
private VectorSource vectorSource;

@Subscribe
public void onInit(final InitEvent event) {
    PointFeature feature = new PointFeature(GeometryUtils.createPoint(10, 10));
    feature.withProperty("weight", 1); (2)

    vectorSource.addFeature(feature);
}
1 Инжектируйте vectorSource в класс контроллера.
2 Передайте значение weight в метод, которое мы ранее указали для элемента heatmap в XML-дескрипторе.

Тепловая карта, связанная с данными

Например, проект включает JPA-сущность:

@JmixEntity
@Table(name = "LOCATION")
@Entity
public class Location {

    @Column(name = "BUILDING")
    private Point building; (1)

    @Column(name = "INTENSITY")
    private Double intensity; (2)

    // getters and setters
}
1 Атрибут building содержит координаты, размещающие здание на карте.
2 Атрибут intensity содержит числовое значение, которое предоставляет некоторую дополнительную информацию.

В XML-дескрипторе мы настраиваем конфигурацию для тепловой карты:

<data>
    <collection id="locationsDc"
                class="com.company.mapssample.entity.Location">
        <loader id="locationsDl" readOnly="true">
            <query>
                <![CDATA[select e from Location e]]>
            </query>
        </loader>
        <fetchPlan extends="_base"/>
    </collection>
</data>
<layout>
    <maps:geoMap id="geoMap"
                 width="100%"
                 height="100%">
        <maps:layers>
            <maps:tile>
                <maps:osmSource/>
            </maps:tile>
            <maps:heatmap blur="10"
                          radius="15"
                          weightProperty="intensity"> (1)
                <maps:heatmapDataVectorSource dataContainer="locationsDc"
                                              property="building"/>
            </maps:heatmap>
        </maps:layers>
    </maps:geoMap>
</layout>
1 Укажите в атрибуте weightProperty значение свойства сущности, содержащего значение интенсивности точки.

Тепловая карта с WeightProvider

HeatmapDataVectorSource имеет специальный метод для случаев, когда у сущности/DTO нет свойства веса или его необходимо вычислить каким-либо другим способом. В этих случаях не обязательно указывать weightProperty, так как вместо него будет использоваться провайдер.

В примере ниже карта создается и первоначально настраивается с помощью XML.

<maps:geoMap id="locationMap"
             width="100%"
             height="100%">
    <maps:layers>
        <maps:tile>
            <maps:osmSource/>
        </maps:tile>
        <maps:heatmap id="heatmapLayer"
                      weightProperty="intensity"
                      blur="15"
                      radius="20">
            <maps:heatmapDataVectorSource
                    id="heatmapDataVectorSource"
                    dataContainer="locationsDc"
                    property="building"/>
        </maps:heatmap>
    </maps:layers>
</maps:geoMap>

После этого мы настроим провайдер веса для heatmapDataVectorSource в Java-контроллере:

@ViewComponent("locationMap.heatmapLayer.heatmapDataVectorSource")
private HeatmapDataVectorSource<Location> heatmapDataVectorSource; (1)

@Subscribe
public void onInit(final InitEvent event) {
    heatmapDataVectorSource.setWeightProvider(location -> { (2)
        int weight = location.getWeight();
        if (weight > 50) {
            return 0.7;
        }
        return 0.3;
    });
}
1 Обратите внимание: heatmapDataVectorSource параметризован.
2 Провайдер вызывается для каждого элемента и должен возвращать вычисленное значение веса.