Tutorial: Updating Kibana filters from Vega

In this tutorial you will build an area chart in Vega using an Elasticsearch search query, and add a click handler and drag handler to update Kibana filters. This tutorial is not a full Vega tutorial, but will cover the basics of creating Vega visualizations into Kibana.

First, create an almost-blank Vega chart by pasting this into the editor:

  1. {
  2. $schema: "https://vega.github.io/schema/vega/v5.json"
  3. data: [{
  4. name: source_0
  5. }]
  6. scales: [{
  7. name: x
  8. type: time
  9. range: width
  10. }, {
  11. name: y
  12. type: linear
  13. range: height
  14. }]
  15. axes: [{
  16. orient: bottom
  17. scale: x
  18. }, {
  19. orient: left
  20. scale: y
  21. }]
  22. marks: [
  23. {
  24. type: area
  25. from: {
  26. data: source_0
  27. }
  28. encode: {
  29. update: {
  30. }
  31. }
  32. }
  33. ]
  34. }

Despite being almost blank, this Vega spec still contains the minimum requirements:

  • Data
  • Scales
  • Marks
  • (optional) Axes

Next, add a valid Elasticsearch search query in the data block:

  1. data: [
  2. {
  3. name: source_0
  4. url: {
  5. %context%: true
  6. %timefield%: order_date
  7. index: kibana_sample_data_ecommerce
  8. body: {
  9. aggs: {
  10. time_buckets: {
  11. date_histogram: {
  12. field: order_date
  13. fixed_interval: "3h"
  14. extended_bounds: {
  15. min: {%timefilter%: "min"}
  16. max: {%timefilter%: "max"}
  17. }
  18. min_doc_count: 0
  19. }
  20. }
  21. }
  22. size: 0
  23. }
  24. }
  25. format: { property: "aggregations.time_buckets.buckets" }
  26. }
  27. ]

Click “Update”, and nothing will change in the visualization. The first step is to change the X and Y scales based on the data:

  1. scales: [{
  2. name: x
  3. type: time
  4. range: width
  5. domain: {
  6. data: source_0
  7. field: key
  8. }
  9. }, {
  10. name: y
  11. type: linear
  12. range: height
  13. domain: {
  14. data: source_0
  15. field: doc_count
  16. }
  17. }]

Click “Update”, and you will see that the X and Y axes are now showing labels based on the real data.

Next, encode the fields key and doc_count as the X and Y values:

  1. marks: [
  2. {
  3. type: area
  4. from: {
  5. data: source_0
  6. }
  7. encode: {
  8. update: {
  9. x: {
  10. scale: x
  11. field: key
  12. }
  13. y: {
  14. scale: y
  15. value: 0
  16. }
  17. y2: {
  18. scale: y
  19. field: doc_count
  20. }
  21. }
  22. }
  23. }
  24. ]

Click “Update” and you will get a basic area chart:

vega tutorial 3

Next, add a new block to the marks section. This will show clickable points to filter for a specific date:

  1. {
  2. name: point
  3. type: symbol
  4. style: ["point"]
  5. from: {
  6. data: source_0
  7. }
  8. encode: {
  9. update: {
  10. x: {
  11. scale: x
  12. field: key
  13. }
  14. y: {
  15. scale: y
  16. field: doc_count
  17. }
  18. size: {
  19. value: 100
  20. }
  21. fill: {
  22. value: black
  23. }
  24. }
  25. }
  26. }

Next, we will create a Vega signal to make the points clickable. You can access the clicked datum in the expression used to update. In this case, you want clicks on points to add a time filter with the 3-hour interval defined above.

  1. signals: [
  2. {
  3. name: point_click
  4. on: [{
  5. events: {
  6. source: scope
  7. type: click
  8. markname: point
  9. }
  10. update: '''kibanaSetTimeFilter(datum.key, datum.key + 3 * 60 * 60 * 1000)'''
  11. }]
  12. }
  13. ]

This event is using the Kibana custom function kibanaSetTimeFilter to generate a filter that gets applied to the entire dashboard on click.

The mouse cursor does not currently indicate that the chart is interactive. Find the marks section, and update the mark named point by adding cursor: { value: "pointer" } to the encoding section like this:

  1. {
  2. name: point
  3. type: symbol
  4. style: ["point"]
  5. from: {
  6. data: source_0
  7. }
  8. encode: {
  9. update: {
  10. ...
  11. cursor: { value: "pointer" }
  12. }
  13. }
  14. }

Next, we will add a drag interaction which will allow the user to narrow into a specific time range in the visualization. This will require adding more signals, and adding a rectangle overlay:

vega tutorial 4

The first step is to add a new signal to track the X position of the cursor:

  1. {
  2. name: currentX
  3. value: -1
  4. on: [{
  5. events: {
  6. type: mousemove
  7. source: view
  8. },
  9. update: "clamp(x(), 0, width)"
  10. }, {
  11. events: {
  12. type: mouseout
  13. source: view
  14. }
  15. update: "-1"
  16. }]
  17. }

Now add a new mark to indicate the current cursor position:

  1. {
  2. type: rule
  3. interactive: false
  4. encode: {
  5. update: {
  6. y: {value: 0}
  7. y2: {signal: "height"}
  8. stroke: {value: "gray"}
  9. strokeDash: {
  10. value: [2, 1]
  11. }
  12. x: {signal: "max(currentX,0)"}
  13. defined: {signal: "currentX > 0"}
  14. }
  15. }
  16. }

Next, add a signal to track the current selected range, which will update until the user releases the mouse button or uses the escape key:

  1. {
  2. name: selected
  3. value: [0, 0]
  4. on: [{
  5. events: {
  6. type: mousedown
  7. source: view
  8. }
  9. update: "[clamp(x(), 0, width), clamp(x(), 0, width)]"
  10. }, {
  11. events: {
  12. type: mousemove
  13. source: window
  14. consume: true
  15. between: [{
  16. type: mousedown
  17. source: view
  18. }, {
  19. merge: [{
  20. type: mouseup
  21. source: window
  22. }, {
  23. type: keydown
  24. source: window
  25. filter: "event.key === 'Escape'"
  26. }]
  27. }]
  28. }
  29. update: "[selected[0], clamp(x(), 0, width)]"
  30. }, {
  31. events: {
  32. type: keydown
  33. source: window
  34. filter: "event.key === 'Escape'"
  35. }
  36. update: "[0, 0]"
  37. }]
  38. }

Now that there is a signal which tracks the time range from the user, we need to indicate the range visually by adding a new mark which only appears conditionally:

  1. {
  2. type: rect
  3. name: selectedRect
  4. encode: {
  5. update: {
  6. height: {signal: "height"}
  7. fill: {value: "#333"}
  8. fillOpacity: {value: 0.2}
  9. x: {signal: "selected[0]"}
  10. x2: {signal: "selected[1]"}
  11. defined: {signal: "selected[0] !== selected[1]"}
  12. }
  13. }
  14. }

Finally, add a new signal which will update the Kibana time filter when the mouse is released while dragging:

  1. {
  2. name: applyTimeFilter
  3. value: null
  4. on: [{
  5. events: {
  6. type: mouseup
  7. source: view
  8. }
  9. update: '''selected[0] !== selected[1] ? kibanaSetTimeFilter(
  10. invert('x',selected[0]),
  11. invert('x',selected[1])) : null'''
  12. }]
  13. }

Putting this all together, your visualization now supports the main features of standard visualizations in Kibana, but with the potential to add even more control. The final Vega spec for this tutorial is here:

Expand final Vega spec

  1. {
  2. $schema: "https://vega.github.io/schema/vega/v5.json"
  3. data: [
  4. {
  5. name: source_0
  6. url: {
  7. %context%: true
  8. %timefield%: order_date
  9. index: kibana_sample_data_ecommerce
  10. body: {
  11. aggs: {
  12. time_buckets: {
  13. date_histogram: {
  14. field: order_date
  15. fixed_interval: "3h"
  16. extended_bounds: {
  17. min: {%timefilter%: "min"}
  18. max: {%timefilter%: "max"}
  19. }
  20. min_doc_count: 0
  21. }
  22. }
  23. }
  24. size: 0
  25. }
  26. }
  27. format: { property: "aggregations.time_buckets.buckets" }
  28. }
  29. ]
  30. scales: [{
  31. name: x
  32. type: time
  33. range: width
  34. domain: {
  35. data: source_0
  36. field: key
  37. }
  38. }, {
  39. name: y
  40. type: linear
  41. range: height
  42. domain: {
  43. data: source_0
  44. field: doc_count
  45. }
  46. }]
  47. axes: [{
  48. orient: bottom
  49. scale: x
  50. }, {
  51. orient: left
  52. scale: y
  53. }]
  54. marks: [
  55. {
  56. type: area
  57. from: {
  58. data: source_0
  59. }
  60. encode: {
  61. update: {
  62. x: {
  63. scale: x
  64. field: key
  65. }
  66. y: {
  67. scale: y
  68. value: 0
  69. }
  70. y2: {
  71. scale: y
  72. field: doc_count
  73. }
  74. }
  75. }
  76. },
  77. {
  78. name: point
  79. type: symbol
  80. style: ["point"]
  81. from: {
  82. data: source_0
  83. }
  84. encode: {
  85. update: {
  86. x: {
  87. scale: x
  88. field: key
  89. }
  90. y: {
  91. scale: y
  92. field: doc_count
  93. }
  94. size: {
  95. value: 100
  96. }
  97. fill: {
  98. value: black
  99. }
  100. cursor: { value: "pointer" }
  101. }
  102. }
  103. },
  104. {
  105. type: rule
  106. interactive: false
  107. encode: {
  108. update: {
  109. y: {value: 0}
  110. y2: {signal: "height"}
  111. stroke: {value: "gray"}
  112. strokeDash: {
  113. value: [2, 1]
  114. }
  115. x: {signal: "max(currentX,0)"}
  116. defined: {signal: "currentX > 0"}
  117. }
  118. }
  119. },
  120. {
  121. type: rect
  122. name: selectedRect
  123. encode: {
  124. update: {
  125. height: {signal: "height"}
  126. fill: {value: "#333"}
  127. fillOpacity: {value: 0.2}
  128. x: {signal: "selected[0]"}
  129. x2: {signal: "selected[1]"}
  130. defined: {signal: "selected[0] !== selected[1]"}
  131. }
  132. }
  133. }
  134. ]
  135. signals: [
  136. {
  137. name: point_click
  138. on: [{
  139. events: {
  140. source: scope
  141. type: click
  142. markname: point
  143. }
  144. update: '''kibanaSetTimeFilter(datum.key, datum.key + 3 * 60 * 60 * 1000)'''
  145. }]
  146. }
  147. {
  148. name: currentX
  149. value: -1
  150. on: [{
  151. events: {
  152. type: mousemove
  153. source: view
  154. },
  155. update: "clamp(x(), 0, width)"
  156. }, {
  157. events: {
  158. type: mouseout
  159. source: view
  160. }
  161. update: "-1"
  162. }]
  163. }
  164. {
  165. name: selected
  166. value: [0, 0]
  167. on: [{
  168. events: {
  169. type: mousedown
  170. source: view
  171. }
  172. update: "[clamp(x(), 0, width), clamp(x(), 0, width)]"
  173. }, {
  174. events: {
  175. type: mousemove
  176. source: window
  177. consume: true
  178. between: [{
  179. type: mousedown
  180. source: view
  181. }, {
  182. merge: [{
  183. type: mouseup
  184. source: window
  185. }, {
  186. type: keydown
  187. source: window
  188. filter: "event.key === 'Escape'"
  189. }]
  190. }]
  191. }
  192. update: "[selected[0], clamp(x(), 0, width)]"
  193. }, {
  194. events: {
  195. type: keydown
  196. source: window
  197. filter: "event.key === 'Escape'"
  198. }
  199. update: "[0, 0]"
  200. }]
  201. }
  202. {
  203. name: applyTimeFilter
  204. value: null
  205. on: [{
  206. events: {
  207. type: mouseup
  208. source: view
  209. }
  210. update: '''selected[0] !== selected[1] ? kibanaSetTimeFilter(
  211. invert('x',selected[0]),
  212. invert('x',selected[1])) : null'''
  213. }]
  214. }
  215. ]
  216. }

Most Popular