ntopng/http_src/vue/sankey_3.vue
2023-01-30 12:58:50 +01:00

276 lines
7.1 KiB
Vue

<!-- (C) 2022 - ntop.org -->
<template>
<div v-if="no_data" class="alert alert-info" id="empty-message">{{ no_data_message || _i18n('flows_page.no_data') }}</div>
<div>
<svg
ref="sankey_chart_ref"
:width="sankey_size.width"
:height="sankey_size.height"
style="margin:10px;">
<g class="nodes" style="stroke: #000;strokeOpacity: 0.5;"/>
<g class="links" style="stroke: #000;strokeOpacity: 0.3;fill:none;"/>
</svg>
</div>
</template>
<script setup >
import { ref, onMounted, onBeforeMount, computed, watch } from "vue";
import { ntopng_utility, ntopng_url_manager, ntopng_status_manager } from "../services/context/ntopng_globals_services.js";
const d3 = d3v7;
const emit = defineEmits(['node_click', 'update_width', 'update_height'])
const margin = {
top: 2.5,
right: 5,
bottom: 2.5,
left: 5
};
const node_width = 10;
const no_data = ref(false)
const props = defineProps({
no_data_message: String,
width: Number,
height: Number,
sankey_data: Object,
});
const sankey_chart_ref = ref(null);
const sankey_size = ref({});
onBeforeMount(async() => {});
onMounted(async () => {
set_sankey_data();
attach_events();
});
watch(() => props.sankey_data, (cur_value, old_value) => {
set_sankey_data(true);
});
function set_sankey_data(reset) {
if (reset) {
$(".nodes", sankey_chart_ref.value).empty();
$(".links", sankey_chart_ref.value).empty();
}
if (props.sankey_data.nodes == null || props.sankey_data.links == null
|| props.sankey_data.length == 0 || props.sankey_data.links.length == 0) {
return;
}
draw_sankey();
}
function attach_events() {
window.addEventListener('resize', () => set_sankey_data(true));
}
let sankey = null;
let sankeyData = null;
async function draw_sankey() {
const colors = d3.scaleOrdinal(d3.schemeCategory10);
let data = props.sankey_data;//await get_sankey_data();
const size = get_size();
sankey_size.value = size;
sankey = create_sankey(size.width - 10, size.height - 5);
sankeyData = sankey(data);
const { links, nodes } = sankeyData;
let d3_nodes = d3.select(sankey_chart_ref.value)
.select("g.nodes")
.selectAll("g")
.data(nodes)
.join((enter) => enter.append("g"))
.attr("transform", (d) => `translate(${d.x0}, ${d.y0})`)
const zoom = d3.zoom()
.scaleExtent([1, 40])
.on("zoom", zoomed);
d3_nodes.append("rect")
// .transition(d3.easeLinear)
// .delay(1000)
// .duration(500)
.attr("height", (d) => d.y1 - d.y0)
.attr("width", (d) => d.x1 - d.x0)
.attr("dataIndex", (d) => d.index)
.attr("fill", (d) => colors(d.index / nodes.length))
.attr("class", "sankey-node")
.attr("style", "cursor:move;");
d3.selectAll("rect").append("title").text((d) => `${d.label}`);
// Relative to container/ node rect
d3_nodes.data(nodes)
.append("text")
// .transition(d3.easeLinear)
// .delay(1000)
// .duration(500)
.attr('class', 'label')
.style('pointer-events', 'auto')
.attr("style", "cursor:pointer;")
.style('fill-opacity', function(d) { return 1; } )
.attr("fill", (d) => "#000")
// .attr("fill", (d) => d3.rgb(colors(d.index / nodes.length)).darker())
.attr("x", (d) => (d.x0 < size.width / 2 ? 6 + (d.x1 - d.x0) : -6))
.attr("y", (d) => (d.y1 - d.y0) / 2)
.attr("alignment-baseline", "middle")
.attr("text-anchor", (d) =>
d.x0 < size.width / 2 ? "start" : "end"
)
.attr("font-size", 12)
.text((d) => d.label)
.on("click", function(event, data_obj){ emit('node_click', data_obj.data); });
d3_nodes
.call(d3.drag().subject(d => d).on("start", dragStart).on("drag", dragMove));
const links_d3 = d3.select(sankey_chart_ref.value)
.select("g.links")
.selectAll("g")
.data(links)
.join((enter) => enter.append("g"))
let lg_d3 = links_d3.append("linearGradient");
lg_d3.attr("id", (d) => `gradient-${d.index}`)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", (d) => d.source.x1)
.attr("x2", (d) => d.target.x0);
lg_d3.append("stop")
.attr("offset", "0")
.attr("stop-color", (d) => colors(d.source.index / nodes.length));
lg_d3.append("stop")
.attr("offset", "100%")
.attr("stop-color", (d) => colors(d.target.index / nodes.length));
links_d3
.append("path")
.style('pointer-events', 'none')
.attr("class", "sankey-link")
.attr("d", d3.sankeyLinkHorizontal())
// .attr("style", `stroke-width: ${d.width}px;`)
.attr("stroke-width", (d) => {
return Math.max(1, d.width);
})
// .transition(d3.easeLinear)
// .delay(1000)
// .duration(500)
.attr("stroke", (d) => `url(#gradient-${d.index}`)
// .attr("stroke", `black`)
// .attr("stroke-width", (d) => Math.max(100, d.width));
links_d3
.append("title")
.text((d) => `${d.label}`);
}
function dragStart(event, d) {
d.__x = event.x;
d.__y = event.y;
d.__x0 = d.x0;
d.__y0 = d.y0;
d.__x1 = d.x1;
d.__y1 = d.y1;
} //dragStart
function dragMove(event, d) {
d3.select(this).attr("transform", function (d) {
const dx = event.x - d.__x;
const dy = event.y - d.__y;
const width = sankey_size.value.width;
const height = sankey_size.value.width;
d.x0 = d.__x0 + dx;
d.x1 = d.__x1 + dx;
d.y0 = d.__y0 + dy;
d.y1 = d.__y1 + dy;
if (d.x0 < 0) {
d.x0 = 0;
d.x1 = node_width;
} // if
if (d.x1 > width) {
d.x0 = width - node_width;
d.x1 = width;
} // if
if (d.y0 < 0) {
d.y0 = 0;
d.y1 = d.__y1 - d.__y0;
} // if
if (d.y1 > height) {
d.y0 = height - (d.__y1 - d.__y0);
d.y1 = height;
} // if
sankey.update(sankeyData);
d3.selectAll(".sankey-link").attr("d", d3.sankeyLinkHorizontal());
return `translate(${d.x0}, ${d.y0})`;
});
}
function zoomed({transform}) {
g.attr("transform", transform);
}
function get_size() {
emit('update_width');
let width = props.width;
if (width == undefined) { width = $(sankey_chart_ref.value).parent().parent().width() - 10 }
emit('update_height');
let height = props.height
if (height == undefined) { height = $(sankey_chart_ref.value).parent().parent().height() - 80; }
return { width, height };
}
function create_sankey(width, height) {
const _sankey = d3.sankey()
.nodeAlign(d3.sankeyCenter)
.nodeWidth(10)
.nodePadding(node_width)
.extent([
[0, 5],
[width, height]
]);
return _sankey;
}
/* Set to true, to show the 'No Data' message */
function set_no_data_flag(set_no_data) {
no_data.value = set_no_data
}
const _i18n = (t) => i18n(t);
defineExpose({ draw_sankey, set_no_data_flag });
</script>
<style>
/* .node rect { */
/* fill-opacity: 0.9; */
/* shape-rendering: crispEdges; */
/* } */
/* .node text { */
/* pointer-events: none; */
/* text-shadow: 0 1px 0 #fff; */
/* } */
/* path.link.link2 { */
/* all: initial; */
/* fill: unset; */
/* stroke: unset; */
/* stroke-opacity: unset; */
/* stroke-width: unset; */
/* } */
/* .link:hover { */
/* stroke-opacity: 0.5; */
/* } */
</style>