Tutorial: Updating Kibana filters from Vega
editTutorial: Updating Kibana filters from Vega
editIn 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:
{
$schema: "https://vega.github.io/schema/vega/v5.json"
data: [{
name: source_0
}]
scales: [{
name: x
type: time
range: width
}, {
name: y
type: linear
range: height
}]
axes: [{
orient: bottom
scale: x
}, {
orient: left
scale: y
}]
marks: [
{
type: area
from: {
data: source_0
}
encode: {
update: {
}
}
}
]
}
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:
data: [
{
name: source_0
url: {
%context%: true
%timefield%: order_date
index: kibana_sample_data_ecommerce
body: {
aggs: {
time_buckets: {
date_histogram: {
field: order_date
fixed_interval: "3h"
extended_bounds: {
min: {%timefilter%: "min"}
max: {%timefilter%: "max"}
}
min_doc_count: 0
}
}
}
size: 0
}
}
format: { property: "aggregations.time_buckets.buckets" }
}
]
Click "Update", and nothing will change in the visualization. The first step is to change the X and Y scales based on the data:
scales: [{
name: x
type: time
range: width
domain: {
data: source_0
field: key
}
}, {
name: y
type: linear
range: height
domain: {
data: source_0
field: doc_count
}
}]
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:
marks: [
{
type: area
from: {
data: source_0
}
encode: {
update: {
x: {
scale: x
field: key
}
y: {
scale: y
value: 0
}
y2: {
scale: y
field: doc_count
}
}
}
}
]
Click "Update" and you will get a basic area chart:
Next, add a new block to the marks section. This will show clickable points to filter for a specific
date:
{
name: point
type: symbol
style: ["point"]
from: {
data: source_0
}
encode: {
update: {
x: {
scale: x
field: key
}
y: {
scale: y
field: doc_count
}
size: {
value: 100
}
fill: {
value: black
}
}
}
}
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.
signals: [
{
name: point_click
on: [{
events: {
source: scope
type: click
markname: point
}
update: '''kibanaSetTimeFilter(datum.key, datum.key + 3 * 60 * 60 * 1000)'''
}]
}
]
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:
{
name: point
type: symbol
style: ["point"]
from: {
data: source_0
}
encode: {
update: {
...
cursor: { value: "pointer" }
}
}
}
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:
The first step is to add a new signal to track the X position of the cursor:
{
name: currentX
value: -1
on: [{
events: {
type: mousemove
source: view
},
update: "clamp(x(), 0, width)"
}, {
events: {
type: mouseout
source: view
}
update: "-1"
}]
}
Now add a new mark to indicate the current cursor position:
{
type: rule
interactive: false
encode: {
update: {
y: {value: 0}
y2: {signal: "height"}
stroke: {value: "gray"}
strokeDash: {
value: [2, 1]
}
x: {signal: "max(currentX,0)"}
defined: {signal: "currentX > 0"}
}
}
}
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:
{
name: selected
value: [0, 0]
on: [{
events: {
type: mousedown
source: view
}
update: "[clamp(x(), 0, width), clamp(x(), 0, width)]"
}, {
events: {
type: mousemove
source: window
consume: true
between: [{
type: mousedown
source: view
}, {
merge: [{
type: mouseup
source: window
}, {
type: keydown
source: window
filter: "event.key === 'Escape'"
}]
}]
}
update: "[selected[0], clamp(x(), 0, width)]"
}, {
events: {
type: keydown
source: window
filter: "event.key === 'Escape'"
}
update: "[0, 0]"
}]
}
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:
{
type: rect
name: selectedRect
encode: {
update: {
height: {signal: "height"}
fill: {value: "#333"}
fillOpacity: {value: 0.2}
x: {signal: "selected[0]"}
x2: {signal: "selected[1]"}
defined: {signal: "selected[0] !== selected[1]"}
}
}
}
Finally, add a new signal which will update the Kibana time filter when the mouse is released while dragging:
{
name: applyTimeFilter
value: null
on: [{
events: {
type: mouseup
source: view
}
update: '''selected[0] !== selected[1] ? kibanaSetTimeFilter(
invert('x',selected[0]),
invert('x',selected[1])) : null'''
}]
}
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
{
$schema: "https://vega.github.io/schema/vega/v5.json"
data: [
{
name: source_0
url: {
%context%: true
%timefield%: order_date
index: kibana_sample_data_ecommerce
body: {
aggs: {
time_buckets: {
date_histogram: {
field: order_date
fixed_interval: "3h"
extended_bounds: {
min: {%timefilter%: "min"}
max: {%timefilter%: "max"}
}
min_doc_count: 0
}
}
}
size: 0
}
}
format: { property: "aggregations.time_buckets.buckets" }
}
]
scales: [{
name: x
type: time
range: width
domain: {
data: source_0
field: key
}
}, {
name: y
type: linear
range: height
domain: {
data: source_0
field: doc_count
}
}]
axes: [{
orient: bottom
scale: x
}, {
orient: left
scale: y
}]
marks: [
{
type: area
from: {
data: source_0
}
encode: {
update: {
x: {
scale: x
field: key
}
y: {
scale: y
value: 0
}
y2: {
scale: y
field: doc_count
}
}
}
},
{
name: point
type: symbol
style: ["point"]
from: {
data: source_0
}
encode: {
update: {
x: {
scale: x
field: key
}
y: {
scale: y
field: doc_count
}
size: {
value: 100
}
fill: {
value: black
}
cursor: { value: "pointer" }
}
}
},
{
type: rule
interactive: false
encode: {
update: {
y: {value: 0}
y2: {signal: "height"}
stroke: {value: "gray"}
strokeDash: {
value: [2, 1]
}
x: {signal: "max(currentX,0)"}
defined: {signal: "currentX > 0"}
}
}
},
{
type: rect
name: selectedRect
encode: {
update: {
height: {signal: "height"}
fill: {value: "#333"}
fillOpacity: {value: 0.2}
x: {signal: "selected[0]"}
x2: {signal: "selected[1]"}
defined: {signal: "selected[0] !== selected[1]"}
}
}
}
]
signals: [
{
name: point_click
on: [{
events: {
source: scope
type: click
markname: point
}
update: '''kibanaSetTimeFilter(datum.key, datum.key + 3 * 60 * 60 * 1000)'''
}]
}
{
name: currentX
value: -1
on: [{
events: {
type: mousemove
source: view
},
update: "clamp(x(), 0, width)"
}, {
events: {
type: mouseout
source: view
}
update: "-1"
}]
}
{
name: selected
value: [0, 0]
on: [{
events: {
type: mousedown
source: view
}
update: "[clamp(x(), 0, width), clamp(x(), 0, width)]"
}, {
events: {
type: mousemove
source: window
consume: true
between: [{
type: mousedown
source: view
}, {
merge: [{
type: mouseup
source: window
}, {
type: keydown
source: window
filter: "event.key === 'Escape'"
}]
}]
}
update: "[selected[0], clamp(x(), 0, width)]"
}, {
events: {
type: keydown
source: window
filter: "event.key === 'Escape'"
}
update: "[0, 0]"
}]
}
{
name: applyTimeFilter
value: null
on: [{
events: {
type: mouseup
source: view
}
update: '''selected[0] !== selected[1] ? kibanaSetTimeFilter(
invert('x',selected[0]),
invert('x',selected[1])) : null'''
}]
}
]
}