2.6.5. Создание диаграммы с инкрементальным обновлением данных

В следующем примере мы рассмотрим диаграмму, которая получает данные из контейнера и обновляет их автоматически. Когда в контейнер добавляются новые данные, диаграмма не обновляется полностью: новые точки добавляются в график на лету каждые 2 секунды. Этот принцип удобно использовать при создании динамически обновляемых виджетов.

Пример основан на тестовом приложении Sales, к которому мы добавим диаграмму для отображения динамики новых заказов, то есть создания новых экземпляров сущности Order.

  1. Скачайте приложение Sales и добавьте к нему компонент charts, следуя инструкции из раздела Настройка проекта приложения.

  2. Создайте в Studio новый пустой экран. Назовите его orders-history, так как в нём будет отображаться история создания новых заказов.

  3. Добавьте к экрану компонент serialChart. Чтобы реализовать инкрементальное обновление данных, необходимо создать контейнер данных с типом CollectionContainer и привязать к нему диаграмму. В этом примере мы не будем загружать данные из базы, вместо этого мы будем создавать тестовые данные на лету, поэтому загрузчик данных в контейнере создавать не нужно.

    Для оси категорий укажите атрибут date, для оси значений – атрибут amount.

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
            caption="msg://caption"
            messagesPack="com.company.sales.web"
            xmlns:chart="http://schemas.haulmont.com/charts/charts.xsd">
        <data>
            <collection id="ordersDc"
                        class="com.company.sales.entity.Order"
                        view="_local"/>
        </data>
        <layout>
            <chart:serialChart id="orderHistoryChart"
                               categoryField="date"
                               dataContainer="ordersDc"
                               width="100%">
                <chart:graphs>
                    <chart:graph valueField="amount"/>
                </chart:graphs>
            </chart:serialChart>
        </layout>
    </window>
  4. Для обновления данных на лету используйте timer – специальный UI-компонент, который будет отправлять HTTP-запросы на сторону сервера.

    • Переключитесь на вкладку Designer.

    • В группе Non-visual components в палитре компонентов найдите Timer и перетащите его в область иерархии компонентов экрана.

    • Выделите компонент в иерархии и перейдите на вкладку Properties.

    • Задайте идентификатор таймера в поле id.

    • Допустим, мы хотим, чтобы данные обновлялись каждые 2 секунды, в этом случае в поле delay укажите значение 2000 миллисекунд.

    • В поле onTimer укажите имя метода Java – updateChart. Этот метод будет вызываться каждый раз при срабатывании события таймера. Сгенерируйте метод в контроллере экрана, нажав на кнопку >>.

    • Поставьте флажки repeating и autostart.

    chart incremental update
    Рисунок 28. Создание таймера
  5. Откройте контроллер экрана OrdersHistory. Для разработки логики работы таймера инжектируйте следующие зависимости: timeSource, metadata и контейнер данных для сущности Order. Мы будем генерировать новый экземпляр сущности Order с произвольным значением amount при каждом событии срабатывания таймера. Новый экземпляр добавляется к контейнеру данных с помощью метода getMutableItems().add().

    Инициализируйте диаграмму в методе onInit(), создав таким же образом исходный экземпляр сущности Order.

    package com.company.sales.web;
    
    import com.company.sales.entity.Order;
    import com.haulmont.cuba.core.global.Metadata;
    import com.haulmont.cuba.core.global.TimeSource;
    import com.haulmont.cuba.gui.components.Timer;
    import com.haulmont.cuba.gui.model.CollectionContainer;
    import com.haulmont.cuba.gui.screen.*;
    
    import javax.inject.Inject;
    import java.math.BigDecimal;
    import java.util.Random;
    
    @UiController("sales_OrdersHistory")
    @UiDescriptor("orders-history.xml")
    public class OrdersHistory extends Screen {
        @Inject
        private Metadata metadata;
        @Inject
        private TimeSource timeSource;
    
        @Inject
        private CollectionContainer<Order> ordersDc;
    
        private Random random = new Random(42);
    
        @Subscribe
        private void onInit(InitEvent event) {
            Order initialValue = metadata.create(Order.class);
            initialValue.setAmount(new BigDecimal(random.nextInt(1000) + 100));
            initialValue.setDate(timeSource.currentTimestamp());
    
            ordersDc.getMutableItems().add(initialValue);
        }
    
    
    
        public void updateChart(Timer source) {
            Order orderHistory = metadata.create(Order.class);
            orderHistory.setAmount(new BigDecimal(random.nextInt(1000) + 100));
            orderHistory.setDate(timeSource.currentTimestamp());;
            ordersDc.getMutableItems().add(orderHistory);
        }
    }

    На этом этапе диаграмма полностью функциональна, но размер контейнера данных после запуска таймера будет стремительно расти, поэтому реализуем ограничение количества отображаемых заказов.

    chart incremental update 2
    Рисунок 29. Данные автоматически обновляются каждые 2 секунды
  6. Создайте экземпляр класса Queue для очереди заказов. При каждом срабатывании таймера созданный заказ будет добавлен наверх очереди itemsQueue. Когда размер очереди превышает 10 заказов, самый старый заказ удаляется.

    private Queue<Order> itemsQueue = new LinkedList<>();
    public void updateChart(Timer source) {
        Order orderHistory = metadata.create(Order.class);
        orderHistory.setAmount(new BigDecimal(random.nextInt(1000) + 100));
        orderHistory.setDate(timeSource.currentTimestamp());;
        ordersDc.getMutableItems().add(orderHistory);
    
        itemsQueue.add(orderHistory);
    
        if (itemsQueue.size() > 10) {
            Order item = itemsQueue.poll();
            ordersDc.getMutableItems().add(item);
        }
    }

Результат

Данные поступают в браузер инкрементально. Если открыть консоль разработчика в Chrome, на вкладке Network будет видно, что каждые 2 секунды страница отправляет HTTP-запрос на backend и в ответе получает очень маленький JSON, содержащий только операции add и remove со значениями поля amount. Это позволяет избежать повторной пересылки всех данных диаграммы.

chart incremental update 3
Рисунок 30. Одновременно отображаются только последние 10 заказов