Платформа Qlik Analytics включает различные API, в том числе Engine API, который позволяет напрямую взаимодействовать с движком Qlik, отображая пользовательские веб-приложения. Сегодня мы рассмотрим специальный интерфейс RxQAP на основе JS, разработанный американским партнером Qlik, компанией Axis. На основе него можно разрабатывать собственные интерактивные графики, которые напрямую взаимодействуют с движком QIX.

Вот пример графика, разработанного при помощи Engine API и RxQAP: http://opensrc.axisgroup.com/examples/chart-with-filter/

tutorial

Код графика доступен по ссылке: 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

});

На этом все на сегодня! Хороших вам разработок!