How can I dynamically add a pattern image to a svg? - javascript

I'm new working with SVG images. I'm using them to paint a jacket several times of different colors without needing to have an image for each colour. This is being done with jQuery.
That was the 'easy' part. I solved it by applying fill: #color; CSS rule to <path> inside <svg>.
The hard part is when trying to fill the svg path with an image. It's really weird. My code is printed just fine in the html inside the <svg>, but not working at all. And when, in chrome's dev tools, I cut the <defs> element, and paste it again exactly where it was, it suddenly works! It's driving me crazy :(.
My code:
var items = [
{
title: 'Africa',
// color: '#776254'
},
{
title: 'Aguamarina',
// color: '#9cd3be'
},
{
title: 'Aluminio',
// color: '#9b9b9b'
},
{
title: 'Amarillo Jamaica',
// color: '#ffcd01'
},
{
title: 'Amatista',
// color: '#4d4169'
},
{
title: 'Ambar Brillante',
// color: '#eb6608'
},
{
title: 'ArĂ¡ndano',
// color: '#604483'
}
];
$(function () {
var PrendaShow = {
$mac_wrapper: $('#prendas-mac-slider-wrapper .mac-slider'),
$fullprints_wrapper: $('#fullprints-gallery'),
img: {
src: 'image.svg',
width: 350,
height: 188
},
init: function () {
this.makeItems();
this.bindEvents();
},
bindEvents: function () {
// events
},
makeItems: function() {
var self = this,
$model = $('<div/>', { class: 'mac-item' });
$model.append($('<div/>', { class: 'item-img' }));
$.each(items, function() {
var $item = $model.clone();
self.$mac_wrapper.append($item);
});
this.svgDraw();
},
svgDraw: function () {
var self = this;
$.get(this.img.src, function(data) {
var $svg = self.normalizeSvg(data);
self.appendImgs($svg);
});
},
normalizeSvg: function (data) {
var $svg = $(data).find('svg');
$svg = $svg.removeAttr('xmlns:a');
$svg.width(this.img.width).height(this.img.height);
return $svg;
},
appendImgs: function ($svg) {
var items = this.$mac_wrapper.find('.mac-item');
$.each(items, function() {
var $clone = $svg.clone(),
color = $(this).data('color');
$clone.find('path').css('fill', 'url(#img1)');
var img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
img.setAttributeNS(null,'height','536');
img.setAttributeNS(null,'width','536');
img.setAttributeNS('http://www.w3.org/1999/xlink','href','http://www.boogdesign.com/examples/svg/daisy-grass-repeating-background.jpg');
img.setAttributeNS(null,'x','10');
img.setAttributeNS(null,'y','10');
img.setAttributeNS(null, 'visibility', 'visible')
$clone.prepend($('<defs/>').html('<pattern id="img1" patternUnits="userSpaceOnUse" width="100" height="100"></pattern>'));
$clone.find('pattern').append(img)
$(this).find('.item-img').append($clone);
});
}
};
PrendaShow.init();
});
#prendas-mac-slider-wrapper {
margin-top: 80px;
}
.mac-slider {
display: flex;
font-size: 0;
padding: 0 100px;
}
.mac-item {
width: 100%;
height: 160px;
min-width: 0;
position: relative;
display: inline-block;
text-align: center;
cursor: pointer;
}
.item-img {
background: url('https://ae01.alicdn.com/kf/HTB1Cqi6KXXXXXbpXVXXq6xXFXXXy/Classic-font-b-White-b-font-font-b-Jacket-b-font-font-b-Men-b-font.jpg') center center no-repeat;
background-size: contain;
pointer-events: none;
position: absolute;
top: 0;
left: -9999px;
right: -9999px;
margin: auto;
}
svg {
mix-blend-mode: multiply;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="hero">
<div class="container-fluid">
<div id="prendas-mac-slider-wrapper" class="row">
<div class="mac-slider">
</div>
</div>
</div>
</div>
I'm not being able to give my exact example because of images upload. I really hope you understand what I'm trying to do here, and what's going wrong.
EDIT: Not sure if this deserves to be in an answer, but I preferred to write it here since I don't think its the right way to do this.
As I knew that, from chrome dev tools, if I deleted the <defs> element and pasted it again, it worked, I tried this and worked:
var svg_html = $svg.html();
$svg.html('');
$svg.html(svg_html);

Your problem is here:
$clone.prepend($('<defs/>').html('<pattern ...
You cannot use jQuery to create SVG elements like this. You are doing it the correct way (with createElementNS()) when you create the <image> element. But you also need to use the same method when creating the <defs> and <pattern> elements.

Related

Change location of pop-up?

I'm relatively new to this and trying to create a pop-up. I'm not sure how to get the pop-up to be in the middle of the page and still populate the box correctly, since it ends up being inside the table. I tried setting a position in the CSS but that didn't work. Maybe I did it wrong?
PHP
foreach ($updateInfo['updates'] as $update) {
echo "<table><tr><td>";
if (isset($update['details']['newsDetails']['fldDatePosted'])) {
echo '<a class="news_popper">'.$update['details']['newsDetails']['fldDatePosted'].'</a><div class="news_pop"><p>'.$update['details']['newsDetails']['fldBody'].'</p></div>';
}
echo "</td></tr></table>";
}
CSS
.news_pop {
display:none;
position: absolute;
z-index: 99999;
padding: 10px;
background: #ffffff;
border: 1px solid #A2ADBC;
}
JS
$(function() {
$('a.news_popper').click(function() {
$(this).next(".news_pop").toggle();
});
});
I would suggest coding a popup in a more object oriented approach, that way you can call it whenever you need it throughout the page, and you can have multiple instances if needed. What I would do is create a constructor for the popup and a div that manages the pop ups.
First the pop up manager:
const popUpManager = (function popUpManager() {
const $popUpManager = document.createElement('div');
$popUpManager.className = "pop-up_manager";
document.body.append($popUpManager);
return {
createPopUp: () => {
const popup = new Popup();
const $popup = popup.getPopup();
$popUpManager.append($popup);
return popup;
}
}
})();
We create a div called pop-up_manager that will house your popup and allow you to place it where ever you need throughout the page.
Then we need to create the blueprint for what a popup is:
class Popup{
constructor() {
this.$popup = document.createElement('div');
this.$popup.className = 'pop-up';
this.$popup.innerHTML = '<div class="exit-button">X</div><div class="body"></div>';
this.$exitButton = this.$popup.querySelector('.exit-button');
this.$body = this.$popup.querySelector('.body');
this.setUpListeners();
}
getPopup() {
return this.$popup;
}
setContent($content) {
if (typeof $content === 'string') {
this.$body.innerHTML = $content;
} else {
this.$body.appendChild($content);
}
}
Every time we call new Popup(); we will generate a div that floats over the page that we can set to house anything we want by passing content to the setContent() method. This will create what is in the pop up.
Finally, the styling can be configured to whatever you want in the css:
.pop-up {
position: fixed;
height: 300px;
width: 300px;
background-color: grey;
/* Positioning */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.body {
height: 100%;
width: 100%;
}
.exit-button {
cursor: pointer;
}
To call the popup from anywhere in you JS all you have to do is:
$('a.news_popper').click(function() {
const popup = popUpManager.createPopUp();
popup.setContent('<h1>INSERT CONTENT TO SHOW USER HERE</h1>');
});
Here is a codepen: CodePen Link

Why does Leaflet.LayerGroup.Collision not work with my L.GeoJSON?

Trying to use the plugin "Leaflet.LayerGroup.Collision.js". I can't see what the error is as it is supposed to hide text when there is a collision. All text is showing but still collide into each other and looks pretty messy on the map.
What can be wrong in the below sample? I have tried to follow the instructions as good as possible but there seems to be something missing!
var point_txt = new L.layerGroup();
function filt_point(feature) {
if (feature.properties.size === "villages") return true;
}
var collisionLayer = L.LayerGroup.collision({ margin: 8 });
$.getJSON("/data/city_villages.geojson", function(json) {
var pointLayer = L.geoJSON.collision(null, {
filter: filt_point,
pointToLayer: function(feature, latlng) {
label = String(('<span class="textLabelclassmall">' + feature.properties.Namn + '</span>');
return new L.marker(latlng, {
icon: createLabelIcon("textLabelclasssmall", label)
});
}
});
var createLabelIcon = function(labelClass, labelText) {
return L.divIcon({
className: labelClass,
html: labelText
});
};
pointLayer.addData(json);
collisionLayer.addLayer(pointLayer);
collisionLayer.addTo(point_txt);
});
style.css:
.textLabelclassmall{
left: 1px;
top: -10px;
background-color: transparent;
display: inline-block;
text-align: left;
white-space:nowrap;
letter-spacing: 0.1em;
font-weight: 500;
font-size: 0.5vw;
}
An instance of Leaflet.LayerGroup.Collision expects the layers added to itself to be L.Markers, and not instances of L.LayerGroup (or instances of derived classes such as L.GeoJSON) - it is simply not prepared for that use case.
Add the individual markers as they are being created, or consider using L.GeoJSON.Collision instead.
I think I find a solution by using background-colourin style.css with transparent and use <span>tag for the label in the js code above. I have updated to the working solution for the code above.

height not matching in Chrome

I'm working through the Single Page Web Applications book and the first example in it isn't working properly in Chrome, but does work in Firefox. The essential code is the following:
.spa-slider {
position: absolute;
bottom: 0;
right: 2px;
width: 300px;
height: 16px;
cursor: pointer;
border-radius: 8px 0 0 0;
background-color: #f00;
}
The JavaScript code is the following:
var spa = (function($) {
var configMap = {
extended_height: 434,
extended_title: 'Click to retract',
retracted_height: 16,
retracted_title: 'Click to extend',
template_html: '<div class="spa-slider"></div>'
},
$chatSlider, toggleSlider, onClickSlider, initModule;
toggleSlider = function() {
var slider_height = $chatSlider.height();
console.log("slide_height: " + slider_height);
if (slider_height == configMap.retracted_height) {
$chatSlider.animate({height: configMap.extended_height})
.attr('title', configMap.extended_title);
return true;
}
if (slider_height == configMap.extended_height) {
$chatSlider.animate({height: configMap.retracted_height})
.attr('title', configMap.retracted_title);
return true;
}
return false;
};
onClickSlider = function(event) {
toggleSlider();
return false;
};
initModule = function($container) {
$container.html(configMap.template_html);
$chatSlider = $container.find('.spa-slider');
$chatSlider.attr('title', configMap.retracted_title)
.click(onClickSlider);
return true;
};
return {initModule: initModule};
}(jQuery));
jQuery(document).ready(function() {
spa.initModule(jQuery('#spa'));
});
My question is essentially the following. The slider doesn't seem to work on Chrome, because console.log("slide_height: " + slider_height); prints 17, so it matches neither of the if guards. On Firefox it prints 16, so the height() property gets the correct value. Can anyone explain why this happens and what is a portable way to write the code?
UPDATE: I use 90% zoom on Chrome and changing it to 100% seems to make the code work correctly. However, the code should clearly work on all zoom levels, so how can this be accomplished? I'm surprised that the book uses code that is so brittle.

Keep / Exclude brushed data in parallel coordinates d3.js

I am working to this example and trying to give it the same functionality for keeping/excluding the information in the brush as this example by Kai.
I have reviewed the code and apparently, Kai's example is built upon V.2 of D3 (at least the script that it calls is there). Because of this most of the coordinate system is not based upon the d3.parcoords.js library. My question is, how would I be able get the same result using parcoords.js ? Do you think it will be needed to modify parcoords.js to include Kai's functionality? I think that the problem is that the method Kai uses conflicts with the parcoords.js methods to draw axes.
I reviewed the parcoords.js documentation and there are no indications of a method or function to extract this information, at least in the same way. I am fairly new to javascript, but any help would be appreciated.
EDIT:
The part of the code I am trying to port is at the very bottom.
<!doctype html>
<title>Decision making tool</title>
<link rel="stylesheet" type="text/css" href="_/d3%20library/d3.parcoords.css">
<link rel="stylesheet" type="text/css" href="_/d3%20library/style.css">
<style>
/* data table styles */
#grid {
height: 198px;
}
.row,
.header {
clear: left;
font-size: 12px;
line-height: 18px;
height: 18px;
}
.row:nth-child(odd) {
background: rgba(0, 0, 0, 0.05);
}
.header {
font-weight: bold;
}
.cell {
float: left;
overflow: hidden;
white-space: nowrap;
width: 100px;
height: 18px;
}
.col-0 {
width: 180px;
}
</style>
<script src="_/d3 library/d3.min.js"></script>
<script src="_/d3 library/d3.parcoords.js"></script>
<script src="_/d3 library/divgrid.js"></script>
<script src="_/script.js"></script>
//The header div includes the buttons that are shown below.
<div id="header">
<h1>EUI assesment tool</h1>
<button title="Zoom in on selected data" id="keep-data" disabled="disabled">Keep</button>
<button title="Remove selected data" id="exclude-data" disabled="disabled">Exclude</button>
</div>
<div id="chart" class="parcoords" style="height:600px;"></div>
<div id="grid"></div>
<script id="brushing">
// quantitative color scale
var green_to_red = d3.scale.linear()
.domain([40, 200])
.range(["green", "red"])
.interpolate(d3.interpolateLab);
var color = function(d) {
return green_to_red(d['Site EUI (kBtu/sq ft)']);
};
var parcoords = d3.parcoords()("#chart")
.color(color)
.margin({
top: 50,
left: 250,
bottom: 20,
right: 20
})
// .composite('lighter')
.alpha(0.25);
// load csv file and create the chart
d3.csv('_/data/Mock Data Chicago.csv', function(data) {
parcoords
.data(data)
.hideAxis(["Property Name", "Address", "Community Area", "ZIP Code", "# of Buildings", "ENERGY STAR Score"])
.mode("queue")
.rate(30)
.interactive()
.render()
.brushMode("1D-axes"); // enable brushing
// create data table, row hover highlighting
var grid = d3.divgrid();
d3.select("#grid")
.datum(data.slice(0, 5))
.call(grid)
.selectAll(".row")
.on({
"mouseover": function(d) {
parcoords.highlight([d])
},
"mouseout": parcoords.unhighlight
});
// update data table on brush event
parcoords.on("brush", function(d) {
d3.select("#grid")
.datum(d.slice(0, 5))
.call(grid)
.selectAll(".row")
.on({
"mouseover": function(d) {
parcoords.highlight([d])
},
"mouseout": parcoords.unhighlight
});
});
});
//Below is the code I copied from Kai's version. I think the if statement is not returning information for the buttons above to enable
if (selected.length < data.length && selected.length > 0) {
d3.select("#keep-data").attr("disabled", null);
d3.select("#exclude-data").attr("disabled", null);
} else {
d3.select("#keep-data").attr("disabled", "disabled");
d3.select("#exclude-data").attr("disabled", "disabled");
};
function keep_data() {
new_data = actives();
if (new_data.length == 0) {
alert("I don't mean to be rude, but I can't let you remove all the data.\n\nTry removing some brushes to get your data back. Then click 'Keep' when you've selected data you want to look closer at.");
return false;
}
data = new_data;
rescale();
}
// Exclude selected from the dataset
function exclude_data() {
new_data = _.difference(data, actives());
if (new_data.length == 0) {
alert("I don't mean to be rude, but I can't let you remove all the data.\n\nTry selecting just a few data points then clicking 'Exclude'.");
return false;
}
data = new_data;
rescale();
}
</script>
It may be something with the if statement not getting information?

CreatePopup replacement

Using regular JavaScript or JQuery I want to replace the createPopup() method like done in this post:
A universal createPopup() replacement?
but I want to use a Div instead of an iFrame. I don't need anything fancy just a simple div which can be styled.
The problem with using a Div is that I have a lot of existing code like this which I would like to remain untouched e.g.
var popup = window.createPopup();
oPopup.document.body.innerHTML = "Click outside <strong>popup</strong> to close.";
In the new createPopup() method below, is there a way to return an object that has the properties document.body.innerHTML to style the Div and the existing code can remain untouched.
if(!window.createPopup){
window.createPopup = function() {
// TODO return div object
}
}
You can use javascript setters and getters in combination with defineProperties to pull off what you are trying to do.
if(!window.createPopup){
window.createPopup = (function() {
// build our object
var o = {
document: {
body: {
_innerHTML: ''
}
}
};
// build the popup
o.document.body._outer = document.createElement('div');
o.document.body._inner = document.createElement('div');
o.document.body._outer.className = 'modal';
o.document.body._inner.className = 'inner';
// attach popup
o.document.body._outer.appendChild(o.document.body._inner);
document.body.appendChild(o.document.body._outer);
// add a property for innerHTML
Object.defineProperties(o.document.body, {
'innerHTML': {
get: function () { return this._innerHTML; },
set: function (x) {
this._innerHTML = x;
this._inner.innerHTML = this._innerHTML;
}
}
});
// return the object
return o;
});
}
var oPopup = window.createPopup();
oPopup.document.body.innerHTML = "Click outside <strong>popup</strong> to close.";
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.5);
}
.modal .inner {
padding: 2em;
background: #fff;
border: 1px solid #eee;
display: inline-block;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

Categories