ASP.NET MVC: История одного проекта "Облако тегов" (часть 11)

ru-RU | создано: 25.06.2012 | опубликовано: 25.06.2012 | обновлено: 30.12.2017 | просмотров за всё время: 9969

Облако меток очень удобный и распространный вариант навигации по сайту. Тема этой статьи - "Облако меток" (или облако тегов, если хотите).

Содержание

ASP.NET MVC: История одного проекта "Готовимся к старту" (часть 1)
ASP.NET MVC: История одного проекта "Всё ради данных" (часть 2)
ASP.NET MVC: История одного проекта "Шаблоны и внешний вид" (часть 3)
ASP.NET MVC: История одного проекта "Еще немного классов" (часть 4)
ASP.NET MVC: История одного проекта "UI - всё для пользователя" (часть 5)
ASP.NET MVC: История одного проекта "UI - Добавление экспоната" (часть 6)
ASP.NET MVC: История одного проекта "UI - Редактирование экспоната" (часть 7)
ASP.NET MVC: История одного проекта "Обработка ошибок" (часть 8)
ASP.NET MVC: История одного проекта "Фильтрация" (часть 9)
ASP.NET MVC: История одного проекта "Поиск" (часть 10)
ASP.NET MVC: История одного проекта "Облако тегов" (часть 11)
ASP.NET MVC: История одного проекта "Главная страница" (часть 12)

Тема дня

Облако тегов очень удобный и распространный вариант навигации по сайту. Тема этой части - "Облако меток" (или облако тегов, если хотите, или вообще, облако ключевых слов).

Предмет разговора

Экспонаты в музее юмора помечаются метками, а значит эти метки можно представить пользователю в виде облака. Облако буду делать с разбиением на группы (кластеры), чтобы оно наглядно отображало частоту использования меток, отображая часто используемы большим размеров, а малоспользуемые, соответственно, меньшим.

Кластерный анализ

Итак, приступим. У меня на сайте в музее очень большое количество меток. Для того чтобы сформировать "правильное" облако меток, воспользуюсь методом кластерного анализа. Этот алгоритм достаточно прост, и для достижения успешного результата надо:

  1. Задать количиство групп (кластеров)
  2. Определить центр масс для каждой группы. Это обычное число, по которому выполняется разбиение. В моем случае это частота использования тега. Минимальное количество я задал для первой группы, а максимальное количество, соответственно, для последней 10-ой группы.
  3. Определить расстояние от центра массы до тега для каждой метки. Это простое арифметическое вычитание из количества использования метки числа центра масс .
  4. Сгруппировать метки в в группы (кластеры). Требуется найти какой центр масс ближе числу использований метки. В эту группу и добавляем метку.
  5. Пересчитываем значения центров масс. На этом этапе надо определить среднее значение частоты использования всех тегов, которые вошли в группу (кластер) в пункте 4.
  6. Повторяем пункты с 3-го по 5-ый, пока не закончатся метки.

Я определю в своем облаке 10 групп (кластеров). У меня кластеры (группы) будут отображаться разными CSS стилями. Таким образом, у меня будет для каждой из групп задан свой стиль в CSS. Определения стилей показывать не буду, пусть каждый раскрасит метки в зависимости от "частоты" самостоятельно.

Класс TagCloud

Для начала создаю класс, который будет использоваться при формировании облака меток:

/// <summary>
/// Класс тега для облака.
/// </summary>
public class TagCloud {

  #region конструкторы
  public TagCloud() { }

  public TagCloud(string name, string css, int total) {
    this.Name = name;
    this.CssClass = css;
    this.Total = total;
  }

  public TagCloud(int id, string name, string css, int total) {
    this.Id = id;
    this.Name = name;
    this.CssClass = css;
    this.Total = total;
  }
  #endregion

  #region свойства
  /// <summary>
  /// Идентификатор
  /// </summary>
  public int Id { get; set; }

  /// <summary>
  /// Наименование
  /// </summary>
  public string Name { get; set; }

  /// <summary>
  /// Стиль Css
  /// </summary>
  public string CssClass { get; set; }

  /// <summary>
  /// Всего изпользован
  /// </summary>
  public int Total { get; set; }
  #endregion

}

Формирование облака

Надеюсь всё понятно, со свойствами и с конструкторами класса TagCloud. Теперь надо сделать расширение (extension), которое будет метки (tag) превращать в метку для облака (TagCloud):

internal static IEnumerable<TagCloud> TagsToCloudItems(this IEnumerable<Tag> source) {
  if (source == null) throw new ArgumentNullException("source");
  return source.Select(x => new TagCloud(x.Id, x.Name, "tag", x.Exhibits.Count));
}

Следующим этапом требуется создать расширение (extension), которое разобъёт сформированные метки для облака (TagCloud) по группам (в кластеры). Приведу этот код целиком:

/// <summary>
/// Создает облако тегов
/// </summary>
/// <param name="tagsCloud">Массив меток</param>
/// <param name="clusterCount">Количество кластеров при генерации</param>
internal static IEnumerable<TagCloud> CreatorCloud(this IEnumerable<TagCloud> tagsCloud, int ClusterCount) {
  int totalCount = tagsCloud.Count();
  tagsCloud = tagsCloud.OrderBy(ff => ff.Total).ToArray();
  List<List<TagCloud>> clusters = new List<List<TagCloud>>();
  if (totalCount > 0) {
    int min = tagsCloud.Min(c => c.Total);
    int max = tagsCloud.Max(c => c.Total) + min;
    int completeRange = max - min;
    double groupRange = (double)completeRange / (double)(ClusterCount);
    List<TagCloud> cluster = new List<TagCloud>();
    double currentRange = min + groupRange;
    for (int i = 0; i < totalCount; i++) {
      while (tagsCloud.ToArray()[i].Total > currentRange) {
        clusters.Add(cluster);
        cluster = new List<TagCloud>();
        currentRange += groupRange;
      }
      cluster.Add(tagsCloud.ToArray()[i]);
    }
    clusters.Add(cluster);
  }
  TagCloud tc;
  List<TagCloud> result = new List<TagCloud>();
  for (int i = 0; i < clusters.Count; i++) {
    foreach (TagCloud item in clusters[i]) {
      tc = new TagCloud(item.Id, item.Name, "tag" + i.ToString(), item.Total);
      result.Add(tc);
    }
  }
  return result.OrderBy(x => x.Name).AsEnumerable();
}

Можно было бы сделать всё в одном методе: и превращение меток в облачные метки, и сразу же разбросать их по группам. Но я специально разделил на два метода. Первый будет использоваться в WCF-сервисе для Silverlight. Далее в контролере Museum создаем новый ActionResult:

[OutputCache(Duration = 2880)]
public ActionResult Cloud() {
  var model = tagRepository
    .AllIncluding(x => x.Exhibits)
    .TagsToCloudItems()
    .CreateCloud(10);
  return View(model);
}

Просто последовательно вызываю два расширения на коллекцию меток (Tag), и затем отдаю их в представление (View). Обратите внимание на то, что я пометил данный метод аттрибутом OutputCache, который на 48 часов будет кэшировать данные выдаваемые в результате выполнения этого метода. Это сделано в силу того, что частота обновления облака не велика. 

UI облака

Если есть метод Cloud значить должно быть и представление (View). В этом представлении подключаю дополнительный CSS для отображения меток по группам кластера, о котором  я говорил выше. А отрисовку меток делает UserControl (выделено жирным):

@section header{
  <link href="@Url.Content("/content/tags.css")" rel="stylesheet" type="text/css" />
}
@model IEnumerable<Calabonga.Mvc.Humor.Models.TagCloud>     
@{
  ViewBag.Title = "Облоко тегов";
<strong>  Layout = "~/Views/Shared/_LayoutMain.cshtml";
</strong>}
<h2>
  Облако меток музейных экспонатов</h2>
<p><strong>@Html.Partial("Controls/TagCloudControl")</strong></p>

И сам контрол для отрисовки меток:

@model IEnumerable<Calabonga.Mvc.Humor.Models.TagCloud>
@if (Model != null && Model.Count() > 0) {

  foreach (Calabonga.Mvc.Humor.Models.TagCloud item in Model) {
  <span style="margin: 2px; padding: 2px; line-height: 2em; background-color: #f5f5f5;">
    @Html.ActionLink(
      string.Concat(item.Name, " (", @item.Total, ")"), 
      "index", 
      "museum", 
      new { t = item.Name }, 
      new { @class = item.CssClass })
  </span>
  }
}

Запускаем... Хм... Ссылки-то на облако меток нет... Облако меток у меня есть только в музее юмора, а на ленте быть не должно. Значить достаточно разместить ссылку на некоторых страницах музея юмора (но не ленты экспонатов, чтобы не вводить в заблуждение посетителей). Я разместил на странице отображения всех экспонатов музея и на детальном просмотре. Метки кликабельны, а значит получилось то, что и требовалось.

Заключение

Осталось сказать: "спасибо за внимание".

Комментарии к статье (1)

31.08.2012 18:39:34 Николай

Думаю "нынче"  теги должны быть с какими то более магическими штуками :)