mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-07-03 19:05:24 +00:00
add topology graph
This commit is contained in:
parent
fb5b7363e3
commit
0f8c522cfd
@ -20,7 +20,7 @@ limitations under the License. -->
|
|||||||
<Schedules />
|
<Schedules />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<Topology></Topology>
|
<process-topology />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +28,7 @@ limitations under the License. -->
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import Tasks from "./components/Tasks.vue";
|
import Tasks from "./components/Tasks.vue";
|
||||||
import Schedules from "./components/Schedules.vue";
|
import Schedules from "./components/Schedules.vue";
|
||||||
import Topology from "./components/Topology.vue";
|
import ProcessTopology from "./components/ProcessTopology.vue";
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.content {
|
.content {
|
||||||
|
@ -0,0 +1,236 @@
|
|||||||
|
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
contributor license agreements. See the NOTICE file distributed with
|
||||||
|
this work for additional information regarding copyright ownership.
|
||||||
|
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
(the "License"); you may not use this file except in compliance with
|
||||||
|
the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License. -->
|
||||||
|
<template>
|
||||||
|
<div ref="chart" class="topology"></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useEbpfStore } from "@/store/modules/ebpf";
|
||||||
|
import { useSelectorStore } from "@/store/modules/selectors";
|
||||||
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
|
import d3tip from "d3-tip";
|
||||||
|
import { simulationInit, simulationSkip } from "./utils/simulation";
|
||||||
|
import { linkElement, anchorElement, arrowMarker } from "./utils/linkElement";
|
||||||
|
import nodeElement from "./utils/nodeElement";
|
||||||
|
import { Call } from "@/types/topology";
|
||||||
|
import zoom from "./utils/zoom";
|
||||||
|
import { ProcessNode } from "@/types/ebpf";
|
||||||
|
|
||||||
|
/*global Nullable, defineProps */
|
||||||
|
const props = defineProps({
|
||||||
|
config: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
default: () => ({ graph: {} }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { t } = useI18n();
|
||||||
|
const selectorStore = useSelectorStore();
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
const ebpfStore = useEbpfStore();
|
||||||
|
const appStore = useAppStoreWithOut();
|
||||||
|
const height = ref<number>(100);
|
||||||
|
const width = ref<number>(100);
|
||||||
|
const simulation = ref<any>(null);
|
||||||
|
const svg = ref<Nullable<any>>(null);
|
||||||
|
const chart = ref<Nullable<HTMLDivElement>>(null);
|
||||||
|
const tip = ref<Nullable<HTMLDivElement>>(null);
|
||||||
|
const graph = ref<any>(null);
|
||||||
|
const node = ref<any>(null);
|
||||||
|
const link = ref<any>(null);
|
||||||
|
const anchor = ref<any>(null);
|
||||||
|
const arrow = ref<any>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await getTopology();
|
||||||
|
|
||||||
|
const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
|
||||||
|
height: 40,
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
height.value = dom.height - 40;
|
||||||
|
width.value = dom.width;
|
||||||
|
svg.value = d3.select(chart.value).append("svg").attr("class", "process-svg");
|
||||||
|
drawGraph();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGraph() {
|
||||||
|
tip.value = (d3tip as any)().attr("class", "d3-tip").offset([-8, 0]);
|
||||||
|
graph.value = svg.value
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "svg-graph")
|
||||||
|
.attr("transform", `translate(-100, -100)`);
|
||||||
|
graph.value.call(tip.value);
|
||||||
|
simulation.value = simulationInit(
|
||||||
|
d3,
|
||||||
|
ebpfStore.nodes,
|
||||||
|
ebpfStore.calls,
|
||||||
|
ticked
|
||||||
|
);
|
||||||
|
node.value = graph.value.append("g").selectAll(".topo-node");
|
||||||
|
link.value = graph.value.append("g").selectAll(".topo-line");
|
||||||
|
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
|
||||||
|
arrow.value = graph.value.append("g").selectAll(".topo-line-arrow");
|
||||||
|
svg.value.call(zoom(d3, graph.value));
|
||||||
|
svg.value.on("click", (event: any) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
ebpfStore.setNode(null);
|
||||||
|
ebpfStore.setLink(null);
|
||||||
|
dashboardStore.selectWidget(props.config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function update() {
|
||||||
|
// node element
|
||||||
|
if (!node.value || !link.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.value = node.value.data(ebpfStore.nodes, (d: ProcessNode) => d.id);
|
||||||
|
node.value.exit().remove();
|
||||||
|
node.value = nodeElement(
|
||||||
|
d3,
|
||||||
|
node.value.enter(),
|
||||||
|
{
|
||||||
|
// dragstart: dragstart,
|
||||||
|
// dragged: dragged,
|
||||||
|
// dragended: dragended,
|
||||||
|
handleNodeClick: handleNodeClick,
|
||||||
|
tipHtml: (data: ProcessNode) => {
|
||||||
|
return ` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tip.value
|
||||||
|
).merge(node.value);
|
||||||
|
// line element
|
||||||
|
link.value = link.value.data(ebpfStore.calls, (d: Call) => d.id);
|
||||||
|
link.value.exit().remove();
|
||||||
|
link.value = linkElement(link.value.enter()).merge(link.value);
|
||||||
|
// anchorElement
|
||||||
|
anchor.value = anchor.value.data(ebpfStore.calls, (d: Call) => d.id);
|
||||||
|
anchor.value.exit().remove();
|
||||||
|
anchor.value = anchorElement(
|
||||||
|
anchor.value.enter(),
|
||||||
|
{
|
||||||
|
handleLinkClick: handleLinkClick,
|
||||||
|
tipHtml: (data: Call) => {
|
||||||
|
const html = `<div><span class="grey">${t(
|
||||||
|
"detectPoint"
|
||||||
|
)}:</span>${data.detectPoints.join(" | ")}</div>`;
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tip.value
|
||||||
|
).merge(anchor.value);
|
||||||
|
// arrow marker
|
||||||
|
arrow.value = arrow.value.data(ebpfStore.calls, (d: Call) => d.id);
|
||||||
|
arrow.value.exit().remove();
|
||||||
|
arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
|
||||||
|
// force element
|
||||||
|
simulation.value.nodes(ebpfStore.nodes);
|
||||||
|
simulation.value
|
||||||
|
.force("link")
|
||||||
|
.links(ebpfStore.calls)
|
||||||
|
.id((d: Call) => d.id);
|
||||||
|
simulationSkip(d3, simulation.value, ticked);
|
||||||
|
const loopMap: any = {};
|
||||||
|
for (let i = 0; i < ebpfStore.calls.length; i++) {
|
||||||
|
const link: any = ebpfStore.calls[i];
|
||||||
|
link.loopFactor = 1;
|
||||||
|
for (let j = 0; j < ebpfStore.calls.length; j++) {
|
||||||
|
if (i === j || loopMap[i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const otherLink = ebpfStore.calls[j];
|
||||||
|
if (
|
||||||
|
link.source.id === otherLink.target.id &&
|
||||||
|
link.target.id === otherLink.source.id
|
||||||
|
) {
|
||||||
|
link.loopFactor = -1;
|
||||||
|
loopMap[j] = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLinkClick(event: any, d: Call) {
|
||||||
|
if (
|
||||||
|
d.source.layer !== dashboardStore.layerId ||
|
||||||
|
d.target.layer !== dashboardStore.layerId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
ebpfStore.setNode(null);
|
||||||
|
ebpfStore.setLink(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ticked() {
|
||||||
|
link.value.attr(
|
||||||
|
"d",
|
||||||
|
(d: Call | any) =>
|
||||||
|
`M${d.source.x} ${d.source.y} Q ${(d.source.x + d.target.x) / 2} ${
|
||||||
|
(d.target.y + d.source.y) / 2 - d.loopFactor * 90
|
||||||
|
} ${d.target.x} ${d.target.y}`
|
||||||
|
);
|
||||||
|
anchor.value.attr(
|
||||||
|
"transform",
|
||||||
|
(d: Call | any) =>
|
||||||
|
`translate(${(d.source.x + d.target.x) / 2}, ${
|
||||||
|
(d.target.y + d.source.y) / 2 - d.loopFactor * 45
|
||||||
|
})`
|
||||||
|
);
|
||||||
|
node.value.attr(
|
||||||
|
"transform",
|
||||||
|
(d: Node | any) => `translate(${d.x - 22},${d.y - 22})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTopology() {
|
||||||
|
const serviceInstanceId =
|
||||||
|
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
||||||
|
ebpfStore.getProcessTopology({
|
||||||
|
serviceInstanceId,
|
||||||
|
duration: appStore.durationTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeClick(d: Node & { x: number; y: number }) {
|
||||||
|
ebpfStore.setNode(d);
|
||||||
|
ebpfStore.setLink(null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.topology {
|
||||||
|
width: calc(100% - 5px);
|
||||||
|
margin: 0 5px 5px 0;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topo-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 5px);
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,49 +0,0 @@
|
|||||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
contributor license agreements. See the NOTICE file distributed with
|
|
||||||
this work for additional information regarding copyright ownership.
|
|
||||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
(the "License"); you may not use this file except in compliance with
|
|
||||||
the License. You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License. -->
|
|
||||||
<template>
|
|
||||||
<div ref="topology" class="topology"></div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import { useEbpfStore } from "@/store/modules/ebpf";
|
|
||||||
import { useSelectorStore } from "@/store/modules/selectors";
|
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
|
||||||
|
|
||||||
const selectorStore = useSelectorStore();
|
|
||||||
const ebpfStore = useEbpfStore();
|
|
||||||
const appStore = useAppStoreWithOut();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getTopology();
|
|
||||||
});
|
|
||||||
|
|
||||||
function getTopology() {
|
|
||||||
const serviceInstanceId =
|
|
||||||
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
|
||||||
ebpfStore.getProcessTopology({
|
|
||||||
serviceInstanceId,
|
|
||||||
duration: appStore.durationTime,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.topology {
|
|
||||||
width: calc(100% - 5px);
|
|
||||||
margin: 0 5px 5px 0;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 150px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const linkElement = (graph: any) => {
|
||||||
|
const linkEnter = graph
|
||||||
|
.append("path")
|
||||||
|
.attr("class", "topo-line")
|
||||||
|
.attr("marker-end", "url(#arrow)")
|
||||||
|
.attr("stroke", "#217EF25f");
|
||||||
|
return linkEnter;
|
||||||
|
};
|
||||||
|
export const anchorElement = (graph: any, funcs: any, tip: any) => {
|
||||||
|
const linkEnter = graph
|
||||||
|
.append("circle")
|
||||||
|
.attr("class", "topo-line-anchor")
|
||||||
|
.attr("r", 5)
|
||||||
|
.attr("fill", "#217EF25f")
|
||||||
|
.on("mouseover", function (event: unknown, d: unknown) {
|
||||||
|
tip.html(funcs.tipHtml).show(d, this);
|
||||||
|
})
|
||||||
|
.on("mouseout", function () {
|
||||||
|
tip.hide(this);
|
||||||
|
})
|
||||||
|
.on("click", (event: unknown, d: unknown) => {
|
||||||
|
funcs.handleLinkClick(event, d);
|
||||||
|
});
|
||||||
|
return linkEnter;
|
||||||
|
};
|
||||||
|
export const arrowMarker = (graph: any) => {
|
||||||
|
const defs = graph.append("defs");
|
||||||
|
const arrow = defs
|
||||||
|
.append("marker")
|
||||||
|
.attr("id", "arrow")
|
||||||
|
.attr("class", "topo-line-arrow")
|
||||||
|
.attr("markerUnits", "strokeWidth")
|
||||||
|
.attr("markerWidth", "6")
|
||||||
|
.attr("markerHeight", "6")
|
||||||
|
.attr("viewBox", "0 0 12 12")
|
||||||
|
.attr("refX", "5")
|
||||||
|
.attr("refY", "6")
|
||||||
|
.attr("orient", "auto");
|
||||||
|
const arrowPath = "M2,2 L10,6 L2,10 L6,6 L2,2";
|
||||||
|
|
||||||
|
arrow.append("path").attr("d", arrowPath).attr("fill", "#217EF25f");
|
||||||
|
return arrow;
|
||||||
|
};
|
@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import icons from "@/assets/img/icons";
|
||||||
|
import { Node } from "@/types/topology";
|
||||||
|
|
||||||
|
icons["KAFKA-CONSUMER"] = icons.KAFKA;
|
||||||
|
export default (d3: any, graph: any, funcs: any, tip: any) => {
|
||||||
|
const nodeEnter = graph
|
||||||
|
.append("g")
|
||||||
|
.call(
|
||||||
|
d3
|
||||||
|
.drag()
|
||||||
|
.on("start", funcs.dragstart)
|
||||||
|
.on("drag", funcs.dragged)
|
||||||
|
.on("end", funcs.dragended)
|
||||||
|
)
|
||||||
|
.on("mouseover", function (event: any, d: Node) {
|
||||||
|
tip.html(funcs.tipHtml).show(d, this);
|
||||||
|
})
|
||||||
|
.on("mouseout", function () {
|
||||||
|
tip.hide(this);
|
||||||
|
})
|
||||||
|
.on("click", (event: any, d: Node | any) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
funcs.handleNodeClick(d);
|
||||||
|
});
|
||||||
|
nodeEnter
|
||||||
|
.append("image")
|
||||||
|
.attr("width", 49)
|
||||||
|
.attr("height", 49)
|
||||||
|
.attr("x", 2)
|
||||||
|
.attr("y", 10)
|
||||||
|
.attr("style", "cursor: move;")
|
||||||
|
.attr("xlink:href", (d: { [key: string]: number }) => {
|
||||||
|
return d.isReal ? icons.CUBEERROR : icons.CUBE;
|
||||||
|
});
|
||||||
|
nodeEnter
|
||||||
|
.append("image")
|
||||||
|
.attr("width", 32)
|
||||||
|
.attr("height", 32)
|
||||||
|
.attr("x", 6)
|
||||||
|
.attr("y", -10)
|
||||||
|
.attr("style", "opacity: 0.5;")
|
||||||
|
.attr("xlink:href", icons.LOCAL);
|
||||||
|
nodeEnter
|
||||||
|
.append("image")
|
||||||
|
.attr("width", 18)
|
||||||
|
.attr("height", 18)
|
||||||
|
.attr("x", 13)
|
||||||
|
.attr("y", -7)
|
||||||
|
.attr("xlink:href", (d: { type: string }) =>
|
||||||
|
!d.type || d.type === "N/A"
|
||||||
|
? icons.UNDEFINED
|
||||||
|
: icons[d.type.toUpperCase().replace("-", "")]
|
||||||
|
);
|
||||||
|
nodeEnter
|
||||||
|
.append("text")
|
||||||
|
.attr("class", "topo-text")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("x", 22)
|
||||||
|
.attr("y", 70)
|
||||||
|
.text((d: { name: string }) =>
|
||||||
|
d.name.length > 20 ? `${d.name.substring(0, 20)}...` : d.name
|
||||||
|
);
|
||||||
|
return nodeEnter;
|
||||||
|
};
|
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
export const simulationInit = (
|
||||||
|
d3: any,
|
||||||
|
dataNodes: any,
|
||||||
|
dataLinks: any,
|
||||||
|
ticked: any
|
||||||
|
) => {
|
||||||
|
const simulation = d3
|
||||||
|
.forceSimulation(dataNodes)
|
||||||
|
.force(
|
||||||
|
"collide",
|
||||||
|
d3.forceCollide().radius(() => 60)
|
||||||
|
)
|
||||||
|
.force("yPos", d3.forceY().strength(1))
|
||||||
|
.force("xPos", d3.forceX().strength(1))
|
||||||
|
.force("charge", d3.forceManyBody().strength(-520))
|
||||||
|
.force(
|
||||||
|
"link",
|
||||||
|
d3.forceLink(dataLinks).id((d: { id: string }) => d.id)
|
||||||
|
)
|
||||||
|
.force(
|
||||||
|
"center",
|
||||||
|
d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2 - 20)
|
||||||
|
)
|
||||||
|
.on("tick", ticked)
|
||||||
|
.stop();
|
||||||
|
simulationSkip(d3, simulation, ticked);
|
||||||
|
return simulation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const simulationSkip = (d3: any, simulation: any, ticked: any) => {
|
||||||
|
d3.timeout(() => {
|
||||||
|
const n = Math.ceil(
|
||||||
|
Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())
|
||||||
|
);
|
||||||
|
for (let i = 0; i < n; i += 1) {
|
||||||
|
simulation.tick();
|
||||||
|
ticked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default (d3: any, graph: any) =>
|
||||||
|
d3
|
||||||
|
.zoom()
|
||||||
|
.scaleExtent([0.3, 10])
|
||||||
|
.on("zoom", (d: any) => {
|
||||||
|
graph
|
||||||
|
.attr("transform", d3.zoomTransform(graph.node()))
|
||||||
|
.attr(
|
||||||
|
`translate(${d.transform.x},${d.transform.y})scale(${d.transform.k})`
|
||||||
|
);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user