четверг, 19 февраля 2015 г.

Облако тегов средствами XSLT

Облако тегов средствами XSLT

Не так давно столкнулся с необходимостью реализовать на одном из проектов то, что в народе называют «облаком тегов» — набор ссылок, в котором наиболее «весомые» элементы имеют бОльший размер. Для этого можно было бы, конечно, посчитать и получить все необходимые данные в PHP, на котором работает проект, но мне хотелось сделать конечный вариант отображения на XSLT и CSS, чтобы все необходимые величины для конфигурирования максимальных/минимальных размеров шрифта, например, были заданы в представлении, а не в логике приложения.

Возможно, кому-то мой опыт окажется полезным, поэтому публикую конечное решение здесь.

Итак, на входе у нас есть простейший XML с тегом и количеством его упоминаний:

<?xml version="1.0" encoding="utf-8" ?>
<cloud>
    <row id="1">
        <name>биология</name>
        <weight>2</weight>
    </row>
    <row id="2">
        <name>русский язык</name>
        <weight>20</weight>
    </row>
    <row id="3">
        <name>алгебра</name>
        <weight>13</weight>
    </row>
    <row id="4">
        <name>география</name>
        <weight>2</weight>
    </row>
    <row id="5">
        <name>физкультура</name>
        <weight>20</weight>
    </row>
    <row id="6">
        <name>астрономия</name>
        <weight>1</weight>
    </row>
    <row id="7">
        <name>правоведение</name>
        <weight>7</weight>
    </row>
    <row id="8">
        <name>история</name>
        <weight>14</weight>
    </row>
</cloud>


* This source code was highlighted with Source Code Highlighter.


теперь сделаем преобразование:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
    <xsl:template match="/">
        <html>
            <body>
                <xsl:apply-templates />
            </body>
        </html>
    </xsl:template>
   
    <xsl:template match="cloud">
        <xsl:variable name="theMax" select="row[not(weight < ../row/weight)]/weight" />
        <xsl:variable name="theMin" select="row[not(weight > ../row/weight)]/weight" />
       
        <xsl:variable name="perc100" select="$theMax - $theMin"/>
        <xsl:variable name="perc1">
            <xsl:choose>
                <xsl:when test="$perc100 = 0">100</xsl:when>
                <xsl:otherwise><xsl:value-of select="100 div $perc100"/></xsl:otherwise>
            </xsl:choose>
        </xsl:variable>
       
        <xsl:variable name="maxfont">26</xsl:variable>
        <xsl:variable name="minfont">11</xsl:variable>
       
        <xsl:variable name="font" select="$maxfont - $minfont"/>
        <div style="width:300px">
        <xsl:for-each select="row">
                <xsl:variable name="size" select="$minfont + ceiling($font div 100 * ((weight - $theMin) * $perc1))"/>
                <a href="/tag/{name}" style="font-size: {$size}px">
                    <xsl:value-of select="name" />
                </a>
                <xsl:if test="position() != last()"><xsl:text> </xsl:text></xsl:if>
        </xsl:for-each>
        </div>
    </xsl:template>
   
</xsl:stylesheet>


* This source code was highlighted with Source Code Highlighter.

В переменные $minfont и $maxfont задаются значения размера шрифта тега в пикселях. Остальные вычисления необходимы для того, чтобы понять, какое количество пикселей нужно прибавить к $minfont в зависимости от величины «веса» того или иного тега. Можно перенести это из переменных в параметры шаблона, и тогда в разных местах сайта можно будет указывать разные значения максимального и минимального размеров шрифтов при вызове шаблона, для более гармоничного отображения.
В результате вычислений значения изменяются довольно плавно, и теги с небольшими различиями в весе при небольшом диапазоне от $minfont до $maxfont будут иметь одинаковый размер.

В результате представленного выше преобразования, получаем следующий HTML:

<html>
    <body>
        <div style="width: 300px;">
        <a href="/tag/биология" style="font-size: 12px;" title="weight: 2">биология</a>
        <a href="/tag/русский язык" style="font-size: 26px;" title="weight: 20">русский язык</a>
        <a href="/tag/алгебра" style="font-size: 21px;" title="weight: 13">алгебра</a>
        <a href="/tag/география" style="font-size: 12px;" title="weight: 2">география</a>
        <a href="/tag/физкультура" style="font-size: 26px;" title="weight: 20">физкультура</a>
        <a href="/tag/астрономия" style="font-size: 11px;" title="weight: 1">астрономия</a>
        <a href="/tag/правоведение" style="font-size: 16px;" title="weight: 7">правоведение</a>
        <a href="/tag/история" style="font-size: 22px;" title="weight: 14">история</a>
        </div>
    </body>
</html>


* This source code was highlighted with Source Code Highlighter.

А выглядит это так:


Таким образом генерировать облако тегов можно и при помощи client-side XSLT-преобразований.

Конструктивная критика приветствуется. Буду рад, если пригодится не только мне. :)
+22
1045
57
crizis 0,7

Комментарии (31)

+2
remal#
Во-первых, лучше указывать относительные размеры шрифтов — проценты или em'ы.

Во-вторых, лучше для вычисления размеры шрифты использовать логарифмическую шкалу. На эту тему было немало статей и сравнения внешнего вида однозначно в пользу нее.
–1
AndrewMayorov#
А еще лучше ограничить множество весов каким-нибудь значением, и сделать по css-классу для каждого из них. Тогда можно будет размер подстраивать по месту. А можно и не ограничиваться одним размером. Хорошо еще, например, цвет менять.

Пример со сменой цвета можно посмотреть тут:
www.yakhnichmotorsport.ru/photos.sdf/ru/gallery/photo/s2010
0
crizis#
Распоряжаться можно как угодно: хотите — задайте в minfont и maxfont значения 1 и 6 соответственно, а в отрисовке ссылки вместо font-size: пишите class=«cloud{$size}», предварительно создав определения для селекторов .cloud1 … .cloud6

Цвето тоже можно изменять в зависимости от веса. Делать можно всякое, тут уже от фантазии зависит. Моей целью было показать рассчёт значений для привзяки чего бы то ни было для облака тегов средствами XSLT, т.е. вынос этой части вместе с параметрами в слой представления из логики приложения, где, согласно моим наблюдениям, это делают значительно чаще.
0
AndrewMayorov#
Веса вы все равно на сервере определяете. Поэтому можно пойти чуть дальше и сразу распределить все веса (количества повторений каждого тэга) по уровням. Причем распределение нужно начинать от середины множества размеров. Чтобы если все веса одинаковые, получались бы тэги среднего размера, а не минимального, например.

Я очень поддерживаю посты про XSLT, потому что считаю, что он сильно недооценен сообществом, но не надо микроскопом гвозди забивать.
0
crizis#
> Веса вы все равно на сервере определяете.

Не совсем так. Веса могут уже лежать в БД, например, в таблице вида «tag — weight», так что значительно удобнее получить всё одним запросом и без дополнительных расчётов выгрузить данные в XML, оставив построение «плюшек» на совести слоя представления. Кроме того, эти или подобные данные вполне могут быть втянуты в XSLT при помощи apply-imports в виде какого-то XML уже на этапе преобразования.

> Чтобы если все веса одинаковые, получались бы тэги среднего размера, а не минимального

Такой задачи я себе просто не ставил, но и её можно реализовать с помощью предложенной схемы.

> но не надо микроскопом гвозди забивать

Не согласен, что предложенное Вами сравнение относится к данной задаче. Если XSLT — это язык для преобразования данных в некоторое представление, то почему нельзя с его помощью строить то, что нужно? Почему, по Вашему мнению, данная задача лежит вне области применения XSLT?
0
AndrewMayorov#
Почему, по Вашему мнению, данная задача лежит вне области применения XSLT?
Я считаю, что это антиреклама. Ну нужно публиковать такие примеры, ибо они только больше оттолкнут людей от XSLT. Его критикуют за громоздкий синтаксис, и тут мы всю эту проблему видим в полный рост. Разве кто-то из критиков восхитится? Нет, он скажет: «во-во, посмотрите на это убожество!».

Надо показывать сильные сторон XSLT — автоматический подбор наиболее подходящих шаблонов, импорты, максимальный уход от императивного стиля. Это то, в чем XSLT — микроскоп. И не надо показывать то, что проще делать в обычном языке, ибо тут сравнение будет не в пользу XSLT.
0
crizis#
> Я считаю, что это антиреклама. … Его критикуют за громоздкий синтаксис

А я слышал критику в адрес его возможностей. :)

> Разве кто-то из критиков восхитится? Нет, он скажет: «во-во, посмотрите на это убожество!».

Те, кто хочет критиковать, всегда найдут для этого повод: это может быть синтаксис, бедный набор функций, отсутствие циклов (именованные шаблоны с рекурсией не в счёт) или скорость преобразования, «заваленный горизонт». На это не нужно обращать внимание.

Мой пример скорее для тех, кто не испугался технологии, но всё-таки считает, что функций в XSLT мало — я лишь показываю, что XSLT хорош не только для соединения разбросанных по XML-дереву кусочков информации с минимумом усилий, но и для решения более сложных задач по анализу этих данных. Никто не говорит, что в XSLT есть функция «magic», и печатать придётся больше символов, чем в другом языке. Но надо понимать, что XSLT имеет и другие стороны, с которыми тоже можно уживаться и реализовывать разные штуковины.

В конце концов, размеры шрифтов для облака тегов — это сугубо интерфейсная вещь, не имеющая никакого отношения к тому, что принято называть «контролером». Сегодня мы выводим упорядоченный список тегов с количеством упоминаний, а завтра решим преобразовать его в «облако» — разве это задача контролера, если представлению достаточно и тех данных, что были использованы раньше?
0
AndrewMayorov#
Критика в адрес возможностей напрямую проистекает из непонимания специфики языка.

Насчет размеров шрифтов, то есть кто должен их рассчитывать, тезис спорный. Тут можно спорить так долго и так бессмысленно, что я предпочту не начинать. :)

Я выступил не с критикой решения, но с критикой вообще выбора такого класса задач для демонстрации возможностей.
0
crizis#
Переделать размер с пикселей на проценты недолго. Я не верстальщик, и использую, как правило, размеры в пикселях — мне так проще понимать, что я получу в итоге ещё до того, как вижу это. :)
0
Sytrus#
Можно пример в em?
0
crizis#
Размер от 1 до 2 em, затем убираем округление и получаем значения со знаками после запятой:


<xsl:variable name="maxfont">2</xsl:variable>
<xsl:variable name="minfont">1</xsl:variable>

<xsl:variable name="size" select="$minfont + $font div 100 * ((weight - $theMin) * $perc1)"/>
<a href="/tag/{name}" style="font-size: {$size}em" title="weight: {weight}">


* This source code was highlighted with Source Code Highlighter.


Результат:

<html>
    <body>
        <div style="width: 300px;">
        <a href="/tag/биология" style="font-size: 1.05263em;" title="weight: 2">биология</a>
        <a href="/tag/русский язык" style="font-size: 2em;" title="weight: 20">русский язык</a>
        <a href="/tag/алгебра" style="font-size: 1.63158em;" title="weight: 13">алгебра</a>
        <a href="/tag/география" style="font-size: 1.05263em;" title="weight: 2">география</a>
        <a href="/tag/физкультура" style="font-size: 2em;" title="weight: 20">физкультура</a>
        <a href="/tag/астрономия" style="font-size: 1em;" title="weight: 1">астрономия</a>
        <a href="/tag/правоведение" style="font-size: 1.31579em;" title="weight: 7">правоведение</a>
        <a href="/tag/история" style="font-size: 1.68421em;" title="weight: 14">история</a>
        </div>
    </body>
</html>


* This source code was highlighted with Source Code Highlighter.
0
Sytrus#
Спасибо. Правда, значение не точное, т.к. 1em = 16px, а 1px ~ .06em, соответственно 12px ~ .75em, а 26px ~ 1.63em. Другими словами, если я задам maxfont 1.63, а minfont .75, правильно ли будет считаться размер шрифта перед выводом результата?
0
crizis#
> правильно ли будет считаться размер шрифта перед выводом результата?

Что-то я не понимаю, видимо. :)

Вы задаёте в $minfont и $maxfont те значения, какие сами захотите. Пиксели, приведённые в топике и EMы в комментарии выше — это просто значения для примера, которые показывают, как я хочу отобразить теги с минимальным и с максимальным весом. А дальше просто пропорционально рассчитываются шаги между ними для промежуточных значений.

Вы можете взять пример из моего комментария выше и установить min и max в 1 и 10 (0.5 и 1, 1 и 1.5, 2.73 и 5 — как Вам нужно для дизайна — решаете Вы) соответственно. Тогда теги с максимальным весом просто будут иметь font-size: 10em, а с минимальным: font-size: 1em. А всё что между этими значениями получит соответствующий своему весу размер в em, и размеры для отображения считать будет уже браузер.

Кстати, если поменять значения местами (сделать min > max), то всё получится наоборот: теги с минимальным весом получат максимальный размер, а теги с максимальным весом станут маленькими. :)
0
Sytrus#
В том все и дело, что шаг между размерами шрифтов в em'ах должен быть .06 (точнее .0625), чтобы размер шрифта считался верно.

Исходя из вашего примера, шаг между размерами составляет .05263em, при расчете размера шрифта получается погрешность ~ 2px, т.е. если 1em = 16px — в вашем примере он составит что-то между 13 и 14px (0.84208em). К тому же, если я поставлю minfont не целое число, а .75 например, погрешность будет еще больше. Понимаете о чем я? :) А потому и прошу привести пример в em. :) Спасибо.
0
crizis#
Самое простое решение, в таком случае, задавать параметры в пикселях, а в результирующий html выводить высоту в em:


<xsl:variable name="maxfont">32</xsl:variable>
<xsl:variable name="minfont">16</xsl:variable>

<xsl:variable name="size" select="($minfont + $font div 100 * ((weight - $theMin) * $perc1)) * 0.06"/>
<a href="/tag/{name}" style="font-size: {$size}em" title="weight: {weight}">



В это случае сохранятся необходимые Вам соотношения. В остальном же надо подумать, как изменить этот алгоритм таким образом, чтобы он учитывал размер необходимого шага.
0
Zitrix#
может, просто ceiling убрать?
0
Insa88#
вы реально трахаете мозг и природу.
0
glebovgin#
Спасибо что поделились, пригодится.
0
crizis#
На здоровье.
0
dmitry_dvp#
Хотелось бы к чему-то придраться конструктивно, да не к чему. Способ поиска максимального — для меня стал приятным удивлением.
Придерусь к мелочам в статье — аттрибуты «id» в примере исходых данных — лишняя информация.
HTML, полученный в конце содержит тайтлы на ссылках, но в XSL о них ни слова =)
0
crizis#
> аттрибуты «id» в примере исходых данных — лишняя информация.

ну, просто выдрал кусок XML оттуда, где это было нужно :) извините.

> полученный в конце содержит тайтлы на ссылках, но в XSL о них ни слова

каюсь, поставил их в тестовый пример после того, как вставил текст шаблона в пост. :)
+1
Zitrix#
theMax, theMin, perc1, maxfont, minfont — так и просится with-param
perc100 так и просится внутрь perc1

а вообще обычная задача, как она на глагну пролезла — не понимаю
0
dmitry_dvp#
«perc1, maxfont, minfont» да, но «theMax, theMin» почему?
0
Zitrix#
рассчет производился бы внутри with-param один раз, а не внутри template для каждого тега
0
crizis#
> theMax, theMin, perc1, maxfont, minfont — так и просится with-param

В тексте топика я указал, что "…Можно перенести это из переменных в параметры шаблона, и тогда в разных местах сайта можно будет указывать разные значения максимального и минимального размеров шрифтов при вызове шаблона…", так что мысли у нас с Вами в этом направлении сходятся. Это вообще можно хранить в какой-нибудь папочке multiuse наравне с построением постраничного вывода или склонения слова после числительного в нужный падеж. :) Предварительно оптимизировав и максимально параметризировав, конечно же. :)

> а вообще обычная задача, как она на глагну пролезла — не понимаю

"…есть многое на свете, друг Горацио…" © Шекспир :)

На самом деле, задач обычных много. Даже эта задача, вчера бывшая для меня чем-то новым завтра станет чем-то обычным. Но мой пример, надеюсь, избавит кого-то от необходимости изобретать велосипед и позволит по–новому взглянуть на XSLT.
Когда мне потребовалось реализовать задачу, я обратился к поисковикам. Ничего, что хоть как-то отвечало бы тому, что мне нужно, я там не нашёл, поэтому реализовал сам. Предполагая, что это может потребоваться кому-то ещё, решил поделиться, ну а раз люди добавляют в избранное и топик оказался на глагне, значит кому-то это оказалось нужно, я так думаю. :)
+1
seriyPS#
С позволения, немного улучшу
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
    <xsl:template match="/">
        <html>
            <body>
                <xsl:apply-templates />
            </body>
        </html>
    </xsl:template>
    
    <xsl:template match="cloud">
        <xsl:variable name="maxfont">26</xsl:variable>
        <xsl:variable name="minfont">11</xsl:variable>

        <xsl:variable name="theMax" select="row[not(weight < ../row/weight)]/weight" />
        <xsl:variable name="theMin" select="row[not(weight > ../row/weight)]/weight" />
        
        <div style="width:300px">
        <xsl:apply-templates select="row">
                <xsl:with-param name="minfont" select="$minfont"/>
                <!-- <xsl:with-param name="pixInWeight" select="($maxfont - $minfont) div ($theMax - $theMin)"> -->
                <xsl:with-param name="pixInWeight">
                 <xsl:choose>
                  <xsl:when test="($theMax - $theMin)=0">0</xsl:when>
                  <xsl:otherwise>
                   <xsl:value-of select="($maxfont - $minfont) div ($theMax - $theMin)"/>
                  </xsl:otherwise>
                 </xsl:choose>
                </xsl:with-param>
        </xsl:apply-templates>
        </div>
    </xsl:template>
    
    <xsl:template match="row">
     <xsl:param name="minfont"/>
     <xsl:param name="pixInWeight"/>
    
        <xsl:variable name="size" select="$minfont + floor(weight*$pixInWeight)"/>
        <a href="/tag/{name}" style="font-size: {$size}px">
            <xsl:value-of select="name" /> 
        </a>
        <xsl:if test="position() != last()"><xsl:text> </xsl:text></xsl:if>
    </xsl:template>
    
</xsl:stylesheet>


Идея та же, просто немного отрефакторил. Из плюсов — отдельный шаблон для показа ссылок и меньше вычислений на каждой итерации, расчетные формулы упростил. Результаты выдает идентичные. Если условиться, что не будет ситуации «у всех тегов одинаковый вес» (деление на ноль), можно убрать choose конструкцию и раскомментировать закомментированный кусок.
0
crizis#
Спасибо, Сергей.

Но всё-таки сложно прогнозировать веса тегов, и вполне может оказаться у всех одинаковое количество, так что лучше от этой ситуации страховаться и не давать теоретической возможности поделить на ноль.
0
kirilloid#
Я бы всю эту «сложную» математику (которую предлагали в комментариях) вообще вынес бы в нормальный язык программирования, а на xslt оставил бы только формирование облака.
0
crizis#
«Формирование облака» предполагает наличие информации о величинах размера текста, и этой очень не хочется отдавать «нормальному языку программирования» потому как относится к интерфейсу. Ну а вся это «сложная» математика как раз избавляет от необходимости лезть в код, если нужно поменять размеры.
Кроме того, предложенный подход даёт возможность многоразового использования данного кода без необходимости предварительных расчётов где-то ещё: нужно на новом проекте облако тегов? — отдали простой XML и подключили нужный шаблон, который сам всё сразу посчитает.

Или я неправильно Вас понял?
0
kirilloid#
Я про подсчёт размера шрифта через логарифмы (они вообще есть в XSLT?) или еще как-то. Если xslt принимает в XML уже размер надписи для каждого тега, то это как раз так, как оно должно быть.
0
crizis#
> Я про подсчёт размера шрифта через логарифмы (они вообще есть в XSLT?)

Нет, там есть довольно скромный набор математических функций. Хотя, этот функционал можно реализовать через именованные шаблоны, например.

> Если xslt принимает в XML уже размер надписи для каждого тега, то это как раз так, как оно должно быть.

С таким утверждением совершенно не согласен, потому что размер надписи — вещь, имеющая отношение только к внешнему виду. :)

0 коммент.:

Отправить комментарий