In this jsFiddle I have an SVG interact.js rect that when resized it snaps to a grid.
This works fine until I start changing margins from zero to a number. The rect is inside a div #mysvg and if I change the margins of the div, the rect snaps incorrectly (there's a shift).
Try changing in the CSS the margins from:
#mysvg {
margin-top: 0px;
margin-left: 0px;
}
To:
#mysvg {
margin-top: 12px;
margin-left: 12px;
}
Then rerun the jsFiddle and you'll see the problem.
Similar issue happens when the body margin is incremented from zero.
How to fix this problem? Is there a way to make the interact.js resize relative to the div, ignoring its margin or where the div is positioned on the page (for example, the div may be located inside another div)?
There is an offset property in interact.snappers.grid which you can use to offset the grid snap:
modifiers: [
interact.modifiers.snap({
targets: [
interact.snappers.grid({
x: 20,
y: 20,
// Here set the offset x, y
// to the margins top and left of the SVG
offset: { x: 12, y: 12 }
}),
]
})
]
You can check it working with #mysvg margin-top and margin-left set to 12px in this jsFiddle, or run the below code snippet:
var svg = document.getElementById('mysvg');
// draw vertical lines
var gridSize = 20;
for (var i=0;i < 100;i++){
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
svg.appendChild(line);
line.setAttribute("x1", (i + 1) * gridSize)
line.setAttribute("y1", 0)
line.setAttribute("x2", (i + 1) * gridSize)
line.setAttribute("y2", 500)
line.setAttribute("stroke-width", 1)
line.setAttribute("stroke", 'gray');
}
// draw vertical lines
for (var i=0;i < 100;i++){
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
svg.appendChild(line);
line.setAttribute("x1", 0)
line.setAttribute("y1", (i + 1) * gridSize)
line.setAttribute("x2", 2000)
line.setAttribute("y2", (i + 1) * gridSize)
line.setAttribute("stroke-width", 1)
line.setAttribute("stroke", 'gray');
}
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
svg.appendChild(rect);
rect.setAttribute('x', 90);
rect.setAttribute('y', 90);
rect.setAttribute('width', 100);
rect.setAttribute('height', 100);
rect.setAttribute('class', 'resize-me');
rect.setAttribute('stroke-width', 2);
rect.setAttribute('stroke', 'black');
rect.setAttribute('fill', 'orange');
interact('.resize-me')
.resizable({
edges: { left: true, right: true, bottom: true, top: true },
margin: 3,
modifiers: [
interact.modifiers.snap({
targets: [
interact.snappers.grid({
x: 20,
y: 20,
// Here set the offset x, y
// to the margins top and left of the SVG
offset: { x: 12, y: 12 }
}),
]
})
]
})
.on('resizemove', function(event) {
var target = event.target;
var x = (parseFloat(target.getAttribute('endx')) || 0)
var y = (parseFloat(target.getAttribute('endy')) || 0)
target.setAttribute('width', event.rect.width);
target.setAttribute('height', event.rect.height);
x += event.deltaRect.left
y += event.deltaRect.top
target.setAttribute('transform', 'translate(' + x + ', ' + y + ')')
target.setAttribute('endx', x)
target.setAttribute('endy', y)
});
svg {
width: 100%;
height: 240px;
-ms-touch-action: none;
touch-action: none;
box-sizing: border-box;
}
body { margin: 0px }
#mysvg {
margin-top: 12px;
margin-left: 12px;
}
<script src="https://cdn.jsdelivr.net/npm/interactjs#latest/dist/interact.min.js"></script>
<svg id="mysvg"></svg>
Related
I need to make it so that it is possible to connect the dice only with straight lines. But to do this, I need each subsequent dice to have a common "top" or "left" coordinate with the previous dice and at the same time a partial random is preserved.
Here is an example:
Here is my code:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Игральные кости</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" type="text/css" href="./style.css">
<link rel="icon" type="image/png" href="./dice.png">
</head>
<body>
<div id="main-container">
<div id="square-container">
</div>
<canvas id="overlay"></canvas>
</div>
</body>
<script src="./script.js"></script>
</html>
html,
body {
margin: 0;
padding: 0;
}
#main-container {
width: 100%;
margin: 10px
}
#square-container {
width: 700px;
height: 600px;
position: absolute;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
background: #dddddd;
border: 5px solid #6b6b6b;
border-radius: 5px;
z-index: 1;
}
#overlay {
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
position: absolute;
top: 0;
left: 0;
}
.js-is-target {
position: absolute;
width: 56px;
height: 56px;
/*background: blue;*/
border-radius: 30px;
}
.square {
top: 13px;
left: 13px;
position: relative;
width: 30px;
height: 30px;
background: red;
background-size: contain;
border-radius: 5px;
z-index: 10;
}
.active-target {
box-shadow: 0 0 0 2px rgba(255, 0, 0, 0.5);
}
//links to images of dice
var imgArr = [
'./dotsOnDice/dice1.jpg',
'./dotsOnDice/dice2.jpg',
'./dotsOnDice/dice3.jpg',
'./dotsOnDice/dice4.jpg',
'./dotsOnDice/dice5.jpg',
'./dotsOnDice/dice6.jpg'
]
//random number from 3 to 6
var randomAmount = Math.floor(Math.random() * (6 - 3 + 1)) + 3;
//limit for recursion
var limitDice = 0;
function addDice() {
// if the limit is equal to a random number, the recursion function is interrupted
if (limitDice === randomAmount) {
return
}
//random number from 2 to 5
let randomAmountImg = Math.floor(Math.random() * (5 - 2 + 1)) + 2;
//random numbers for dice coordinates
let position = {
top: Math.random() * 520,
left: Math.random() * 600
}
//saved previous coordinates for comparison
let positionAfter = {
top: 0,
left: 0
}
//creating an id that we press later
let idSquare = 'idSquare-' + limitDice;
//subtract the previous coordinates from the current coordinates
let top = position.top - positionAfter.top;
let left = position.left - positionAfter.left;
//we compare the positions of the previous coordinates to avoid a large number of overlaps
if (top > 50 || left > 50) {
positionAfter.top = position.top;
positionAfter.left = position.left;
//adding dice to the playing field
let square = $('<div class="js-is-target"><div id="' + idSquare + '" class="square"></div></div>');
square.appendTo('#square-container').css({ left: position.left + 'px', top: position.top + 'px' });
if (limitDice === 0) {
$('#idSquare-0').css('background-image', 'url(' + imgArr[randomAmountImg] + ')');
} else if (limitDice === 1) {
$('#idSquare-1').css('background-image', 'url(' + imgArr[randomAmountImg] + ')');
} else if (limitDice === 2) {
$('#idSquare-2').css('background-image', 'url(' + imgArr[randomAmountImg] + ')');
} else if (limitDice === 3) {
$('#idSquare-3').css('background-image', 'url(' + imgArr[randomAmountImg] + ')');
} else if (limitDice === 4) {
$('#idSquare-4').css('background-image', 'url(' + imgArr[randomAmountImg] + ')');
} else if (limitDice === 5) {
$('#idSquare-5').css('background-image', 'url(' + imgArr[randomAmountImg] + ')');
}
limitDice += 1;
}
addDice();
}
addDice();
//here we store the canvas field
const canvas = document.getElementById('overlay');
//here we set the format in 2D for canvas
const ctx = canvas.getContext('2d');
//here we store the width and height of the canvas field. We set the height and width in HTML
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
//in the variable we store the element that we clicked on
let targetEl = null;
//in the variable we store the coordinates of the center of the element we clicked on
let targetCenter = null;
//we check whether you clicked on the mouse or not
var down = false;
//coordinates of the pressed cursor mouseCoords
var mouseCoords = {
x: 0,
y: 0
}
//coordinates of the starting point
var centerCoords = {
x: 760,
y: 280
}
//в этой функции мы вычисляем где центр у прямоугольника
function getCenter(el) {
//if not true, the function terminates and returns null
if (!el) return null;
//we save the coordinates in the rect constant using the getBoundingClientRect method
const rect = el.getBoundingClientRect();
//returning the x and y coordinates. Here we calculate the center of the rectangle
return {
x: rect.left + rect.width / 2 + window.scrollX,
y: rect.top + rect.height / 2 + window.scrollY,
}
}
//connection points
function joinPoints(ctx, from, to) {
//dotted line size
const dashSize = 10;
//draw a dotted line and write the size in the arguments
ctx.setLineDash([dashSize]);
//setting the gradient
var stroke = ctx.createLinearGradient(from.x, from.y, to.x, to.y);
//setting the color and position
stroke.addColorStop(0, 'blue');
stroke.addColorStop(1, 'red');
//passing the colors to the strokeStyle property to set the color
ctx.strokeStyle = stroke;
//line thickness
ctx.lineWidth = 5;
//draw a contour (line)
ctx.beginPath();
//moves the contour point to the specified coordinates without drawing a line. Starting point
ctx.moveTo(from.x, from.y);
//adds a new contour point and creates a line to this point from the last specified point. End point
ctx.lineTo(to.x, to.y);
//draw a line
ctx.stroke();
}
//when you click the mouse
$(document).mousedown(function (e) {
down = true;
mouseCoords.x = e.pageX;
mouseCoords.y = e.pageY;
});
//when moving the mouse
$(document).mousemove(function (event) {
if (down) {
mouseCoords.x = event.pageX;
mouseCoords.y = event.pageY;
console.log(mouseCoords.x + ' ' + mouseCoords.y);
}
});
//when we release the mouse button
$(document).mouseup(function (e) {
down = false;
});
$(".js-is-target").hover(function (e) {
if (down) {
down = false;
//we transmit data about which js-is-target we clicked on
targetEl = e.target;
//we write down where the center of the object we clicked on is. We calculate using the getCenter() function
targetCenter = getCenter(targetEl);
//changing the coordinates of the starting point
centerCoords.x = targetCenter.x;
centerCoords.y = targetCenter.y;
}
});
//first we count, then we draw a line from ctx.lineTo to ctx.moveTo
function drwaDot() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = 'red';
ctx.moveTo(mouseCoords.x, mouseCoords.y);
ctx.lineTo(centerCoords.x, centerCoords.y);
ctx.stroke();
};
//the drawing interval of our line
var intervalId = setInterval(function () {
if (down) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
joinPoints(ctx, { x: centerCoords.x, y: centerCoords.y }, mouseCoords);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}, 10);
jsfiddle link
under the comment "//we compare the positions of the previous coordinates to avoid a large number of overlaps" changed the condition:
if(top < 10 || left < 10){
dice spawn code
}
That's what came out of it:
In this jsFiddle I have an SVG rect that is resizable using interact.js. There's also a 10px by 10px grid and the .resizable function has included a 10px by 10 px snap. The objective is to resize the rect and have the edges snap exactly on the grid.
In most cases it works fine, but many times it is not, as you can see in the picture below. Maybe an adjustment needs to be done manually on resizeend ? How to fix this problem?
As Erik said:
With this target.setAttribute(attr/a, Math.round(v/10)*10) it seems to work:
.on('resizemove', function(event) {
// Resize the rect, not the group, it will resize automatically
const target = event.target.querySelector('rect');
for (const attr of ['width', 'height']) {
let v = Number(target.getAttribute(attr));
v += event.deltaRect[attr];
target.setAttribute(attr, Math.round(v/10)*10);
}
for (const attr of ['top', 'left']) {
const a = attr == 'left' ? 'x' : 'y';
let v = Number(target.getAttribute(a));
v += event.deltaRect[attr];
target.setAttribute(a, Math.round(v/10)*10);
}
findLocations(rect, handles);
});
Full Demo here - https://jsfiddle.net/alexander_L/1mzs36qL/3/ and below:
const svg = document.getElementById('mysvg');
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
// draw vertical lines
var gridSize = 10;
for (var i=0;i < 100;i++){
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
svg.appendChild(line);
line.setAttribute("x1", (i + 1) * gridSize)
line.setAttribute("y1", 0)
line.setAttribute("x2", (i + 1) * gridSize)
line.setAttribute("y2", 500)
line.setAttribute("stroke-width", 1)
line.setAttribute("stroke", 'gray');
}
// draw horizontal lines
for (var i=0;i < 100;i++){
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
svg.appendChild(line);
line.setAttribute("x1", 0)
line.setAttribute("y1", (i + 1) * gridSize)
line.setAttribute("x2", 2000)
line.setAttribute("y2", (i + 1) * gridSize)
line.setAttribute("stroke-width", 1)
line.setAttribute("stroke", 'gray');
}
svg.appendChild(group);
group.appendChild(rect);
group.setAttribute('class', 'resize-me');
rect.setAttribute('x', 100);
rect.setAttribute('y', 100);
rect.setAttribute('width', 100);
rect.setAttribute('height', 100);
rect.setAttribute('stroke-width', 1);
rect.setAttribute('stroke', 'white');
rect.setAttribute('fill', 'grey');
// Create the handles
const handles = [];
for (let i = 0; i < 8; i++) {
const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
handle.setAttribute('width', 8);
handle.setAttribute('height', 8);
handle.setAttribute('stroke-width', 1);
handle.setAttribute('stroke', 'white');
handle.setAttribute('fill', 'black');
handles.push(handle);
group.appendChild(handle);
}
// Manually assign them their resize duties (R->L, T->B)
handles[0].classList.add('resize-top', 'resize-left');
handles[1].classList.add('resize-top');
handles[2].classList.add('resize-top', 'resize-right');
handles[3].classList.add('resize-left');
handles[4].classList.add('resize-right');
handles[5].classList.add('resize-bottom', 'resize-left');
handles[6].classList.add('resize-bottom');
handles[7].classList.add('resize-bottom', 'resize-right');
// This function takes the rect and the list of handles and positions
// the handles accordingly
const findLocations = (r, h) => {
const x = Number(r.getAttribute('x'));
const y = Number(r.getAttribute('y'));
const width = Number(r.getAttribute('width'));
const height = Number(r.getAttribute('height'));
// Important these are in the same order as the classes above
let locations = [
[0, 0],
[width / 2, 0],
[width, 0],
[0, height / 2],
[width, height / 2],
[0, height],
[width / 2, height],
[width, height]
];
// Move each location such that it's relative to the (x,y) of the rect,
// and also subtract half the width of the handles to make up for their
// own size.
locations = locations.map(subarr => [
subarr[0] + x - 4,
subarr[1] + y - 4
]);
for (let i = 0; i < locations.length; i++) {
h[i].setAttribute('x', locations[i][0]);
h[i].setAttribute('y', locations[i][1]);
}
}
interact('.resize-me')
.resizable({
edges: {
left: '.resize-left',
right: '.resize-right',
bottom: '.resize-bottom',
top: '.resize-top'
},
modifiers: [
interact.modifiers.snap({
targets: [
interact.snappers.grid({
x: 10,
y: 10,
})
]
})
]
})
.on('resizemove', function(event) {
// Resize the rect, not the group, it will resize automatically
const target = event.target.querySelector('rect');
for (const attr of ['width', 'height']) {
let v = Number(target.getAttribute(attr));
v += event.deltaRect[attr];
target.setAttribute(attr, Math.round(v/10)*10);
}
for (const attr of ['top', 'left']) {
const a = attr == 'left' ? 'x' : 'y';
let v = Number(target.getAttribute(a));
v += event.deltaRect[attr];
target.setAttribute(a, Math.round(v/10)*10);
}
findLocations(rect, handles);
});
findLocations(rect, handles);
svg {
width: 100%;
height: 240px;
background-color: #2e9;
-ms-touch-action: none;
touch-action: none;
}
body { margin: 0; }
<script src="https://cdn.jsdelivr.net/npm/interactjs#latest/dist/interact.min.js"></script>
<svg id="mysvg"></svg>
I am working on a project where i need to create resizable rectangles on my image.
I found this codepen useful:
https://codepen.io/taye/pen/xEJeo
the problem is when i write same code in my codepen, its not working same. see
here
https://codepen.io/KushalParikh/pen/rJOeOy.
Can you please tell me what I am missing. I am new to javascript and svg.
my code
var svgCanvas = document.querySelector('svg'),
svgNS = 'http://www.w3.org/2000/svg',
rectangles = [];
function Rectangle (x, y, w, h, svgCanvas) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.stroke = 5;
this.el = document.createElementNS(svgNS, 'rect');
this.el.setAttribute('data-index', rectangles.length);
this.el.setAttribute('class', 'edit-rectangle');
rectangles.push(this);
this.draw();
svgCanvas.appendChild(this.el);
}
Rectangle.prototype.draw = function () {
this.el.setAttribute('x', this.x + this.stroke / 2);
this.el.setAttribute('y', this.y + this.stroke / 2);
this.el.setAttribute('width' , this.w - this.stroke);
this.el.setAttribute('height', this.h - this.stroke);
this.el.setAttribute('stroke-width', this.stroke);
}
interact('.edit-rectangle')
// change how interact gets the
// dimensions of '.edit-rectangle' elements
.rectChecker(function (element) {
// find the Rectangle object that the element belongs to
var rectangle = rectangles[element.getAttribute('data-index')];
// return a suitable object for interact.js
return {
left : rectangle.x,
top : rectangle.y,
right : rectangle.x + rectangle.w,
bottom: rectangle.y + rectangle.h
};
})
.inertia({
// don't jump to the resume location
// https://github.com/taye/interact.js/issues/13
zeroResumeDelta: true
})
.restrict({
// restrict to a parent element that matches this CSS selector
drag: 'svg',
// only restrict before ending the drag
endOnly: true,
// consider the element's dimensions when restricting
elementRect: { top: 0, left: 0, bottom: 1, right: 1 }
})
.draggable({
max: Infinity,
onmove: function (event) {
var rectangle = rectangles[event.target.getAttribute('data-index')];
rectangle.x += event.dx;
rectangle.y += event.dy;
rectangle.draw();
}
})
.resizable({
max: Infinity,
onmove: function (event) {
var rectangle = rectangles[event.target.getAttribute('data-index')];
rectangle.w = Math.max(rectangle.w + event.dx, 10);
rectangle.h = Math.max(rectangle.h + event.dy, 10);
rectangle.draw();
}
});
interact.maxInteractions(Infinity);
for (var i = 0; i < 5; i++) {
new Rectangle(50 + 100 * i, 80, 80, 80, svgCanvas);
}
svg {
width: 100%;
height: 240px;
background-color: #2e9;
-ms-touch-action: none;
touch-action: none;
}
.edit-rectangle {
fill: #92e;
stroke: black;
}
body { margin: 0; }
<svg>
</svg>
Indeed your pen is not working properly. You forgot to add interact.js to your pen as necessary javascript resource. Just go to Settings|Javascript and add something like https://c4d6f7d727e094887e93-4ea74b676357550bd514a6a5b344c625.ssl.cf2.rackcdn.com/interact-1.1.1.min.js
You need to import
https://c4d6f7d727e094887e93-4ea74b676357550bd514a6a5b344c625.ssl.cf2.rackcdn.com/interact-1.1.1.min.js
in your JS
The Goal :
The idea is to create an element grid (image gallery for exemple) that would infinitely loop on itself scrolling on two axes.
There should be no holes nor too much randomness (avoid having the same element randomly falling aside from itself). And this no matter how many element there is in the first place (it seems easy to infinite loop through a grid of 16 (4*4) elements, not that much over 17 (17*1). (My guess is that any prime number of elements is by definition a pain to make a grid of).
So I actually found a wonderful working exemple :
http://www.benstockley.com/
It's actually really close (probably better) than what I was imagining. Now it's using canvas and i tried looking at the javascript and it's a 30000 minified lines long script so I really can't read any core logic behind it.
Math side / Problem solving :
This is the logic and theory behind the problem, the math involved and the mindset.
How the program should process the list of elements so we have no holes, infinite grid, best repartion of the elements over all the axes.
My guess is that it somehow has to be procedural. I'm not sure if we should create grids or loop through the list on every axes (kind of like sudoku ? i don't know);
Pratical side / UI / UX :
Any advice on the technologies involved, pieces of code. I'm guessing it classic DOM is out of the way and that somehow canvas or 2D webgl will be mandatory. But I would love to hear any advice on this side.
Besides all the elements grid processing. The UI and UX involved in exploring a 2D infinite or vast layout in DOM or renderer is somehow not classical. The best technologies or advice on doing this are welcome.
Exemples :
I would welcome any working exemple that somewhat share an aspect of this problem.
I've got a fiddle that's set up to arrange your 2d grid.
It functions by using horizontal and vertical "step sizes". So, moving one step right in the grid advances the horizontal step size in the list. Moving one step down advances the vertical step size in the list (and they accumulate).
We allow the advances in the list to loop back to zero when the end is reached.
It likely makes sense to use a horizontal step size of 1 (so a row of your grid will maintain your list order). For the vertical step size, you want an integer that shares no common divisors with the list length. Though it's no guarantee, I used the (rounded) square root of the list length as something that will work in lots of cases.
I'll reproduce the fiddle here:
var list = ['red','green','blue','cyan','orange','yellow','pink'];
var hstep = 1;
var vstep = Math.ceil(Math.sqrt(list.length));
function getListItem(x,y) {
var index = x * hstep + y * vstep;
return list[index % list.length];
}
var elementSize = 30;
var gutterSize = 10;
function getOffset(x,y) {
return [10 + (elementSize + gutterSize) * x, 10 + (elementSize + gutterSize) * y];
}
var frame = $('.frame');
function drawElement(x,y) {
var listItem = getListItem(x,y);
var offsets = getOffset(x,y);
var element = $('<div></div>').addClass('element').css({
left: offsets[0] + 'px',
top: offsets[1] + 'px',
'background-color': listItem
});
frame.append(element);
}
function drawElements() {
var x = 0, y = 0;
while (10 + (elementSize + gutterSize) * x < frame.width()) {
while (10 + (elementSize + gutterSize) * y < frame.height()) {
drawElement(x,y);
y++;
}
y = 0;
x++;
}
}
drawElements();
.frame {
border: 2px solid black;
margin: 40px auto;
height: 300px;
width: 300px;
position: relative;
overflow: hidden;
}
.frame .element {
position: absolute;
width: 30px;
height: 30px;
}
.buttons {
position: absolute;
top: 0px;
width: 100%;
}
.buttons button {
position: absolute;
width: 30px;
height: 30px;
padding: 5px;
}
button.up {top: 0px; left: 46%;}
button.down {top: 355px; left: 46%;}
button.left {top: 160px; left: 15px;}
button.right {top: 160px; right: 15px;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="frame">
</div>
<div class="buttons">
<button class="up">↑</button>
<button class="down">↓</button>
<button class="left">←</button>
<button class="right">→</button>
</div>
You can see I've left some simple buttons to implement movement, but they are not functional yet. If you wanted to continue implementation along the lines of what I've done here, you could render your elements to a certain range beyond the visible frame, then implement some sort of animated repositioning. The renderElements function here only renders what is visible, so you can use something like that and not get stuck in rendering infinite elements, even though there's no theoretical limit to how far you can "scroll".
#arbuthnott I edited your code to implement the exploration via decrementing relativeX and relativeY variables. Also I inserted an "origin" div (1x1 px, overflow visible). This DOM element will represent the X and Y origin. I'm not sure it's essential but it's really convenient.
Now my function currently remove all elements and reinsert all elements on each update (every 500ms for now).
The idear would be to find a way to compare which elements I need versus which one already exists.
Maybe storing existing elements into an array, and compare the array with the "query" array. Than see just the elements that are missing.
This is the idear, not sure about the implementation (I suck at handling arrays).
https://jsfiddle.net/bnv6mumd/64/
var sources = ['red','green','blue','cyan','orange','yellow','pink','purple'];
var frame = $('.frame'),
origin = $('.origin');
var fWidth = 600,
fHeight = 300,
srcTotal = sources.length,
srcSquare = Math.ceil(Math.sqrt(srcTotal)),
rX = 0,
rY = 0;
var gridSize = 30,
gutterSize = 5,
elementSize = gridSize - gutterSize;
function getSourceItem(x,y) {
var index = x + y * srcSquare;
return sources[Math.abs(index) % srcTotal];
}
function getOffset(x,y) {
return [gridSize * x,gridSize * y];
}
function drawElement(x,y) {
var sourceItem = getSourceItem(x,y);
var offsets = getOffset(x,y);
var element = $('<div></div>').addClass('element').css({
left: offsets[0] + 'px',
top: offsets[1] + 'px',
'background-color': sourceItem,
});
origin.append(element);
}
function init() {
var x = 0, y = 0;
while ( gridSize * x < fWidth) {
while ( gridSize * y < fHeight) {
drawElement(x,y);
y++;
}
y = 0;
x++;
}
}
function updateElements() {
origin.empty();
var x = -Math.trunc(rX / gridSize) -1, y = - Math.trunc(rY / gridSize) -1;
while ( gridSize * x + rX < fWidth) {
while ( gridSize * y + rY < fHeight) {
drawElement(x,y);
y++;
}
y = -Math.ceil(rY / gridSize);
x++;
}
}
function animate() {
rX -= 5;
rY -= 5;
origin.css({left: rX, top: rY})
updateElements();
console.log("relative X : " + rX + " | relative Y : " + rY);
}
setInterval(animate, 500)
init();
.frame {
border: 2px solid black;
margin: 40px auto;
height: 300px;
width: 600px;
position: relative;
overflow: hidden;
}
.origin {
height: 1px;
width: 1px;
position: absolute;
overflow: visible;
}
.frame .element {
position: absolute;
width: 25px;
height: 25px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="frame">
<div class="origin" style="top:0;left:0;"></div>
</div>
This is my final snippet version (i will start to work on real implementation specific to my case now).
I think I optimized in a decent way DOM operations, code structure etc (I am very well open to suggestions though).
I now only update the elements that needs to be updated (click near the frame to show overflow)
https://jsfiddle.net/bnv6mumd/81/
var sources = ['red', 'green', 'blue', 'cyan', 'orange', 'yellow', 'pink', 'purple'];
var frame = $('.frame'),
origin = $('.origin');
var srcTotal = sources.length,
srcSquare = Math.round(Math.sqrt(srcTotal)),
fWidth = 200,
fHeight = 200,
cellSize = 50,
gutterSize = 20,
gridSize = [Math.floor(fWidth / cellSize) + 1, Math.floor(fHeight / cellSize) + 1],
aX = 0, // Absolute/Applied Coordinates
aY = 0,
rX = 0, // Relative/frame Coordinates
rY = 0;
function getSrcItem(x, y) {
var index = x + y * srcSquare;
return sources[Math.abs(index) % srcTotal];
}
function getOffset(x, y) {
return [cellSize * x, cellSize * y];
}
function getY() {
return Math.floor(-rY / cellSize);
}
function getX() {
return Math.floor(-rX / cellSize);
}
function drawElement(x, y) {
var srcItem = getSrcItem(x, y),
offsets = getOffset(x, y),
element = $('<div></div>').addClass('element').css({
left: offsets[0] + 'px',
top: offsets[1] + 'px',
'background-color': srcItem,
}).attr({
"X": x,
"Y": y
});
origin.append(element);
}
function drawCol(x, y) {
var maxY = y + gridSize[1];
while (y <= maxY + 1) {
drawElement(x - 1, y - 1);
y++;
}
}
function drawLign(x, y) {
var maxX = x + gridSize[0];
while (x <= maxX + 1) {
drawElement(x - 1, y - 1);
x++;
}
}
function drawGrid() {
origin.empty();
var x = getX(),
y = getY(),
maxX = x + gridSize[0],
maxY = y + gridSize[1];
while (y <= maxY + 1) {
drawLign(x, y);
x = getX();
y++;
}
}
function updateX(x, y, diffX, diffY) {
if (Math.sign(diffX) == -1) {
drawCol(aX - 1, y);
$('[x=' + (aX + gridSize[0]) + ']').remove();
aX--;
} else if (Math.sign(diffY) == 1) {
drawCol(aX + gridSize[0] + 2, y);
$('[x=' + (aX - 1) + ']').remove();
aX++;
}
}
function updateY(x, y, diffX, diffY) {
if (Math.sign(diffY) == -1) {
drawLign(x, aY - 1);
$('[y=' + (aY + gridSize[0]) + ']').remove();
aY--;
} else if (Math.sign(diffY) == 1) {
drawLign(x, aY + gridSize[0] + 2);
$('[y=' + (aY - 1) + ']').remove();
aY++;
}
}
function animate() {
rX += 1;
rY += 1;
origin.css({
left: rX,
top: rY
});
var x = getX(),
y = getY(),
diffX = x - aX,
diffY = y - aY;
if (diffX) {
updateX(x, y, diffX, diffY)
};
if (diffY) {
updateY(x, y, diffX, diffY)
};
requestAnimationFrame(animate);
}
$('body').click(function() {
$(frame).toggleClass("overflow");
})
drawGrid();
animate();
.frame {
border: 2px solid black;
margin: 100px auto;
height: 200px;
width: 200px;
position: relative;
}
.overflow{
overflow:hidden;
}
.origin {
height: 1px;
width: 1px;
position: absolute;
overflow: visible;
}
.frame .element {
position: absolute;
width: 30px;
height: 30px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="frame overflow">
<div class="origin" style="top:0;left:0;"></div>
</div>
I have the following data in a csv file called BarData.csv:
Fruit,dt,amount
Apple,12/28/2016,-1256
Apple,12/29/2016,-500
Apple,12/30/2016,3694
Apple,12/31/2016,5586
Apple,1/1/2017,4558
Apple,1/2/2017,6696
Apple,1/3/2017,7757
Apple,1/4/2017,8528
Apple,1/5/2017,5543
Apple,1/6/2017,3363
Apple,1/7/2017,5464
Pear,12/25/2017,250
Pear,12/26/2017,669
Pear,12/27/2017,441
Pear,12/28/2017,159
Pear,12/29/2017,357
Pear,12/30/2017,775
Pear,12/31/2017,669
The following html, css, and javascript is in one .html file:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>BAR SINGLE FUNCTION</title>
<script src="http://d3js.org/d3.v3.js"></script>
<style type="text/css">
#radioDiv {
top: 45px;
font-family: verdana;
font-size: 8px;
width: 455px;
}
#TOPbarChart {
position: absolute;
top: 50px;
left: 30px;
width: 750px;
height: 195px;
}
.axis--y path,
.axis--x path {
display: none;
}
.axis--x line,
.axis--y line {
stroke: black;
fill: none;
stroke-width: 2px
}
.yAxis text,
.xAxis text {
font: 7pt Verdana;
stroke: none;
fill: black;
}
.title,
.titleX {
font-family: Verdana;
font-size: 10px;
}
</style>
</head>
<body>
<div id="radioDiv">
<label>
<input id="radioFrt" type="radio" name="frt" value="Apple" class="radioB" checked> APPLE
</label>
<label>
<input type="radio" name="frt" value="Pear" class="radioB"> PEAR
</label>
</div>
<div id="TOPbarChart"></div>
<script type="text/javascript">
var currentFruit = "Apple";
var currentColr = "#00a5b6";
var barDataCSV_Dly = "BarData.csv";
//
//
// radio button
document.getElementById("radioFrt").checked = true;
d3.selectAll('input[name="frt"]').on("change", function change() {
currentFruit = this.value;
TOPbarChart(currentFruit, currentColr);
});
//FORMATS
var parseDate = d3.time.format("%m/%d/%Y").parse;
//
// BASIC SIZING
//
function barChartBasics() {
var margin = {
top: 25,
right: 35,
bottom: 25,
left: 70
},
width = 550 - margin.left - margin.right,
height = 155 - margin.top - margin.bottom,
colorBar = d3.scale.category20(),
barPaddingFine = 1,
barPaddingThick = 2;
return {
margin: margin,
width: width,
height: height,
colorBar: colorBar,
barPaddingFine: barPaddingFine,
barPaddingThick: barPaddingThick
};
}
// create svg element
var basics = barChartBasics();
var svg = d3.select("#TOPbarChart")
.append("svg")
.attr({
"width": basics.width + basics.margin.left + basics.margin.right,
"height": basics.height + basics.margin.top + basics.margin.bottom,
id: "svgTOPbarChart"
});
// create svg group
var plot = svg
.append("g")
.attr({
"transform": "translate(" + basics.margin.left + "," + basics.margin.top + ")",
id: "svgPlotTOPbarChart"
});
var axisPadding = 2;
var leftAxisGroup = svg
.append('g')
.attr({
transform: 'translate(' + (basics.margin.left - axisPadding) + ',' + (basics.margin.top) + ')',
'class': "yAxis axis--y",
id: "yAxisGTOPbarChart"
});
var bottomAxisGroup = svg
.append('g')
.attr({
'class': "xAxis axis--x",
id: "xAxisGTOPbarChart"
});
var titleTxt = svg.append("text")
.attr({
x: basics.margin.left + 12,
y: 20,
'class': "title",
'text-anchor': "start"
})
// create scales with ranges
var xScale = d3.time.scale().range([0, basics.width]);
var yScale = d3.scale.linear().range([basics.height, 0]);
function TOPbarChart(
frt, colorChosen) {
// get the data
d3.csv(barDataCSV_Dly, function(rows) {
TOPbarData = rows.map(function(d) {
return {
"Fruit": d.Fruit,
"dt": parseDate(d.dt),
"amount": +d.amount
};
}).filter(function(row) {
if (row['Fruit'] == frt) {
return true;
}
});
// create domains for the scales
xScale.domain(d3.extent(TOPbarData, function(d) {
return d.dt;
}));
var amounts = TOPbarData.map(function(d) {
return d.amount;
});
var yMax = d3.max(amounts);
var yMin = d3.min(amounts);
var yMinFinal = 0;
if (yMin < 0) {
yMinFinal = yMin;
}
yScale.domain([yMinFinal, yMax]);
// introduce the bars
// var plot = d3.select("#svgPlotTOPbarChart")
var sel = plot.selectAll("rect")
.data(TOPbarData);
sel.enter()
.append("rect")
.attr({
x: function(d, i) {
return xScale(d.dt);
},
y: function(d) {
return yScale(d.amount);
},
width: (basics.width / TOPbarData.length - basics.barPaddingFine),
height: function(d) {
return basics.height - yScale(d.amount);
},
fill: colorChosen,
'class': "bar"
});
// this little function will create a small ripple affect during transition
var dlyRipple = function(d, i) {
return i * 100;
};
sel
.transition()
.duration(dlyRipple) //1000
.attr({
x: function(d, i) {
return xScale(d.dt);
},
y: function(d) {
return yScale(d.amount);
},
width: (basics.width / TOPbarData.length - basics.barPaddingFine),
height: function(d) {
return basics.height - yScale(d.amount);
},
fill: colorChosen
});
sel.exit().remove();
// add/transition y axis - with ticks and tick markers
var axisY = d3.svg.axis()
.orient('left')
.scale(yScale)
.tickFormat(d3.format("s")) // use abbreviations, e.g. 5M for 5 Million
.outerTickSize(0);
leftAxisGroup.transition().duration(1000).call(axisY);
// add/transition x axis - with ticks and tick markers
var axisX = d3.svg.axis()
.orient('bottom')
.scale(xScale);
bottomAxisGroup
.attr({
transform: 'translate(' + (basics.margin.left + ((basics.width / TOPbarData.length) / 2)) + ',' + (basics.margin.top + basics.height) + ')',
})
.transition().duration(1000).call(axisX.ticks(5));
titleTxt.text("Daily: last " + TOPbarData.length + " days");
// console.log(TOPbarData.length)
});
}
//
//
//
//
TOPbarChart(currentFruit, currentColr);
//
//
//
//
</script>
</body>
</html>
When all the data is positive everything is pretty much ok - but when some of the data is negative we can see the result in this plunker demo:
http://plnkr.co/edit/1hudJYkRq2MnuIlwxXZi?p=preview
How do I amend the code so that:
- the negative bars are shown?
- the base of the positive bars moves vertically up when negative numbers are included?
- the vertical movement is also included in the transition?
Above is more than 1 question but help on any would be appreciated.
The key is to play with the y and height attributes of the bars to position them correctly.
For y, change it to:
y: function(d) {
return yScale(Math.max(0, d.amount));
},
And for the height, change it to:
height: function(d) {
return Math.abs(yScale(d.amount) - yScale(0));
},
You can then style the negative bars to make them a different color.
Check the updated Plunkr - http://plnkr.co/edit/q7dQsPW0PiPuwFTy8gLN?p=preview
Edit:
For the coloring part, you can achieve it with a 1 liner if you want to reduce lines and want more simplicity.
Instead of:
fill: function(d) {
var col = colorChosen
if (d.amount < 0) {
col = "#FF0000";
}
return col;
},
});
You can do:
fill: function(d) {
return d.amount < 0 ? "#FF0000" : colorChosen;
},