Платформа Qlik Analytics включает различные API, в том числе Engine API, который позволяет напрямую взаимодействовать с движком Qlik, отображая пользовательские веб-приложения. Сегодня мы рассмотрим специальный интерфейс RxQAP на основе JS, разработанный американским партнером Qlik, компанией Axis. На основе него можно разрабатывать собственные интерактивные графики, которые напрямую взаимодействуют с движком QIX.
Вот пример графика, разработанного при помощи Engine API и RxQAP: http://opensrc.axisgroup.com/examples/chart-with-filter/
Код графика доступен по ссылке: https://github.com/axisgroup/RxQAP/tree/master/examples/chart-with-filter.
Эта статья не является исчерпывающим руководством по работе с движком Qlik и Engine API, но позволяет понять основы работы с ним.
НА ЗАМЕТКУ! Дополнительная информация по движку QIX есть в видео-выступлении Хенрика Кронстрома на qRUG Camp 2016: Qlik под капотом
Что такое RxQAP и как он работает
RxQAP передает паттерн Observable, также как это реализовано в RxJS, в Qlik Engine API. То есть это значит каждый поток можно «слушать».
НА ЗАМЕТКУ! RxJS представляет собой модульную библиотеку, позволяющую создавать и компоновать потоки данных. Подход, используемый в Rx, появился в .NET и оттуда был перенесен во многие популярные языки. В зависимости от сложности реализуемой логики к проекту может подключаться как весь RxJS, так и его отдельные модули. При написании всех этих where(), buffer(), map() и пр. методов нам совершенно не важно, что является источником данных. Это может быть массив или же вебсокет, который может присылать по 100 объектов в секунду, а потом вдруг остановиться на минуту. Все эти события равны для нас и обрабатываются одинаково.
Итак, например, в qsocks, чтобы подключиться к серверу и асинхронно получить правило для глобального класса в сессии, нужно:
1
2 |
qsocks.connect();
// -> returns a Promise for a Global instance |
В RxQAP вы напишите что-то вроде следующего, но вместо этого получите Observable:
1
2 |
RxQ.connectEngine();
// -> returns an Observable for a QixGlobal instance |
НА ЗАМЕТКУ!
Основное отличие между Promises и Observables, в том что Promises активны, а Observables отложенные. Это ключевая особенность, которую нужно учитывать особенно при работе с RxQAP, в отличие от qsocks. Метод qsocks выполняется сразу; RxQAP выполняются только тогда, когда где-то добавлен подписчик.
Есть два ключевых компонента RxQ: это Qix Objects и Qix Observables.
Объекты QIX
Объекты QIX представляют сущность класса QIX Engine и обеспечивают методы, которые возвращают Observables для определенных обращений API.
НА ЗАМЕТКУ!
Для дополнительной информации по QIX Engine Classes можно почитать в официальном руководстве для разработчиков Qlik.
Сущность QIX Object является определенным указателем (дескриптором) внутри сессии QIX. Давайте представим, что мы создаем сессию с QIX Engine и получаем обратное указание для глобального класса -1. Так, с сущностью QixGlobal этого дескриптора, мы можем создать поток Observable:
1
2 3 4 5 |
var myEngine;
// -> assume you somehow magically got a QixGlobal instance for a session you have
var productVersion$ = myEngine.productVersion(); // -> returns an Observable that will stream the product version response |
Итак, как получить Qix Object? Так, любое обращение Engine API, которое вы делаете через RxQAP, которое возвращает сущность Engine Class с указанием, будет возвращать Qix Object для этой сущности. В RxQAP можно использовать метод connectEngine, чтобы подключиться к серверу и вернуть сущность QixGlobal для установленной сессии через Observable:
1
2 |
var engine$ = RxQ.connectEngine();
// -> returns Observable of QixGlobal instances |
Этот observable будет создавать поток сущностей QixGlobal. Если мне нужен доступ к классу QixGlobal, я могу это легко сделать через operator или subscriber:
1
2 3 4 |
engine$
.do(function(qG) { console.log(“a qix global instance being streamed: “, qG); }); |
Теперь давайте вспомним, что Qix Objects может возвращать обращения API к Observables для этого класса. Поэтому мы можем использовать поток engine$, чтобы создать новые потоки на основе обращений к Qix Object. Так, можно создать поток productVersion$ таким образом:
1
2 3 4 |
var productVersion$ = engine$
.mergeMap(function(qG) { return qG.productVersion() }); |
mergeMap – это полезный оператор библиотеки асинхронного программирования RxJS, который заберет значения из внутреннего Observable и распределит значения в выходном потоке Observable. При помощи этого оператора мы можем легко создать новые потоки любой функции API, объединяя эти операторы вместе. Например, мы хотим получить настройки приложения:
1
2 3 4 5 6 7 |
var appProp$ = RxQ.connectEngine()
.mergeMap(function(qG) { return qG.openDoc(“<YOUR-DOC-ID>”); }) .mergeMap(function(qA) { return qA.getAppProperties(); }); |
А теперь перейдем к Qix Observables.
Qix Observables
Qix Observables – классы, которые расширяют класс Observable пользовательскими операторами, связанными Engine API. Qix Observables существуют для всех классов QIX, таких как:
- GlobalObservable
- AppObservable
- FieldObservable
- GenericObjectObservable
- etc.
Каждый Qix Observable предполагает, что данные, которые проходят через него будут только связаны с классом Qix. Например, GlobalObservables пойдет только через объекты QixGlobal. И если он получит другие данные, это вызовет ошибку.
Поэтому QIX Observables работает только с одним типом данных. Например, GlobalObservable имеет оператор qOpenDoc:
1
2 3 4 5 6 7 8 9 |
engine$
.mergeMap(function(qG) { return qG.openDoc(“<DOC-ID>»); }); // -> yields an Observable that passes QixApp objects
engine$ .qOpenDoc(“<DOC-ID>”); // -> yields an AppObservable that passes QixApp objects |
Когда вы используете операторы Qix из Qix Observables, операторы будут автоматически назначать итоговые роли Observable соответствующему Qix Observable, когда это необходимо. В примере выше, .qOpenDoc возвращает AppObservable, чтобы мы могли затем обратиться к операторам приложений. С использованием метода mergeMap это работать не будет.
В итоге мы можем записать настройки нашего потока таким образом:
1
2 3 |
var appProp$ = RxQ.connectEngine()
.qOpenDoc(“<YOUR-DOC-ID>”) .qGetAppProperties(); |
Вот таким образом работает RxQAP. С теорией по RxQAP разобрались, теперь давайте создадим диаграмму!
Шаг 1: Создайте проект
Чтобы создать диаграмму, используем существующую библиотеку JS, которая называется ChartJS. Для работы с движком QIX мы будем использовать RxQAP, а также библиотеку RxJS для программирования систем реального времени, чтобы использовать Observables как часть взаимодействия с интерфейсом. Загрузим в наше приложение эти три библиотеки:
1
2 3 4 5 |
<head>
<script src=»rxqap.js»></script> <script src=»https://unpkg.com/@reactivex/rxjs/dist/global/Rx.js«></script> <script src=»https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.4.0/Chart.bundle.js«></script> </head> |
В нашем проекте будет список и диаграмма, поэтому нам нужно поместить контейнеры с элементами. Поэтому добавляем div-блок и тег ul для списка, а также div для графика. Также добавим ссылку на файл JS, где будет выполняться код для подключения QIX и создания диаграммы:
1
2 3 4 5 6 7 8 9 10 11 12 |
<body>
<div id=»lb-cont»> <strong>Product Group Filter</strong> <ul id=»myListbox»> </ul> </div> <div id=»chart-cont»> <strong>Revenue by Region</strong> <canvas id=»myChart» ></canvas> </div> <script src=»main.js»></script> </body> |
Теперь добавим таблицу стилей css. Здесь мы настроим параметры контейнера, размеры всех элементов и расстояние между контейнерами. Помещаем style в хэдер:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<style>
* { box-sizing: border-box; font-family: sans-serif; font-size: 12px; }
#lb-cont { display: inline-block; vertical-align: top; margin-right: 30px; }
ul { list-style: none; padding: 0px; font-size: 12px; margin-top: 5px; }
#chart-cont { width: 900px; display: inline-block; vertical-align: top; } </style> |
Шаг 2: Установите соединение с сервером
Теперь мы готовы написать JavaScript в нашем основном файле. Первое, что нам нужно сделать – определить сервер, к которому мы будем подключаться. Это делается при помощи объекта настроек, у которого имеются настройки хоста, портов и прочее.
Детали его настройки есть в файле RxQAP README. Вот пример настроек:
1
2 3 4 5 |
// Define a server
var config = { host: «sense.axisgroup.com», isSecure: false }; |
НА ЗАМЕТКУ!
Для продолжения выполнения работы скрипта, сделайте настройки конфигурации сервера.
После настроек конфигурации сервера, займемся GlobalObservable, который создаст сессию с QIX Engine, а также потоком QixGlobal:
1
2 |
// GlobalObservable that connects to the server and returns QixGlobal
var eng$ = RxQ.connectEngine(config); |
В данном случае GlobalObservable, который возвращает эта функция, — Hot GlobalObservable. Все остальные методы RxQAP возвращают Cold Observables, которую вы можете преобразовать в Hot Observable, когда будет необходимо.
НА ЗАМЕТКУ!
В чем разница между Hot/Cold Observable? Cold Observables создают свои значения внутри Observable, так, что любой subscriber получает новый набор значений. В Hot Observables значения создаются вне Observable, а subscriber получает аналогичный набор данных. Дополнительная информация по этому вопросу есть по ссылке:http://blog.thoughtram.io/angular/2016/06/16/cold-vs-hot-observables.html
Шаг 3: Откройте документ во время сессии
Поскольку у нас есть уже GlobalObservable, используем его для открытия документа внутри этой сессии. Используем оператор qOpenDoc, который вернет App Observable:
1
2 3 |
var app$ = eng$
.qOpenDoc(«24703994-1515-4c2c-a785-d769a9226143”); // -> returns Cold AppObservable |
В этом примере мы используем файл “Executive Dashboard.qvf”, который есть во всех приложениях Qlik Sense по умолчанию.
НА ЗАМЕТКУ!
Методы Engine API по каждому классу описаны в официальном руководстве разработчиков http://help.qlik.com/en-US/sense-developer/3.1/Subsystems/EngineAPI/Content/Classes/classes.htm
Как упоминалось выше, здесь будет работать Cold AppObservable. Так, для каждого нового подключенного подписчика будет вновь открываться приложение. В этом примере, я хочу, чтобы объект QixApp для этого документа открывался один раз и раздавался подписчикам в любое время, поэтому делаем Hot AppObservable:
1
2 3 4 5 6 |
// Observable that opens the Executive Dashboard QixApp and shares it
var app$ = eng$ .qOpenDoc(«24703994-1515-4c2c-a785-d769a9226143») .publishReplay(1) .refCount(); // -> returns a Hot AppObservable |
Шаг 4: Создайте ваши объекты
Чтобы построить диаграмму и список, нам нужно иметь динамические данные для них на сервере. Мы можем определить необходимые наборы данных свойствами объекта:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// GenericObject definition for the chart
var genObjProp = { qInfo: { qType: «chart» }, qHyperCubeDef: { «qDimensions»: [ { «qDef»: { «qFieldDefs»: [ «Region» ] }, «qNullSuppression»: true } ], «qMeasures»: [ { «qDef»: { «qDef»: «sum([Sales Amount])» } } ], «qInitialDataFetch»: [ { «qLeft»: 0, «qTop»: 0, «qWidth»: 2, «qHeight»: 1000 } ] } };
// Define a generic object for the listbox var lbProp = { «qInfo»: { «qType»: «filter» }, «qListObjectDef»: { «qDef»: { «qFieldDefs»: [ «Product Group Desc» ] }, «qInitialDataFetch»: [ { «qLeft»: 0, «qTop»: 0, «qWidth»: 1, «qHeight»: 1000 } ] } }; |
Для автоматизации процесса динамических настроек будет удобно использовать Qix Structure Generator.
Поскольку теперь у нас есть эти определения, мы можем создать типичные объекты, используя наш оператор QIX Operator:
1
2 3 4 5 6 7 |
// Create the generic object for the chart
var gO$ = app$ .qCreateSessionObject(genObjProp);
// Create the listbox generic object var lb$ = app$ .qCreateSessionObject(lbProp); |
Шаг 5: Получите данные и отобразите их
Теперь нам нужно получить данные и отобразить их.
Для этого нам нужно использовать поток, с оператором.qLayouts():
1
2 3 4 5 6 7 |
gO$
.qLayouts(); // -> streams a layout of our chart generic object whenever it changes
lb$ .qLayouts(); // -> streams a layout of our listbox generic object whenever it changes |
Теперь у нас есть поток данных, и все, что нужно сделать – подписаться на эти потоки, а также раздать их:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// Initialize the chart with empty data
var ctx = document.getElementById(«myChart»); var myChart = new Chart(ctx, { type: ‘bar’, data: { labels: [], datasets: [{ label: ‘Revenue ($)’, data: [], backgroundColor: ‘rgba(75, 192, 192, 0.2)’, borderColor: «rgba(75, 192, 192, 1)», borderWidth: 1 }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: true, callback: function(label){return ‘ $’ + Math.round(label).toString().replace(/\B(?=(\d{3})+(?!\d))/g, «,»);} } }] } } });
// Get an observable stream of generic object layouts when the data changes, and update the chart with latest data gO$ .qLayouts() .subscribe(function(layout) { var data = layout.response.qLayout.qHyperCube.qDataPages[0].qMatrix; myChart.data.labels = data.map(function(d) { return d[0].qText; }); myChart.data.datasets[0].data = data.map(function(d) { return d[1].qNum; }); myChart.update(); });
// Get listbox layouts and update the listbox UI var ul = document.getElementById(«myListbox»); lb$ .qLayouts() .subscribe(function(layout) { var data = layout.response.qLayout.qListObject.qDataPages[0].qMatrix; ul.innerHTML = data .map(function(m) { return «<li class='» + m[0].qState + «‘ data-qelemno=» + m[0].qElemNumber + «>» + m[0].qText + «</li>»; }) .join(«»); }); |
И еще одно: давайте добавим CSS в раздел style, чтобы в листбоксе была привычная цветовая схема «зеленый-белый-серый»:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
li {
padding: 5px 10px; border: 1px solid rgb(200,200,200); margin-top: -1px; cursor: pointer; }
li.S { background-color: rgba(75, 192, 192, .5); }
li.X { background-color: rgb(230,230,230); } |
Шаг 6: Создание событий для кликов при выборке
И заключительный шаг – добавить функциональность к списку для фильтрации данных по выборке пользователя. Есть разные способы это сделать, но в этом примере сделаем это через библиотеку RxJS.
Создадим события в списке. Сделаем это при помощи конструктора RxJS’ fromEvent:
1 | var select$ = Rx.Observable.fromEvent(ul,»click») |
Далее используем оператор .withLatestFrom:
1
2 |
var select$ = Rx.Observable.fromEvent(ul,»click»)
.withLatestFrom(lb$); |
Далее пишем такой код для обращения к API:
1
2 3 4 5 6 7 8 |
var select$ = Rx.Observable.fromEvent(ul,»click»)
.withLatestFrom(lb$) .map(function(vals) { var evt = vals[0]; var lbObj = vals[1]; var elemNo = parseInt(evt.target.getAttribute(«data-qelemno»)); return lbObj.SelectListObjectValues(«/qListObjectDef»,[elemNo],true); }); |
1
2 3 4 5 6 7 8 9 10 11 |
var select$ = Rx.Observable.fromEvent(ul,»click»)
.withLatestFrom(lb$) .map(function(vals) { var evt = vals[0]; var lbObj = vals[1]; var elemNo = parseInt(evt.target.getAttribute(«data-qelemno»)); return lbObj.SelectListObjectValues(«/qListObjectDef»,[elemNo],true) .publish(); // -> a multicast ConnectableObservable }); // -> an Observable that streams multicast ConnectableObservables |
Когда мы используем метод .publish(), создается подкласс, который называется ConnectableObservable:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var select$ = Rx.Observable.fromEvent(ul,»click»)
.withLatestFrom(lb$) .map(function(vals) { var evt = vals[0]; var lbObj = vals[1]; var elemNo = parseInt(evt.target.getAttribute(«data-qelemno»)); return lbObj.SelectListObjectValues(«/qListObjectDef»,[elemNo],true) .publish(); // -> a multicast ConnectableObservable }); // -> an Observable that streams multicast ConnectableObservables
// When selection call is created, execute it select$.subscribe(function(sel) { sel.connect(); // -> connects the selection call observable, therefore initiating the call }); |
На этом все на сегодня! Хороших вам разработок!
Свежие комментарии