Статьи

Играя с SVG и JavaScript

По какой — то причине, я никогда не брал очень хороший взгляд на SVG (ссылка MozDev) в прошлом. Я концептуально знал, что это был способ описать графику в формате XML, и я знал, что Adobe имеет большой объем истории / поддержки, но я никогда не думал, что это полезно для того, что я делаю в своей повседневной работе. Это было до прошлой недели, когда читатель отправил интересный вопрос.

Читатель хотел взять данные графства для Америки и отобразить их на экране. Первоначально я был в растерянности относительно того, как это будет сделано. Очевидно, что часть AJAX не имела большого значения, но я понятия не имел, как, черт возьми, это будет сделано. Затем читатель связал меня с этим ресурсом — картой Америки в SVG, в которой каждый округ определен в чистых, славных данных. Я провел небольшое исследование и обнаружил, что данные SVG существуют в браузере DOM. Это означает, что вы можете редактировать — насколько я знаю — практически все — что связано с данными. Woot!

Я решил начать просто, хотя. Я знал, что Adobe Illustrator может выплевывать файлы SVG, поэтому я открыл Illustrator и использовал весь свой художественный талант для его создания.

Хотя это и не самая захватывающая графика в мире, она дала мне кое-что для начала. Файл SVG полностью основан на XML. Вы можете увидеть источник этого здесь:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="300px" height="300px" viewBox="0 0 300 300" enable-background="new 0 0 300 300" xml:space="preserve">
<rect x="19" y="37" fill="#52B848" stroke="#000000" stroke-miterlimit="10" width="80" height="59" id="greenBlock" />
<rect x="168" y="53" fill="#E61E25" stroke="#000000" stroke-miterlimit="10" width="80" height="59"/>
<rect x="35" y="179" fill="#DFE21D" stroke="#000000" stroke-miterlimit="10" width="227" height="81"/>
<text transform="matrix(1 0 0 1 102 215)" font-family="'MyriadPro-Regular'" font-size="12" id="textBlock">TESTING</text>
</svg>

Никогда прежде не видя SVG, вы можете догадаться, что делает каждая часть. Я провел небольшое исследование и обнаружил, что одним из способов доступа к данным SVG является вызов DOM getSVGDocument (). Чтобы это работало, вам нужно дождаться события загрузки изображения. Я использовал тег объекта для встраивания файла SVG, добавил прослушиватель событий, а затем сделал две простые модификации.

<script>

function svgloaded() {
  console.log("test");
	var svgEmbed = document.querySelector("#svgembed");
	var svg = svgEmbed.getSVGDocument();

	var td = svg.getElementById("textBlock");
	td.textContent = "Foo";

	var gn = svg.getElementById("greenBlock");
	gn.setAttribute("fill", "#ff0000");

}

document.addEventListener("DOMContentLoaded", function(){

	var svgEmbed = document.querySelector("#svgembed");
	svgEmbed.addEventListener("load", svgloaded);

},false);
</script>


<object data="Untitled-1.svg" id="svgembed"></object>

Не очень захватывающе, но и чертовски просто. Вы можете запустить демо здесь или посмотреть на скриншот ниже.

Я проверил это в Chrome, Firefox и IE10, и он отлично работал там. Из того, что я мог видеть в своем исследовании, «основы» SVG работали довольно хорошо везде, но более продвинутые варианты не были универсально доступны. (Проверьте этот список параметров SVG на CanIUse.com для получения дополнительной информации.)

Поэтому, учитывая, что у нас есть JavaScript-доступ к отдельным частям SVG-документа и мы можем легко изменять эти данные, я оглянулся на SVG-документ американского округа. Это очень большой файл (около 2 мегабайт), содержащий данные, которые «рисуют» каждый округ в теге пути. Вот пример двух стран.

  <path
     style="font-size:12px;fill:#d0d0d0;fill-rule:nonzero;stroke:#000000;stroke-opacity:1;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt;marker-start:none;stroke-linejoin:bevel"
     d="M 345.25498,268.988 L 345.54298,269.051 L 345.62898,269.092 L 347.56698,273.203 L 345.67898,273.306 L 345.24598,272.761 L 344.01998,271.43 L 343.64598,271.151 L 345.25498,268.988"
     id="22121"
     inkscape:label="West Baton Rouge, LA" />
  <path
     style="font-size:12px;fill:#d0d0d0;fill-rule:nonzero;stroke:#000000;stroke-opacity:1;stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt;marker-start:none;stroke-linejoin:bevel"
     d="M 336.45198,274.027 L 338.12498,273.058 L 338.70498,276.839 L 338.56598,277.345 L 337.41198,276.88 L 336.69998,276.384 L 335.77298,276.078 L 334.94298,276.037 L 336.45198,274.027"
     id="22055"
     inkscape:label="Lafayette, LA" />

Поскольку у меня не было доступа к каким-либо реальным данным для отображения, я разработал быстрый скрипт ColdFusion, который анализировал XML, считывал данные округа и создавал значение для каждого округа от 1 до 10. На данный момент давайте предположим, что число показывает, насколько вероятно, что вас съест человек. Мой сценарий затем сохранил этот файл как X.json, где X представляет год. Я создавал файлы с 1990 по 1995 год. Теперь у меня есть данные, мне просто нужно было построить приложение на их основе.

Первое, что я хотел сделать, — убедиться, что этот огромный файл SVG не затянул все приложение. Мой босс, Алан Гринблатт, недавно написал на эту тему: Асинхронная загрузка SVG . Я решил сделать что-то более сложное. Хотя его решение позволило бы мне отложить загрузку SVG, я хотел вообще пропустить загрузку SVG, если браузер поддерживает IndexedDB. Я быстро собрал немного логики, чтобы справиться с этим.

var storeIndexedDB = false;
var db;
		
$(document).ready(function() {
			
	$("#svg-content").html("<p>Loading content...</p>");

	//check for indexeddb and cache
	if("indexedDB" in window) {
		loadViaIndexedDB();                
	} else {
		loadViaAjax();
	}
});

function loadViaIndexedDB() {
	var openRequest = indexedDB.open("svgdemo",1);

	openRequest.onupgradeneeded = function(e) {
		var thisDB = e.target.result;
		
		console.log("running onupgradeneeded");
		
		if(!thisDB.objectStoreNames.contains("binarycache")) {
			var os = thisDB.createObjectStore("binarycache", {autoIncrement:true});
			os.createIndex("name", "name", {unique:true});
		}
	}

	openRequest.onsuccess = function(e) {
		console.log("running onsuccess");
		
		db = e.target.result;

		//Do we have it?
		var transaction = db.transaction(["binarycache"],"readonly");
		var store = transaction.objectStore("binarycache");
		var index = store.index("name");

		var request = index.get("usa.svg");

		request.onsuccess = function(e) {
			
			var result = e.target.result;
			if(result) {
				console.log('had it in store');
				loadSVG(result.data);
			} else {
				//Don't have it, so load Ajax way and flag to cache
				storeIndexedDB = true;
				loadViaAjax();
			}	
		}	

	}	

	openRequest.onerror = function(e) {
		//Do something for the error
	}
   
}
		
function loadViaAjax() {
	$.get("USA_Counties.svg", {}, function(res) {
		console.log("svg loaded via ajax");
		if(storeIndexedDB) {
			console.log("I supposed indexeddb, so will store");
			db.transaction(["binarycache"],"readwrite").objectStore("binarycache").add({data:res,name:"usa.svg"});
		}
		loadSVG(res);
	},"text");

}

Полагаю, это довольно много кода, но на самом деле все сводится к следующим шагам:

  1. Браузер пользователя поддерживает IndexedDB

    1. Откройте базу данных и посмотрите, нужно ли нам создать начальный магазин
    2. Посмотрим, есть ли у нас там данные
    3. Если нет, установите флаг, говорящий о том, что мы готовы сохранить SVG и отмените запрос на загрузку через Ajax.
  2. Нет IndexedDB? Нет проблем. Просто загрузите его через Ajax
  3. И еще одну последнюю проверку , чтобы убедиться , что мы сделали поддержку индексированной , но только еще не загружены. Если это так, сохраните этого щенка.

Конечным результатом является вызов loadSVG.

function loadSVG(data) {
	console.log('about to use the svg');
	$("#svg-content").html(data);
	$(".yearButton").on("click", loadYear);
}

Все, что это делает, это вытягивает SVG в DOM и добавляет слушателей к нескольким кнопкам. Кнопки используются для загрузки удаленных данных JSON, которые я создал ранее. Теперь давайте посмотрим на это.

function loadYear(e) {
	var year = $(this).text();
	$.get(year + ".json", {}, function(res) {
		for(var i=0, len=res.length;i<len; i++) {
			//console.log(res[i].ID);
			var id = res[i].ID;
			var total = res[i].DATA;
			$("#"+id).attr("style","fill:"+dataToCol(total));
		}
	},"json");
}

//I just translate 1-10 to a color
function dataToCol(x) {
	switch(x) {
		case 1: return "#ff0000";
		case 2: return "#d61a00";
		case 3: return "#cd3300";
		case 4: return "#b34d00";
		case 5: return "#9a6600";
		case 6: return "#808000";
		case 7: return "#669A00";
		case 8: return "#4db300";
		case 9: return "#1ad600";
		case 10: return "#00ff00";
	}
}

loadYear — это относительно простой Ajax-вызов нашего JSON-файла. Получив данные, я перебираю все графства, получаю значение и переводю значение 1-10 в цвет с красного на зеленый. Спасибо Клиффу Джонстону (@iClifDotMe) за значения RGB.

Конечный результат довольно крутой, я думаю. JSON-файлы имеют размер около 70 КБ каждый, поэтому их не так уж плохо загрузить. Вы можете увидеть полную демонстрацию здесь: http://www.raymondcamden.com/demos/2013/feb/2/test3.html

Для меня первоначально потребовалось около 5 секунд для загрузки, и каждый вызов Ajax «чувствует» около секунды или около того. Учитывая объем передаваемых данных, я чувствую, что они работают адекватно.

Но потом я решил, что этого недостаточно. Я подумал, что если мы кэшируем формы округов, почему бы не кэшировать удаленные данные? Я быстро настроил сохранение данных в кеше SessionStorage браузера.

function loadYear(e) {

	var year = $(this).text();

	if("sessionStorage" in window && sessionStorage[year]) {
		renderYearData(JSON.parse(sessionStorage[year]));
	} else {
		$.get(year + ".json", {}, function(res) {
			console.log("storing to "+year);
			if("sessionStorage" in window) sessionStorage[year] = res;
			renderYearData(JSON.parse(res));
		},"text")
	}
}

Тривиальная модификация, но если вы немного щелкнете по ней, она должна быть немного более быстрой. Вы можете найти это демо здесь: http://www.raymondcamden.com/demos/2013/feb/2/test4.html