Using lines with arrows pointing from source to target - svg

I am trying to convert a D3 force-directed graph codebase.
Source code:
https://codesandbox.io/s/d3js-draggable-force-directed-graph-py3rf
var nodes = [
{ color: "red", size: 5 },
{ color: "orange", size: 10 },
{ color: "yellow", size: 15 },
{ color: "green", size: 20 },
{ color: "blue", size: 25 },
{ color: "purple", size: 30 }
];
var links = [
{ source: "red", target: "orange" },
{ source: "orange", target: "yellow" },
{ source: "yellow", target: "green" },
{ source: "green", target: "blue" },
{ source: "blue", target: "purple" },
{ source: "purple", target: "red" },
{ source: "green", target: "red" }
];
var svg = d3
.select("svg")
.attr("width", width)
.attr("height", height);
var linkSelection = svg
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("stroke", "black")
.attr("stroke-width", 1);
var nodeSelection = svg
.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", d => d.size)
.attr("fill", d => d.color)
.call(
d3
.drag()
.on("start", dragStart)
.on("drag", drag)
.on("end", dragEnd)
);
the current product looks like this:
And the snippet is here:
/* eslint-disable no-undef */
var width = 600;
var height = 600;
var nodes = [
{ color: "red", size: 5 },
{ color: "orange", size: 10 },
{ color: "yellow", size: 15 },
{ color: "green", size: 20 },
{ color: "blue", size: 25 },
{ color: "purple", size: 30 }
];
var links = [
{ source: "red", target: "orange" },
{ source: "orange", target: "yellow" },
{ source: "yellow", target: "green" },
{ source: "green", target: "blue" },
{ source: "blue", target: "purple" },
{ source: "purple", target: "red" },
{ source: "green", target: "red" }
];
var svg = d3
.select("svg")
.attr("width", width)
.attr("height", height);
var linkSelection = svg
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("stroke", "black")
.attr("stroke-width", 1);
var nodeSelection = svg
.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", d => d.size)
.attr("fill", d => d.color)
.call(
d3
.drag()
.on("start", dragStart)
.on("drag", drag)
.on("end", dragEnd)
);
var simulation = d3.forceSimulation(nodes);
simulation
.force("center", d3.forceCenter(width / 2, height / 2))
.force("nodes", d3.forceManyBody())
.force(
"links",
d3
.forceLink(links)
.id(d => d.color)
.distance(d => 5 * (d.source.size + d.target.size))
)
.on("tick", ticked);
function ticked() {
// console.log(simulation.alpha());
nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y);
linkSelection
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
}
function dragStart(d) {
// console.log('drag start');
simulation.alphaTarget(0.5).restart();
d.fx = d.x;
d.fy = d.y;
}
function drag(d) {
// console.log('dragging');
// simulation.alpha(0.5).restart()
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragEnd(d) {
// console.log('drag end');
simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg
version="1.1"
baseProfile="full"
xmlns="http://www.w3.org/2000/svg"
></svg>
But my goal is draw lines with arrows on the end, where the arrows are pointing to a target from a source. Does anyone know how to properly change the line type, and have it point the right way?

You can create a marker representing the arrow tip in your svg, then use it in a marker-end attribute on your lines.
Here is an example: http://thenewcode.com/1068/Making-Arrows-in-SVG

There is a reasonable solution here but it is D3 v3 and uses a path not line.
Basically, using a marker you calculate the amount to shorten your line so that it starts at the boundary of the source node (i.e. shorten by the source radius) and ends at the boundary of the target node (i.e. shorten at the end by the radius of the target node plus the dimension of the marker).
The hard-coded 12 in the snippet is the dimension of the marker - if you change e.g. markerWidth then you will probably need to play with this constant in the tick function.
So, there are 3 edits to your original code: add the marker, append the marker to the line and in tick, do the math to shorten each end and replot the line. See below:
var width = 600;
var height = 200;
var nodes = [
{ color: "red", size: 5 },
{ color: "orange", size: 10 },
{ color: "yellow", size: 15 },
{ color: "green", size: 20 },
{ color: "blue", size: 25 },
{ color: "purple", size: 30 }
];
var links = [
{ source: "red", target: "orange" },
{ source: "orange", target: "yellow" },
{ source: "yellow", target: "green" },
{ source: "green", target: "blue" },
{ source: "blue", target: "purple" },
{ source: "purple", target: "red" },
{ source: "green", target: "red" }
];
var svg = d3
.select("svg")
.attr("width", width)
.attr("height", height);
// append a path marker to svg defs
svg.append("defs").selectAll("marker")
.data(["dominating"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 0)
.attr("refY", 0)
.attr("markerWidth", 12)
.attr("markerHeight", 12)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
//
var linkSelection = svg
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("stroke", "black")
.attr("stroke-width", 1)
// add marker to line
.attr("marker-end", d => "url(#dominating)");
//
var nodeSelection = svg
.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", d => d.size)
.attr("fill", d => d.color)
.call(
d3
.drag()
.on("start", dragStart)
.on("drag", drag)
.on("end", dragEnd)
);
var simulation = d3.forceSimulation(nodes);
simulation
.force("center", d3.forceCenter(width / 2, height / 2))
.force("nodes", d3.forceManyBody())
.force(
"links",
d3
.forceLink(links)
.id(d => d.color)
.distance(d => 5 * (d.source.size + d.target.size))
)
.on("tick", ticked);
function ticked() {
// console.log(simulation.alpha());
nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y);
linkSelection
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
// recalculate and back off the distance
linkSelection.each(function(d, i, n) {
// current path length
const pl = this.getTotalLength();
// radius of marker head plus def constant
const mrs = (d.source.size);
const mrt = (d.target.size) + 12;
// get new start and end points
const m1 = this.getPointAtLength(mrs);
const m2 = this.getPointAtLength(pl - mrt);
// new line start and end
d3.select(n[i])
.attr("x1", m1.x)
.attr("y1", m1.y)
.attr("x2", m2.x)
.attr("y2", m2.y);
});
}
function dragStart(d) {
// console.log('drag start');
simulation.alphaTarget(0.5).restart();
d.fx = d.x;
d.fy = d.y;
}
function drag(d) {
// console.log('dragging');
// simulation.alpha(0.5).restart()
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragEnd(d) {
// console.log('drag end');
simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg
version="1.1"
baseProfile="full"
xmlns="http://www.w3.org/2000/svg"
></svg>
This works on v4.13 and v5.7.

Related

Chart.js: how to set multiple color to tick of y axes if > or < 0

How to set multiple tick color of y axes: red if < 0 and green if > 0?
Tnx in advance
scales: {
yAxes: [{
ticks: {
fontColor: ["Red","Black","Green"],
callback: function(value,index,values) {
if (value == 0 ) {
return black color;
} else if (value > 0) {
return green color;
} else {
return red color;
}
},
}]
}
You will have to update to v3 of the lib, then you can make use of the scriptable options like this:
options: {
scales: {
y: {
ticks: {
color: (tick) => (tick.tick.value < 0 ? 'red' : tick.tick.value > 0 ? 'green' : '#666')
}
}
}
}
Live example:
var options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [-12, 19, 3, -5, 2, 3],
borderWidth: 1,
borderColor: 'red',
backgroundColor: 'red'
}]
},
options: {
scales: {
y: {
ticks: {
color: (tick) => (tick.tick.value < 0 ? 'red' : tick.tick.value > 0 ? 'green' : '#666')
}
}
}
}
}
var ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.3.0/chart.js"></script>
</body>

gsap split Text on slide

I have this part of a slide code, and I want to implement the gsap split Text in h1 (NexText), so that the result is like this: https://codepen.io/GreenSock/pen/aVJRBg
However, I don't know how to implement that in my code.
function nextSlide(pageNumber) {
const nextPage = pages[pageNumber];
const currentPage = pages[current];
const nextLeft = nextPage.querySelector(".hero .model-left");
const nextRight = nextPage.querySelector(".hero .model-right");
const currentLeft = currentPage.querySelector(".hero .model-left");
const currentRight = currentPage.querySelector(".hero .model-right");
const nextText = nextPage.querySelector("h1");
const portofolio = document.querySelector(".portofolio");
const tl = new TimelineMax({
onStart: function() {
slides.forEach(slide => {
slide.style.pointerEvents = "none";
});
},
onComplete: function() {
slides.forEach(slide => {
slide.style.pointerEvents = "all";
});
}
});
tl.fromTo(currentLeft, 0.3, { y: "-10%" }, { y: "-100%" })
.fromTo(currentRight, 0.3, { y: "10%" }, { y: "100%" }, "-=0.2")
.to(portofolio, 0.3, { backgroundImage: backgrounds[pageNumber] })
.fromTo(
currentPage,
0.3,
{ opacity: 1, pointerEvents: "all" },
{ opacity: 0, pointerEvents: "none" }
)
.fromTo(
nextPage,
0.3,
{ opacity: 0, pointerEvents: "none" },
{ opacity: 1, pointerEvents: "all" },
"-=0.6"
)
.fromTo(nextLeft, 0.6, { y: "-100%" }, { y: "-10%" }, "-=0.6")
.fromTo(nextRight, 0.4, { y: "100%" }, { y: "10%" }, "-=0.8")
.fromTo(nextText, 0.3, { opacity: 0, y: 0 }, { opacity: 1, y: 0 })
.set(nextLeft, { clearProps: "all" })
.set(nextRight, { clearProps: "all" });
current = pageNumber;
}
In particular, I don't know how to make these two parts combine:
const nextText = nextPage.querySelector ("h1");
and
var mySplitText = new SplitText ("# quote", {type: "chars, words, lines"})
What you are trying to achieve can be done via:
var variableThatStoresMySplit H1= new SplitText (nextText , {type: "chars"})
And then later in your code you have to use a stagger to through the array SplitText created for you by doing:
gsap.staggerFrom(variableThatStoresMySplit .chars, 0.5, {y:20, opacity:0}, 0.04)
I hope this solves your issue

Connect 2 elements jointjs

I'm a newbie in jointjs. Today I have a small example as below:
I have a start Activity
var startEndActivity = function (x, y, name, fillColor, textColor, size) {
fillColor = fillColor || '#007FBE';
textColor = textColor || "#000";
size = size || { width: 100, height: 40 };
var rect = new joint.shapes.basic.Rect({
position: { x: x, y: y },
size: size,
attrs: {
rect: { fill: fillColor, rx: 5, ry: 5, 'stroke-width': 1, stroke: '#002F5D' },
text: {
text: name, fill: textColor,
'font-size': 14, 'font-family': 'sans-serif'
}
}
});
graph.addCell(rect);
return rect;}
I have a condition Activity
var activityDecision = function (x, y, name, fillColor, textColor, size{
fillColor = fillColor || '#BF664C';
textColor = textColor || "#080808";
size = size || { width: 200, height: 60 };
var node = new joint.shapes.basic.Rhombus({
position: { x: x, y: y },
size: size,
});
node.attr({
rect: { fill: fillColor, 'stroke-width': 1, stroke: 'white' },
text: {
text: name, fill: textColor,
}
});
graph.addCell(node);
return node;}
I want to click on start activity and I can draw a arrow to connect between 2 elements. Thank you so much
The most common approach that I know of is to use ports on your elements. This link should get you started on that route:
WORKING WITH PORTS
If you prefer to have the entire element behave as a port you need to look into the "magnetic" attribute. This link should help you get started researching what you need (especially the first answer):
How to interactively create links in JointJS
I found solution for this. Thank you so much. Just add more attribute like this
el.attr('rect/magnet', true).attr('text/pointer-events', 'none');
function create(type) {
var link = new joint.dia.Link({
source: { x: 10, y: 20 },
target: { x: 350, y: 20 },
attrs: {}
});
link.prop({...});
link.addTo(graph)
}
//init connection:
new joint.dia.Link({
source: { id: 'source-id' },
target: { id: 'target-id', port: 'port_id'}
});
In order to connect two element you have to work with Ports (documentation):
My best advice for you is to learn how to implement ports by looking in the JointJS source code, as a reference look for object: joint.shapes.devs.Model (live demo + source code inside)
something like this:
var myRect = joint.shapes.devs.Model.extend({
portMarkup: '<circle class="port-body myCustomClass"/>',
defaults: _.defaultsDeep({
type: 'myRect',
}, joint.shapes.basic.Generic.prototype.defaults),
});
and inside of startEndActivity function change the var rect to:
var startRect = new myRect({
position: { x: x, y: y },
size: size,
attrs: {
rect: { fill: fillColor, rx: 5, ry: 5, 'stroke-width': 1, stroke: '#002F5D' },
text: {
text: name, fill: textColor,
'font-size': 14, 'font-family': 'sans-serif'
}
},
ports: {
groups: {
'out': {
position: {
name: 'right'
},
attrs: {
'.port-label': {
fill: '#000'
},
'.port-body': {
fill: '#fff',
stroke: '#000',
r: 10,
magnet: true
}
},
label: {
position: {
name: 'right',
args: {
y: 10
}
}
}
}
}
});
do the same for the second element.

d3.js: zoom and drag failure

I'm using d3.js but I'm having a problem. When I zoom in SVG, is not commensurate. Also, after dragging, the handling of SVG I made; a further drifting. I'm giving the necessary code. What can I do to fix this code?
Code:
/* Tünelin bütün elemanları için bir dizi oluşturuldu
Daha sonra her ayrı eleman için dizi oluşturulup,
datalar bu doğrultuda çekilecektir.
*/
var tunnelElements = new Array();
tunnelElements = [{ "x": 0, "y": 0, "radius": 10, "color" : "green" },
{ "x": 25, "y": 25, "radius": 10, "color" : "green"},
{ "x": 50, "y": 50, "radius": 10, "color" : "green" },
{ "x": 75, "y": 75, "radius": 10, "color" : "green"},
{ "x": 100, "y": 100, "radius": 10, "color" : "green" },
{ "x": 125, "y": 125, "radius": 10, "color" : "green" },
{ "x": 62.5, "y": 62.5, "radius": 10, "color" : "red" },
{ "x": 0, "y": 125, "radius": 10, "color" : "purple" },
{ "x": 25, "y": 100, "radius": 10, "color" : "purple" },
{ "x": 50, "y": 75, "radius": 10, "color" : "purple" },
{ "x": 75, "y": 50, "radius": 10, "color" : "purple" },
{ "x": 100, "y": 25, "radius": 10, "color" : "purple" },
{ "x": 125, "y": 0, "radius": 10, "color" : "purple" }];
/* Tünel elemanları datadan çekilip circles dizisine
kopyalanmaktadır.
*/
var circles = []
for (var i = 0; i < tunnelElements.length; i++) {
circles[i] = tunnelElements[i];
};
console.log(circles);
var width = 719, height = 262;
var X = d3.scale.linear()
var Y = d3.scale.linear()
/* Semantic zoom için zoom değişkeni oluşturuldu */
var zoom = d3.behavior.zoom()
.x(X).y(Y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
/* Alternatif zoom
.on("zoom", function () {
circle.attr("transform", transform)
});
*/
/* Elementler svg olarak oluşturuldu */
var svg = d3.select("#d3")
.append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
/* Road background çağırdık */
var road = svg.selectAll("image").data([0]);
road.enter()
.append("svg:image")
.attr("xlink:href", "http://ahmetates.com.tr/road.svg")
.attr("width", width)
.attr("height", height);
var circle;
/* Bütün elemenlar seçilip transform fonksiyonu
sayesinde drag and drop özelliği kazandı */
circle = svg.selectAll("circle")
.data(circles)
.enter().append("circle")
.attr("transform", transform);
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
/* Fonksiyonlar */
function zoomed() {
road.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
circle.attr("transform", transform)
}
function transform(d) {
return "translate("+X(d.x)+", "+Y(d.y)+")";
}
function generateit(){
var circles = svg.selectAll("circle");
var json_circles = JSON.stringify(circles.data());
d3.select("#console").html('tunnelElements = '+json_circles+';');
}
d3.select("#exportit").on("click", generateit);
var circleAttributes = circle
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", function (d) { return d.radius; })
.style("fill", function(d) { return d.color; })
.call(drag);
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
#d3 svg{
margin:30px;
padding:10px;
border:1px solid #333;
width: 100%;
height: auto;
}
.line{
stroke-width: 3px;
}
#exportit {
background:#000;
cursor:pointer;
color:#fff;
width:45px;
padding:2px 4px;
}
<script src="http://d3js.org/d3.v3.min.js"></script>
<div id="exportit">Export</div>
<div id="d3"></div>
<div id="console"></div>
EDIT:
It seems that problem is not explained very well. The first problem can be traced like this: drag any circle, then drag background and you will see that dragged circle moved to random position. The second problem is scaling and you can trace that zooming in and out, then you can see that positions of circles are changed according to background.
You should make a main group and in that add all the components to which you want to give zoom and drag.
Something like this:
var maingroup = svg.append("g").call(zoom);
//add road and circles in this maingroup
//drag events on circle as you had done no change in that.
Here is a working fiddle this should give more clarity:
http://jsfiddle.net/cyril123/ow8r5n6f/3/

How to mark discrete points on a time series graph using D3 / Rickshaw?

I'm using Rickshaw to create a live-updating time series graph.
Here is the demo: http://abhshkdz.github.io/icuvisualanalytics/prototypes/rickshaw.html
The data is in csv format (time,value), and this is the core javascript for the visualization:
var count = 0, index=0;
var margin = {top: 10, right: 10, bottom: 10, left: 10},
width = window.innerWidth - margin.right - margin.left - 100,
height = window.innerHeight - margin.top - margin.bottom - 100;
var graph = new Rickshaw.Graph( {
element: document.querySelector("#chart"),
width: width,
height: height,
renderer: 'line',
min: -300,
max: 500,
preserve: true,
series: new Rickshaw.Series.FixedDuration(
[
{
name: 'ECG',
color: palette.color()
}
],
undefined,
{
timeInterval: 12.5,
maxDataPoints: 400,
timeBase: data[index][count].x
})
})
var x_axis = new Rickshaw.Graph.Axis.Time( { graph: graph } );
var y_axis = new Rickshaw.Graph.Axis.Y( {
graph: graph,
orientation: 'left',
tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
element: document.getElementById('y_axis')
} );
var hoverDetail = new Rickshaw.Graph.HoverDetail( {
graph: graph
} );
graph.render();
setInterval(function () {
if (count == 2397) {
count = 0;
index++;
}
var d = {'ECG': data[index][count+=3].y};
graph.series.addData(d);
graph.render();
}, 12.5);
Now there is another set of data which is generated by an algorithm. That data is also in csv format (time,value). It basically finds the peaks of this plot. Using that data, I want to mark those points on this visualization itself.
As far as I looked, Rickshaw does not natively support multiple series using different renderers (either both have to be line or both scatter plots etc.).
So how should I go about this?
The multi renderer feature was added about a month ago. This example shows how to combine several renderers in one chart. The relevant code of the example:
var graph = new Rickshaw.Graph( {
element: document.getElementById("chart"),
renderer: 'multi',
width: 900,
height: 500,
dotSize: 2,
series: [
{
name: 'temperature',
data: seriesData.shift(),
color: 'rgba(255, 0, 0, 0.4)',
renderer: 'stack'
}, {
name: 'heat index',
data: seriesData.shift(),
color: 'rgba(255, 127, 0, 0.4)',
renderer: 'stack'
}, {
name: 'dewpoint',
data: seriesData.shift(),
color: 'rgba(127, 0, 0, 0.3)',
renderer: 'scatterplot'
}, {
name: 'pop',
data: seriesData.shift().map(function(d) { return { x: d.x, y: d.y / 4 } }),
color: 'rgba(0, 0, 127, 0.4)',
renderer: 'bar'
}, {
name: 'humidity',
data: seriesData.shift().map(function(d) { return { x: d.x, y: d.y * 1.5 } }),
renderer: 'line',
color: 'rgba(0, 0, 127, 0.25)'
}
]
});

Resources