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

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

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

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

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

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

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

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
            caption="msg://caption"
            class="com.company.sales.web.screens.OrdersHistory"
            messagesPack="com.company.sales.web.screens"
            xmlns:chart="http://schemas.haulmont.com/charts/charts.xsd">
        <dsContext>
            <collectionDatasource id="ordersDs"
                                  class="com.company.sales.entity.Order"
                                  allowCommit="false"
                                  refreshMode="NEVER"/>
        </dsContext>
        <dialogMode height="600"
                    width="800"/>
        <layout expand="orderHistoryChart"
                spacing="true">
            <chart:serialChart id="orderHistoryChart"
                               categoryField="date"
                               datasource="ordersDs"
                               width="100%">
                <chart:graphs>
                    <chart:graph valueField="amount"/>
                </chart:graphs>
            </chart:serialChart>
        </layout>
    </window>
  4. Для обновления данных на лету используйте timer - специальный UI-компонент, который будет отправлять HTTP-запросы на сторону сервера.

    Отройте вкладку Properties дизайнера экрана и нажмите на кнопку Timers, чтобы добавить таймер на экран. Заполните поле id. Допустим, мы хотим, чтобы данные обновлялись каждые 2 секунды, в этом случае в поле delay укажем значение 2000 миллисекунд.

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

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

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

    public class OrdersHistory extends AbstractWindow {
        @Inject
        private Metadata metadata;
        @Inject
        private TimeSource timeSource;
        @Inject
        private CollectionDatasource<Order, UUID> ordersDs;
    
        private Random random = new Random(42);
    
        @Override
        public void init(Map<String, Object> params) {
            super.init(params);
    
            Order initialValue = metadata.create(Order.class);
            initialValue.setAmount(new BigDecimal(random.nextInt(1000) + 100));
            initialValue.setDate(timeSource.currentTimestamp());
    
            ordersDs.includeItem(initialValue);
        }
    
        public void updateChart(Timer source) {
            Order orderHistory = metadata.create(Order.class);
            orderHistory.setAmount(new BigDecimal(random.nextInt(1000) + 100));
            orderHistory.setDate(timeSource.currentTimestamp());;
    
            ordersDs.includeItem(orderHistory);
        }
    }

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

    chart incremental update 2
    Рисунок 4. Данные автоматически обновляются каждые 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());;
    
        ordersDs.includeItem(orderHistory);
    
        itemsQueue.add(orderHistory);
    
        if (itemsQueue.size() > 10) {
            Order item = itemsQueue.poll();
            ordersDs.excludeItem(item);
        }
    }

Результат

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

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