Карта зарплат бюджетников
Сложность: средняя.
Необходимое время: 30 мин.
В статье представлена инструкция по созданию своего компонента для движка InstantCMS2. В конце статьи приведена ссылка на архив с исходным кодом и содержимым всех файлов из этой инструкции. Пример внешнего вида компонента, который можно создать, используя данный туториал, представлен на иллюстрации.
@akiritch в моем предыдущем посте Продолжение поста «Зарплаты ректората МГТУ им. Н.Э. Баумана» усомнился #comment_325445602 в наличии функции отображения зарплат ка карте по регионам. Поэтому публикую здесь на Пикабу исходный код модуля для движка InstantCMS 2, который мы используем на сайте сообщества Зарплата бюджетников - этот исходный код в свое время был опубликован на Хабре (там фрагменты с кодом более читыемые, потому что сохраняются отступы в начале строк и присутствует подсветка синтаксиса PHP).
Для начала несколько слов про движок соц.сети / сообщества / блогосоциальной сети InstantCMS2. Эта бесплатная CMS может являться отличным компромиссом, возможно, лучшим из существующих.
В базовой версии уже заложен более богатый функционал по сравнению с LiveStreet CMS.
Скриншот сравнения функционала не привожу, потому что по ссылке дана информация не по самой последней версии движка InstantCMS.
Достоинства и недостатки InstantCMS2
Из минусов — количество модулей, дополнений, тем для данного движка достаточно ограничено. Качество технической поддержки немного хромает. Живого активного сообщества вокруг данного движка нет, а регистрация на форуме вообще только по приглашению. Но все эти минусы с лихвой перекрывает факт бесплатности движка InstantCMS 2.
Из плюсов — из коробки предоставляется сразу: форум, профили пользователей с возможность добавления в друзья, статьи, блоги, новости, группы, фотоальбомы, статичные страницы сайта. А также комментарии, ленты RSS, поля RSS, возможность настраивать главное меню, нормальная модерация и вполне удобная админка.
Скачать движок InstantCMS 2 с функцией авто-установки можно с официального сайта проекта.
Процесс установки хорошо документирован и интуитивно понятен.
Свой компонент для InstantCMS2
Перейдем непосредственно к вопросу написания отдельного компонента.
Для создания нового компонента создайте папку, в которой будет ваш компонент (назовем его newcomponent), в директории \SiteDirectory\system\controllers\, т.е полный адрес к созданной директории будет \SiteDirectory\system\controllers\newcomponent\ — все буквы в названии компонента должны быть строчными, это важно!
Далее в этой папке создаем файл frontend.php — это главный файл, без которого компонент не будет работать.
В этом файле создаем класс с таким же названием. Название класса совпадает с названием папки. И этот класс наследуется от системного класса cmsFrontend.
В этом классе мы имеем возможность добавлять методы, описывающие действия компонента.
Что такое действия компонента? Давайте взглянем на следующее изображение:
Каждый адрес страницы состоит из нескольких сегментов:
/controller — Название компонента.
/action — Название действия. Каждый компонент может иметь несколько действий внутри себя.
/p1/p2/p3/… — Любое количество параметров, необходимых для этого действия.
Как определяется действие компонента? Определяется публичный метод в классе компонента, который называется actionНазваниеДействияСБольшойБуквы. Для главной страницы компонента siteaddress.ru/newcomponent/ необходимо определить метод actionIndex(). Для внутренней страницы компонента siteaddress.ru/newcomponent/act/ необходимо определить метод actionAct().
<?php
class newcomponent extends cmsFrontend {
public function actionIndex() {
$template = cmsTemplate::getInstance();
$template->render('index');
}
public function actionAct() {
$errors = false;
$form = $this->getForm('newForm');
$is_submitted = $this->request->has('submit');
$newForm = $form->parse($this->request, $is_submitted);
if ($is_submitted){
$errors = $form->validate($this, $newForm);
if (!errors) {
$choropleth = $this->model->getChoropleth($newForm);
}
if (!errors) {
cmsUser::addSessionMessage(LANG_FORM_ERRORS, 'error');
}
}
$template = cmsTemplate::getInstance();
$template->render('act', array(
'form' => $form,
'errors' => $errors,
'newForm' => $newForm
));
}
}
?>
Внимательный читатель заметил использование метода $this->model->getChoropleth().
Для использования методов модели в директории \SiteDirectory\system\controllers\newcomponent\ создаем файл model.php
Однако читать данные мы будем из файлов, поэтому поставим здесь заглушку. Описание файла модели приведено с целью обучения.
<?php
class modelNewComponent extends cmsModel {
public function getChoropleth($average_zarplata) {
$choropleth = array();
return $choropleth;
}
}
?>
Строка $template->render('index'); определяет вывод настоящего шаблона, который должен быть создан в директории \SiteDirectory\templates\default\controllers\newcomponent\. Где \default — название используемой темы на сайте (можно найти и скачать новую тему с сайта сообщества instantcms и изменить используемую тему через админку), папку \newcomponent необходимо будет создать самостоятельно, это папка для шаблонов нового компонента.
В этой папке должен быть создан файл index.tpl.php для главной страницы компонента, и act.tpl.php — для внутренней.
<?php
$this->setPageTitle('Заголовок страницы в названии окна браузера');
$this->addBreadcrumb('Название страницы в цепи хлебных крошек');
$this->addToolButton(array(
'class' => 'item',
'title' => 'Название кнопки в меню действий для перехода на внутреннюю страницу компонента',
'href' => $this->href_to('act')
));
?>
<h1>Главный заголовок страницы</h1>
<p>Содержание страницы</p>
Внутренняя страница компонента будет содержать форму выбора параметров.
Для начала создадим папку \forms\ в папке нашего компонента \SiteDirectory\system\controllers\newcomponent\.
В директории \SiteDirectory\system\controllers\newcomponent\forms\ создаем файл form_newForm.php
Форма будет очень простой, она предлагает пользователю выбрать два параметра из выпадающих списков.
<?php
class formNewcomponentnewform extends cmsForm {
public function init() {
return array(
array(
'type' => 'fieldset',
'childs' => array (
new fieldList('par1', array(
'title' => 'Параметр1',
'items' => array (
"ТекстовыйИдентификатор1" => "ТекстовыйПараметр1",
"ТекстовыйИдентификатор2" => "ТекстовыйПараметр2"
)
)),
new fieldList('par2', array(
'title' => 'Параметр2',
'items' => array (
1 => "1",
2 => "2"
)
))
)
)
);
}
}
?>
Интерактивная SVG картограмма
После этого перейдем к созданию шаблона для внутренней страницы компонента /act — создаем файл act.tpl.php и размещаем его в директории \SiteDirectory\templates\default\controllers\newcomponent\.
Для создания уникального сервиса воспользуемся разработкой пользователя KoGor (пользуясь случаем, хочу передать огромную благодарность за проведенный KoGor 'ом труд и хорошо оформленную и интуитивно понятную статью) — инфограммой карты Российской Федерации с распределением по регионам.
В результате, у нас должна получится примерно такая приятная карта России:
<?php
$this->setPageTitle('Заголовок страницы в названии окна браузера');
$this->addBreadcrumb('Название главной страницы компонента в цепи хлебных крошек', $this->href_to(''));
$this->addBreadcrumb('Название страницы в цепи хлебных крошек');
$arr_par1_id = array('ТекстовыйИдентификатор1' => 1 ,
'ТекстовыйИдентификатор2' => 2
);
$filename='/upload/zarplata-'.$arr_par1_id[$_GET['par1']].'-'.$_GET['par2'].'.csv';
if (!isset ($_GET['par1']) || !isset ($_GET['par2'])) $filename='/upload/zarplata-1-1.csv';
$this->renderForm($form, $newForm, array(
'action' => '',
'method' => 'get',
'toolbar' => false
), $errors);
?>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="http://d3js.org/queue.v1.min.js"></script>
<script type="text/javascript" src="http://d3js.org/topojson.v0.min.js"></script>
<!-- <script type="text/javascript" src="http://d3js.org/topojson.v1.min.js"></script> -->
<style>
path {
stroke:white;
stroke-width: 1px;
}
body {
font-family: Arial, sans-serif;
}
.city {
font: 10px sans-serif;
font-weight: bold;
}
.legend {
font-size: 12px;
}
div.tooltip {
position: absolute;
text-align: center;
width: 150px;
height: 25px;
padding: 2px;
font-size: 10px;
background: #FFFFE0;
border: 1px;
border-radius: 8px;
pointer-events: none;
}
</style>
<script type="text/javascript">
var width = 720,
height = 375;
// Setting color domains(intervals of values) for our map
var color_domain = [10000, 15000, 20000, 30000, 50000]
var ext_color_domain = [0, 10000, 15000, 20000, 30000, 50000]
var legend_labels = ["до 10000 руб.", "10000-15000 руб.", "15000-20000 руб.", "20000-30000 руб.", "30000-50000 руб.", "от 50000 руб."]
var color = d3.scale.threshold()
.domain(color_domain)
.range(["#ff1300", "#ff4e40", "#ff7d73", "#ffba00", "#ffcb40", "#adfcad"]);
var div = d3.select("form").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var svg = d3.select("form").append("svg")
.attr("width", width)
.attr("height", height)
.style("margin", "10px auto");
var projection = d3.geo.albers()
.rotate([-105, 0])
.center([-10, 65])
.parallels([52, 64])
.scale(500)
.translate([width / 2, height / 2]);
var path = d3.geo.path().projection(projection);
//Reading map file and data
queue()
.defer(d3.json, "/upload/russia.json")
.defer(d3.csv, "<?php echo $filename; ?>")
.await(ready);
//Start of Choropleth drawing
function ready(error, map, data) {
var rateById = {};
var nameById = {};
data.forEach(function(d) {
rateById[d.RegionCode] = +d.AverageZarplata;
nameById[d.RegionCode] = d.RegionName;
});
//Drawing Choropleth
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(topojson.object(map, map.objects.russia).geometries)
//.data(topojson.feature(map, map.objects.russia).features) <-- in case topojson.v1.js
.enter().append("path")
.attr("d", path)
.style("fill", function(d) {
return color(rateById[d.properties.region]);
})
.style("opacity", 0.8)
//Adding mouseevents
.on("mouseover", function(d) {
d3.select(this).transition().duration(300).style("opacity", 1);
div.transition().duration(300)
.style("opacity", 1)
div.text(nameById[d.properties.region] + " : " + rateById[d.properties.region])
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY -30) + "px");
})
.on("mouseout", function() {
d3.select(this)
.transition().duration(300)
.style("opacity", 0.8);
div.transition().duration(300)
.style("opacity", 0);
})
// Adding cities on the map
d3.tsv("/upload/cities.tsv", function(error, data) {
var city = svg.selectAll("g.city")
.data(data)
.enter()
.append("g")
.attr("class", "city")
.attr("transform", function(d) { return "translate(" + projection([d.lon, d.lat]) + ")"; });
city.append("circle")
.attr("r", 3)
.style("fill", "lime")
.style("opacity", 0.75);
city.append("text")
.attr("x", 5)
.text(function(d) { return d.City; });
});
}; // <-- End of Choropleth drawing
//Adding legend for our Choropleth
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
var ls_w = 20, ls_h = 20;
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.8);
legend.append("text")
.attr("x", 50)
.attr("y", function(d, i){ return height - (i*ls_h) - ls_h - 4;})
.text(function(d, i){ return legend_labels[i]; });
</script>
Исходные данные для картограммы
Для того, чтобы карта заработала, остался последний шаг. Размещаем файлы cities.tsv, russia.json, zarplata-1-1.csv, zarplata-1-2.csv, zarplata-2-1.csv, zarplata-2-2.csv (приведены в архиве, ссылка на который есть в конце статьи) в директории \SiteDirectory\upload\.
Заходим по адресу siteaddress.ru/newcomponent/act/ — здесь все регионы на карте России подкрашены темно-серым цветом и при наведении появляется название региона — NaN. Для отображения каких-нибудь реальных данных замените значения в последнем столбце .csv файлов на численные данные.
Демо компонента
Подобный модуль разработан мною для сайта ЗарплатаБюджетников.РФ в разделе Карта зарплат. Демо модуля можно посмотреть по ссылке. (сейчас данных нет, модуль отключен, а чтобы собрать карту, нужны регистрации с указанием зарплат и регионов от пользователей)
Бонус
Напоследок, небольшой хинт. В дефолтном шаблоне по умолчанию в InstantCMS 2 боковая колонка вместе с меню действий пропадает при уменьшении ширины окна браузера. Но на мобильных девайсах исчезновение боковой колонки и меню действий очень не удобно, т.к. у пользователей пропадает довольно таки много возможных действий. Для изменения этой ситуации можно проделать следующее. Найдите в директории \templates\default\css\ файл theme-layout.css, и замените в нем кусочек кода
/* Media Queries ============================================================ */
@Media screen and (max-width: 980px) {
#body section { width:100% !important; }
#body aside { display:none; }
}
@Media screen and (max-width: 800px) {
#body section { width:100% !important; }
#body aside { display:none; }
/* Media Queries ============================================================ */
@media screen and (max-width: 980px) {
#body section { width:100% !important; }
#body aside { width:100% !important; }
}
@media screen and (max-width: 800px) {
#body section { width:100% !important; }
#body aside { width:100% !important; }
Т.е. по факту необходимо исправить всего 2 строчки #body aside { display:none; } на #body aside { width:100% !important; } — после этого боковая колонка при уменьшении ширины браузера будет съезжать в основную колонку после находящегося в нем контента (перед футером).
Исходный код
Все описанные скрипты и файлы можно скачать в архиве, который нужно будет просто загрузить и распаковать в корневую директорию вашего сайта, работающего на InstantCMS2.