mirror of
https://github.com/getredash/redash.git
synced 2026-05-11 00:00:57 -04:00
* client: Add lint command
Signed-off-by: koooge <koooooge@gmail.com>
* client: Override eslint rule object-curly-newline to keep current style
Signed-off-by: koooge <koooooge@gmail.com>
* client: Override eslint rule no-else-return to keep current style
Signed-off-by: koooge <koooooge@gmail.com>
* client: Fix eslint import/named
Signed-off-by: koooge <koooooge@gmail.com>
* client: eslint-5
Signed-off-by: koooge <koooooge@gmail.com>
* codeclimate: Delete the old setting
Signed-off-by: koooge <koooooge@gmail.com>
* client: Downgrade eslint 5 to 4 in codeclimate
Signed-off-by: koooge <koooooge@gmail.com>
* client: npx install-peerdeps --dev eslint-config-airbnb
Signed-off-by: koooge <koooooge@gmail.com>
* client: Enbale .jsx lint
Signed-off-by: koooge <koooooge@gmail.com>
* client: Set warn
Signed-off-by: koooge <koooooge@gmail.com>
* client: Fix lint indent, implicit-arrow-linebreak, lines-between-class-members
Signed-off-by: koooge <koooooge@gmail.com>
* client: Disable eslint operator-linebreak
Signed-off-by: koooge <koooooge@gmail.com>
* Revert "client: Downgrade eslint 5 to 4 in codeclimate"
This reverts commit f0fb0f0059.
* client: Fix react/button-has-type
Signed-off-by: koooge <koooooge@gmail.com>
* client: Disable an eslint rule react/jsx-one-expression-per-line
Signed-off-by: koooge <koooooge@gmail.com>
* codeclimate: Disable no-multiple-empty-lines
Signed-off-by: koooge <koooooge@gmail.com>
* client: Disable eslint react/destructuring-assignment
Signed-off-by: koooge <koooooge@gmail.com>
288 lines
7.0 KiB
JavaScript
288 lines
7.0 KiB
JavaScript
import angular from 'angular';
|
|
import _ from 'lodash';
|
|
import d3 from 'd3';
|
|
|
|
import d3sankey from '@/lib/visualizations/d3sankey';
|
|
import editorTemplate from './sankey-editor.html';
|
|
|
|
function getConnectedNodes(node) {
|
|
// source link = this node is the source, I need the targets
|
|
const nodes = [];
|
|
node.sourceLinks.forEach((link) => {
|
|
nodes.push(link.target);
|
|
});
|
|
node.targetLinks.forEach((link) => {
|
|
nodes.push(link.source);
|
|
});
|
|
|
|
return nodes;
|
|
}
|
|
|
|
function graph(data) {
|
|
const nodesDict = {};
|
|
const links = {};
|
|
const nodes = [];
|
|
|
|
const validKey = key => key !== 'value' && key.indexOf('$$') !== 0;
|
|
const keys = _.sortBy(_.filter(_.keys(data[0]), validKey), _.identity);
|
|
|
|
function normalizeName(name) {
|
|
if (name) {
|
|
return name;
|
|
}
|
|
|
|
return 'Exit';
|
|
}
|
|
|
|
function getNode(name, level) {
|
|
name = normalizeName(name);
|
|
const key = `${name}:${String(level)}`;
|
|
let node = nodesDict[key];
|
|
if (!node) {
|
|
node = { name };
|
|
const id = nodes.push(node) - 1;
|
|
node.id = id;
|
|
nodesDict[key] = node;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function getLink(source, target) {
|
|
let link = links[[source, target]];
|
|
if (!link) {
|
|
link = { target, source, value: 0 };
|
|
links[[source, target]] = link;
|
|
}
|
|
|
|
return link;
|
|
}
|
|
|
|
function addLink(sourceName, targetName, value, depth) {
|
|
if ((sourceName === '' || !sourceName) && depth > 1) {
|
|
return;
|
|
}
|
|
|
|
const source = getNode(sourceName, depth);
|
|
const target = getNode(targetName, depth + 1);
|
|
const link = getLink(source.id, target.id);
|
|
link.value += parseInt(value, 10);
|
|
}
|
|
|
|
data.forEach((row) => {
|
|
addLink(row[keys[0]], row[keys[1]], row.value, 1);
|
|
addLink(row[keys[1]], row[keys[2]], row.value, 2);
|
|
addLink(row[keys[2]], row[keys[3]], row.value, 3);
|
|
addLink(row[keys[3]], row[keys[4]], row.value, 4);
|
|
});
|
|
|
|
return { nodes, links: _.values(links) };
|
|
}
|
|
|
|
function spreadNodes(height, data) {
|
|
const nodesByBreadth = d3
|
|
.nest()
|
|
.key(d => d.x)
|
|
.entries(data.nodes)
|
|
.map(d => d.values);
|
|
|
|
nodesByBreadth.forEach((nodes) => {
|
|
nodes = _.filter(_.sortBy(nodes, node => -node.value), node => node.name !== 'Exit');
|
|
|
|
const sum = d3.sum(nodes, o => o.dy);
|
|
const padding = (height - sum) / nodes.length;
|
|
|
|
_.reduce(
|
|
nodes,
|
|
(y0, node) => {
|
|
node.y = y0;
|
|
return y0 + node.dy + padding;
|
|
},
|
|
0,
|
|
);
|
|
});
|
|
}
|
|
|
|
function createSankey(element, data) {
|
|
const margin = {
|
|
top: 10,
|
|
right: 10,
|
|
bottom: 10,
|
|
left: 10,
|
|
};
|
|
const width = element.offsetWidth - margin.left - margin.right;
|
|
const height = element.offsetHeight - margin.top - margin.bottom;
|
|
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
|
|
const format = d => d3.format(',.0f')(d);
|
|
const color = d3.scale.category20();
|
|
|
|
data = graph(data);
|
|
data.nodes = _.map(data.nodes, d => _.extend(d, { color: color(d.name.replace(/ .*/, '')) }));
|
|
|
|
// append the svg canvas to the page
|
|
const svg = d3
|
|
.select(element)
|
|
.append('svg')
|
|
.attr('class', 'sankey')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom)
|
|
.append('g')
|
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
// Set the sankey diagram properties
|
|
const sankey = d3sankey()
|
|
.nodeWidth(15)
|
|
.nodePadding(10)
|
|
.size([width, height]);
|
|
|
|
const path = sankey.link();
|
|
|
|
sankey
|
|
.nodes(data.nodes)
|
|
.links(data.links)
|
|
.layout(0);
|
|
|
|
spreadNodes(height, data);
|
|
sankey.relayout();
|
|
|
|
// add in the links
|
|
const link = svg
|
|
.append('g')
|
|
.selectAll('.link')
|
|
.data(data.links)
|
|
.enter()
|
|
.append('path')
|
|
.filter(l => l.target.name !== 'Exit')
|
|
.attr('class', 'link')
|
|
.attr('d', path)
|
|
.style('stroke-width', d => Math.max(1, d.dy))
|
|
.sort((a, b) => b.dy - a.dy);
|
|
|
|
// add the link titles
|
|
link.append('title').text(d => `${d.source.name} → ${d.target.name}\n${format(d.value)}`);
|
|
|
|
const node = svg
|
|
.append('g')
|
|
.selectAll('.node')
|
|
.data(data.nodes)
|
|
.enter()
|
|
.append('g')
|
|
.filter(n => n.name !== 'Exit')
|
|
.attr('class', 'node')
|
|
.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
|
|
function nodeMouseOver(currentNode) {
|
|
let nodes = getConnectedNodes(currentNode);
|
|
nodes = _.map(nodes, i => i.id);
|
|
node
|
|
.filter((d) => {
|
|
if (d === currentNode) {
|
|
return false;
|
|
}
|
|
|
|
if (_.includes(nodes, d.id)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.style('opacity', 0.2);
|
|
link
|
|
.filter(l => !(_.includes(currentNode.sourceLinks, l) || _.includes(currentNode.targetLinks, l)))
|
|
.style('opacity', 0.2);
|
|
}
|
|
|
|
function nodeMouseOut() {
|
|
node.style('opacity', 1);
|
|
link.style('opacity', 1);
|
|
}
|
|
|
|
// add in the nodes
|
|
node.on('mouseover', nodeMouseOver).on('mouseout', nodeMouseOut);
|
|
|
|
// add the rectangles for the nodes
|
|
node
|
|
.append('rect')
|
|
.attr('height', d => d.dy)
|
|
.attr('width', sankey.nodeWidth())
|
|
.style('fill', d => d.color)
|
|
.style('stroke', d => d3.rgb(d.color).darker(2))
|
|
.append('title')
|
|
.text(d => `${d.name}\n${format(d.value)}`);
|
|
|
|
// add in the title for the nodes
|
|
node
|
|
.append('text')
|
|
.attr('x', -6)
|
|
.attr('y', d => d.dy / 2)
|
|
.attr('dy', '.35em')
|
|
.attr('text-anchor', 'end')
|
|
.attr('transform', null)
|
|
.text(d => d.name)
|
|
.filter(d => d.x < width / 2)
|
|
.attr('x', 6 + sankey.nodeWidth())
|
|
.attr('text-anchor', 'start');
|
|
}
|
|
|
|
function sankeyRenderer() {
|
|
return {
|
|
restrict: 'E',
|
|
template: '<div class="sankey-visualization-container" resize-event="handleResize()"></div>',
|
|
link(scope, element) {
|
|
const container = element[0].querySelector('.sankey-visualization-container');
|
|
|
|
function refreshData() {
|
|
const queryData = scope.queryResult.getData();
|
|
if (queryData) {
|
|
// do the render logic.
|
|
angular.element(container).empty();
|
|
createSankey(container, queryData);
|
|
}
|
|
}
|
|
|
|
scope.handleResize = _.debounce(refreshData, 50);
|
|
scope.$watch('queryResult && queryResult.getData()', refreshData);
|
|
scope.$watch('visualization.options.height', (oldValue, newValue) => {
|
|
if (oldValue !== newValue) {
|
|
refreshData();
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
function sankeyEditor() {
|
|
return {
|
|
restrict: 'E',
|
|
template: editorTemplate,
|
|
};
|
|
}
|
|
|
|
export default function init(ngModule) {
|
|
ngModule.directive('sankeyRenderer', sankeyRenderer);
|
|
ngModule.directive('sankeyEditor', sankeyEditor);
|
|
|
|
ngModule.config((VisualizationProvider) => {
|
|
const renderTemplate =
|
|
'<sankey-renderer options="visualization.options" query-result="queryResult"></sankey-renderer>';
|
|
|
|
const editTemplate = '<sankey-editor></sankey-editor>';
|
|
const defaultOptions = {
|
|
defaultRows: 7,
|
|
};
|
|
|
|
VisualizationProvider.registerVisualization({
|
|
type: 'SANKEY',
|
|
name: 'Sankey',
|
|
renderTemplate,
|
|
editorTemplate: editTemplate,
|
|
defaultOptions,
|
|
});
|
|
});
|
|
}
|
|
|
|
init.init = true;
|