The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/panels/parallelcoordinates/module.js

513 lines
17 KiB

angular.module('kibana.parallelcoordinates', [])
12 years ago
.controller('parallelcoordinates', function ($scope, eventBus) {
$scope.activeDocs = [];
// Set and populate defaults
var _d = {
query : "*",
size : 100, // Per page
pages : 5, // Pages available
offset : 0,
sort : ['@timestamp','desc'],
group : "default",
style : {'font-size': '9pt'},
fields : [],
sortable: true,
spyable: true
}
_.defaults($scope.panel, _d)
$scope.init = function () {
$scope.set_listeners($scope.panel.group);
// Now that we're all setup, request the time from our group
eventBus.broadcast($scope.$id,$scope.panel.group,"get_time")
//and get the currently selected fields
eventBus.broadcast($scope.$id,$scope.panel.group,"get_fields")
};
$scope.set_listeners = function(group) {
eventBus.register($scope,'time',function(event,time) {
$scope.panel.offset = 0;
set_time(time)
});
eventBus.register($scope,'query',function(event,query) {
$scope.panel.offset = 0;
$scope.panel.query = _.isArray(query) ? query[0] : query;
$scope.get_data();
});
eventBus.register($scope,'sort', function(event,sort){
$scope.panel.sort = _.clone(sort);
$scope.get_data();
});
eventBus.register($scope,'selected_fields', function(event, fields) {
$scope.panel.fields = _.clone(fields)
$scope.$emit('render');
});
};
$scope.get_data = function (segment,query_id) {
// Make sure we have everything for the request to complete
if (_.isUndefined($scope.panel.index) || _.isUndefined($scope.time))
return;
var _segment = _.isUndefined(segment) ? 0 : segment
$scope.segment = _segment;
$scope.panel.loading = true;
var request = $scope.ejs.Request().indices($scope.panel.index[_segment])
.query(ejs.FilteredQuery(
ejs.QueryStringQuery($scope.panel.query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to)
)
)
.size($scope.panel.size*$scope.panel.pages)
.sort($scope.panel.sort[0],$scope.panel.sort[1]);
$scope.populate_modal(request);
var results = request.doSearch();
// Populate scope when we have results
results.then(function (results) {
$scope.panel.loading = false;
if(_segment === 0) {
$scope.hits = 0;
$scope.data = [];
query_id = $scope.query_id = new Date().getTime()
}
12 years ago
// Check for error and abort if found
if(!(_.isUndefined(results.error))) {
$scope.panel.error = $scope.parse_error(results.error);
return;
}
12 years ago
// Check that we're still on the same query, if not stop
if($scope.query_id === query_id) {
$scope.data= $scope.data.concat(_.map(results.hits.hits, function(hit) {
return flatten_json(hit['_source']);
}));
12 years ago
$scope.hits += results.hits.total;
12 years ago
// Sort the data
$scope.data = _.sortBy($scope.data, function(v){
return v[$scope.panel.sort[0]]
});
12 years ago
// Reverse if needed
if($scope.panel.sort[1] == 'desc')
$scope.data.reverse();
// Keep only what we need for the set
$scope.data = $scope.data.slice(0,$scope.panel.size * $scope.panel.pages)
} else {
return;
}
$scope.$emit('render')
});
};
// I really don't like this function, too much dom manip. Break out into directive?
$scope.populate_modal = function (request) {
$scope.modal = {
title: "Inspector",
body: "<h5>Last Elasticsearch Query</h5><pre>" + 'curl -XGET ' + config.elasticsearch + '/' + $scope.panel.index + "/_search?pretty -d'\n" + angular.toJson(JSON.parse(request.toString()), true) + "'</pre>"
}
};
function set_time(time) {
$scope.time = time;
$scope.panel.index = _.isUndefined(time.index) ? $scope.panel.index : time.index
$scope.get_data();
}
$scope.$watch('activeDocs', function(v) {
eventBus.broadcast($scope.$id,$scope.panel.group,"table_documents",
{query:$scope.panel.query,docs:$scope.activeDocs});
});
})
.directive('parallelcoordinates', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
//used to store a variety of directive-level variables
var directive = {};
12 years ago
scope.initializing = false;
/**
* Initialize the panels if new, or render existing panels
*/
scope.init_or_render = function() {
if (typeof directive.svg === 'undefined') {
12 years ago
//prevent duplicate initialization steps, if render is called again
//before the svg is setup
if (!scope.initializing) {
init_panel();
}
} else {
render_panel();
}
};
12 years ago
/**
* Receive render events
*/
scope.$on('render', function () {
scope.init_or_render();
});
12 years ago
/**
* On window resize, re-render the panel
*/
angular.element(window).bind('resize', function () {
scope.init_or_render();
});
12 years ago
/**
* Load the various panel-specific scripts then initialize
* the svg and set appropriate D3 settings
*/
function init_panel() {
directive.m = [80, 100, 80, 100];
directive.w = $(elem[0]).width() - directive.m[1] - directive.m[3];
directive.h = $(elem[0]).height() - directive.m[0] - directive.m[2];
12 years ago
scope.initializing = true;
// Using LABjs, wait until all scripts are loaded before rendering panel
var scripts = $LAB.script("common/lib/d3.v3.min.js?rand="+Math.floor(Math.random()*10000));
12 years ago
scripts.wait(function () {
directive.x = d3.scale.ordinal().domain(scope.panel.fields).rangePoints([0, directive.w]);
directive.y = {};
directive.line = d3.svg.line().interpolate('cardinal');
directive.axis = d3.svg.axis().orient("left");
var viewbox = "0 0 " + (directive.w + directive.m[1] + directive.m[3]) + " " + (directive.h + directive.m[0] + directive.m[2]);
directive.svg = d3.select(elem[0]).append("svg")
12 years ago
.attr("width", "100%")
.attr("height", "100%")
.attr("viewbox", viewbox)
12 years ago
.append("svg:g")
.attr("transform", "translate(" + directive.m[3] + "," + directive.m[0] + ")");
12 years ago
// Add foreground lines.
directive.foreground = directive.svg.append("svg:g")
12 years ago
.attr("class", "foreground");
12 years ago
scope.initializing = false;
render_panel();
});
12 years ago
}
12 years ago
// Returns the path for a given data point.
function path(d) {
return directive.line(scope.panel.fields.map(function(p) { return [directive.x(p), directive.y[p](d[p])]; }));
12 years ago
}
12 years ago
// Handles a brush event, toggling the display of foreground lines.
function brush() {
var actives = scope.panel.fields.filter(function(p) { return !directive.y[p].brush.empty(); }),
extents = actives.map(function(p) { return directive.y[p].brush.extent(); });
12 years ago
//.fade class hides the "inactive" lines, helps speed up rendering significantly
directive.foregroundLines.classed("fade", function(d) {
12 years ago
return !actives.every(function(p, i) {
var pointValue;
if (directive.ordinals[p] === true) {
pointValue = directive.y[p](d[p]);
} else {
pointValue = d[p];
}
var inside = extents[i][0] <= pointValue && pointValue <= extents[i][1];
12 years ago
return inside;
});
});
//activeDocs contains the actual doc records for selected lines.
//will be broadcast out to the table
var activeDocs = _.filter(scope.data, function(v) {
return actives.every(function(p,i) {
var inside = extents[i][0] <= v[p] && v[p] <= extents[i][1];
return inside;
});
})
12 years ago
scope.$apply(function() {
scope.activeDocs = activeDocs;
});
}
12 years ago
//Drag functions are used for dragging the axis aroud
function dragstart(d) {
directive.i = scope.panel.fields.indexOf(d);
12 years ago
}
12 years ago
function drag(d) {
directive.x.range()[directive.i] = d3.event.x;
scope.panel.fields.sort(function(a, b) { return directive.x(a) - directive.x(b); });
directive.foregroundLines.attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
directive.traits.attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
directive.brushes.attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
directive.axisLines.attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
directive.foregroundLines.attr("d", path);
12 years ago
}
12 years ago
function dragend(d) {
directive.x.domain(scope.panel.fields).rangePoints([0, directive.w]);
12 years ago
var t = d3.transition().duration(500);
t.selectAll(".trait").attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
t.selectAll(".axis").attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
t.selectAll(".brush").attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
12 years ago
t.selectAll(".foregroundlines").attr("d", path);
}
12 years ago
/**
* Render updates to the SVG. Typically happens when the data changes (time, query)
* or when new options are selected
*/
function render_panel() {
12 years ago
//update the svg if the size has changed
directive.w = $(elem[0]).width() - directive.m[1] - directive.m[3];
directive.h = $(elem[0]).height() - directive.m[0] - directive.m[2];
directive.svg.attr("viewbox", "0 0 " + (directive.w + directive.m[1] + directive.m[3]) + " " + (directive.h + directive.m[0] + directive.m[2]));
directive.x = d3.scale.ordinal().domain(scope.panel.fields).rangePoints([0, directive.w]);
directive.y = {};
directive.line = d3.svg.line().interpolate('cardinal');
directive.axis = d3.svg.axis().orient("left").ticks(5);
directive.ordinals = {};
scope.panel.fields.forEach(function(d) {
var firstField = scope.data[0][d];
if (_.isString(firstField)) {
if (isValidDate(new Date(firstField))) {
//convert date timestamps to actual dates
_.map(scope.data, function(v) {
v[d] = new Date(v[d]);
});
var extents = d3.extent(scope.data, function(p) { return p[d]; });
directive.y[d] = d3.time.scale()
.domain([extents[0],extents[1]])
.range([directive.h, 0]);
} else {
directive.ordinals[d] = true;
var value = function(v) { return v[d]; };
var values = _.map(_.uniq(scope.data, value),value);
12 years ago
directive.y[d] = d3.scale.ordinal()
.domain(values)
.rangePoints([directive.h, 0]);
}
} else if (_.isNumber(firstField)) {
directive.y[d] = d3.scale.linear()
12 years ago
.domain(d3.extent(scope.data, function(p) { return +p[d]; }))
.range([directive.h, 0]);
} else if (_.isDate(firstField)) {
//this section is only used after timestamps have been parsed into actual date objects...
//avoids reparsing
var extents = d3.extent(scope.data, function(p) { return p[d]; });
directive.y[d] = d3.time.scale()
.domain([extents[0],extents[1]])
.range([directive.h, 0]);
12 years ago
}
directive.y[d].brush = d3.svg.brush()
.y(directive.y[d])
12 years ago
.on("brush", brush);
});
//setup the colors for the lines
setColors();
12 years ago
//pull out the actively selected columns for rendering the axis/lines
var activeData = _.map(scope.data, function(d) {
var t = {};
_.each(scope.panel.fields, function(f) {
t[f] = d[f];
});
return t;
});
//Lines
directive.foregroundLines = directive.foreground
12 years ago
.selectAll(".foregroundlines")
.data(activeData, function(d, i){
var id = "";
_.each(d, function(v) {
id += i + "_" + v;
});
return id;
});
directive.foregroundLines
12 years ago
.enter().append("svg:path")
.attr("d", path)
.attr("class", "foregroundlines")
.attr("style", function(d) {
return "stroke:" + directive.colors(d[scope.panel.fields[0]]) + ";";
12 years ago
});
directive.foregroundLines.exit().remove();
12 years ago
//Axis group
directive.traits = directive.svg.selectAll(".trait")
12 years ago
.data(scope.panel.fields, String);
directive.traits
12 years ago
.enter().append("svg:g")
.attr("class", "trait")
.attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
directive.traits
12 years ago
.exit().remove();
//brushes used to select lines
directive.brushes = directive.svg.selectAll(".brush")
12 years ago
.data(scope.panel.fields, String);
directive.brushes
12 years ago
.enter()
.append("svg:g")
.attr("class", "brush")
.each(function(d) {
d3.select(this)
.call(directive.y[d].brush)
.attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
12 years ago
})
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
//this section is repeated because enter() only works on "new" data, but we always need to
//update the brushes if things change. This just calls the brushing function, so it doesn't
//affect currently active rects
directive.brushes
12 years ago
.each(function(d) {
d3.select(this)
.call(directive.y[d].brush)
.attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
12 years ago
});
directive.brushes
12 years ago
.exit().remove();
//vertical axis and labels
directive.axisLines = directive.svg.selectAll(".axis")
12 years ago
.data(scope.panel.fields, String);
directive.axisLines
12 years ago
.enter()
.append("svg:g")
.attr("class", "axis")
.each(function(d) {
d3.select(this)
.call(directive.axis.scale(directive.y[d]))
.attr("transform", function(d) { return "translate(" + directive.x(d) + ")"; });
12 years ago
}).call(d3.behavior.drag()
.origin(function(d) { return {x: directive.x(d)}; })
12 years ago
.on("dragstart", dragstart)
.on("drag", drag)
.on("dragend", dragend))
.append("svg:text")
.attr("text-anchor", "middle")
.attr("y", -9)
.text(String);
directive.axisLines
12 years ago
.exit().remove();
//Simulate a dragend in case there is new data and we need to rearrange
dragend();
12 years ago
}
function setColors() {
var firstPanelField = scope.data[0][scope.panel.fields[0]];
var extents = d3.extent(scope.data, function(p) { return p[scope.panel.fields[0]]; });
if (_.isString(firstPanelField)) {
var value = function(v) { return v[firstPanelField]; };
var values = _.map(_.uniq(scope.data, value),value);
values = scope.data;
directive.colors = d3.scale.ordinal()
.domain(values)
.range(d3.range(values.length).map(d3.scale.linear()
.domain([0, values.length - 1])
.range(["red", "blue"])
.interpolate(d3.interpolateLab)));
} else if (_.isNumber(firstPanelField)) {
directive.colors = d3.scale.linear()
.domain([extents[0],extents[1]])
.range(["#4580FF", "#FF9245"]);
} else if (_.isDate(firstPanelField)) {
directive.colors = d3.time.scale()
.domain([extents[0],extents[1]])
.range(["#4580FF", "#FF9245"]);
}
}
function isValidDate(d) {
if ( Object.prototype.toString.call(d) !== "[object Date]" )
return false;
return !isNaN(d.getTime());
}
12 years ago
}
};
});