TagCloud を JavaScript でパワーアップ

Posted by
ぴろり
Posted at
2005/09/24 17:27
Trackbacks
関連記事 (1)
Comments
コメント (7)
Post Comment
コメントできます
Category
MovableType カテゴリ

 巷では Ajax 云々が大人気です。あの Google Maps も実のところ、Ajax のコアを成す JavaScript を使ってあれだけのことを実現しています。FLASH に頼らずにブラウザだけで完結しているのは本当に驚きですね。JavaScript もまだまだ捨てたものじゃないという優れたお手本だと思います。

 さて、記事分類の新たな切り口として先日、本サイトにも導入してみた TagCloud ですが、JavaScript の実力にビックリしたことを糧に、今回、これを JavaScript を用いてパワーアップしてみました。Google Maps には遠く遠く及びもしませんでしたが、その一部を真似てスライダコントロールモドキを導入し、また、タグのソート順を自由に切り替えられるようにもしてみました。

この記事を Delicious に追加する   このエントリーをはてなブックマークに追加  

TagCloud

 タグによる folksonomy の実現には、 Ogawa::Memorandaで公開されている Tagwire Pluginを使用しています。 また、TagCloud の生成は、 TagwireでTagCloudのコードを 大変に参考にさせて頂きました。 コードの本質部分はほとんど同じになっています。 有用なプラグインを開発・公開されている ogawa さんに感謝を m(_ _)m
 以下に独自の改造を紹介しながら説明していますので、 本サイトの TagCloudを参照されながら読んで頂くと判りやすいかと思います。

フォントサイズの調整

 タグの使用回数からフォントサイズを求める計算式を変更しています。 記事が増えてブログ全体のタグの使用回数が増えてくると、 オリジナルのコードαではフォントサイズが上限なく大きくなります。 改造後のコードβではタグの使用回数の最大値と最小値を基にして、 使用頻度が最も少ないタグのフォントサイズを size_min に、 使用頻度が最も多いタグのフォントサイズを size_max に線形補間しています。 これにより、ブログ全体のタグの使用回数の多寡に関わらず、 ダイナミックに変化する TagCloud を生成することができます。
// オリジナル(α)
function calcFontSize (count) {
	return count / 6 + 12;
}

// 改造後(β)
// min count of tag appearance (cf. to retrieve with MTTags)
var count_min = <MTTags lastn="1" sort_by="count" sort_order="ascend"><$MTTagCount$></MTTags>;
// max count of tag appearance (cf. to retrieve with MTTags)
var count_max = <MTTags lastn="1" sort_by="count" sort_order="descend"><$MTTagCount$></MTTags>;

function calcFontSize (count) {
	var size_min = 12;			// min font size on count_max
	var size_max = 28;			// max font size on count_min
	return (size_max - size_min) * (count - count_min) / (count_max - count_min) + size_min;
}

 カットオフ値による表示・非表示についても、同じ原理で正規化された出現回数を基に判定しています。

今は単純に線形補間としていますが、タグの使用頻度の分布まで考えた方がモア・ベター

ソート順の切り替え

 タグのソート順をボタン一発で変更できるようにしてみました。 今のところ"辞書順","更新日順","使用頻度順"によるソート切替えが可能です。 使用頻度順ソートについては、カットオフ値による表示・非表示切替えがあるので、 効果のほどはイマイチかもしれません。
 切替えボタンの onclick ハンドラで所望の TagCloud を表示し、 それ以外を非表示にしています。単純。
function E (id) { return document.getElementById (id); }

function selectCloud (index) {
	var clouds = ['tags_name', 'tags_date', 'tags_count'];
	for (var i = 0; i < clouds.length; i++)
		E(clouds [i]).style.display = 'none';
	initCloud (clouds [index]);
	E(clouds [index]).style.display = 'block';
}
:
:
<input type="button" onclick="selectCloud (0);" value="辞書順">
<input type="button" onclick="selectCloud (1);" value="更新日">
<input type="button" onclick="selectCloud (2);" value="使用頻度">

更新日でソート

 オリジナルのTagwire Pluginでは、 タグを使用頻度と辞書順でしかソートできません。 今回、更新日によるソートにも対応したかったので、プラグインも少し改造してあります。 tagwire.pltags サブルーチン中に以下の部分を追加します。
my @list;
if ($sort_by eq 'tag' || $sort_by eq 'keyword' ) {
	@list = $sort_order eq 'ascend' ?
	sort { lc $a cmp lc $b } keys %tags :
	sort { lc $b cmp lc $a } keys %tags;
} elsif ($sort_by eq 'tag-case' || $sort_by eq 'keyword-case') {
	@list = $sort_order eq 'ascend' ?
	sort keys %tags :
	sort reverse keys %tags;

;# piroli++ ↓ここから追加
} elsif ($sort_by eq 'date') {
	@list = $sort_order eq 'ascend' ?
	sort { $ts{$a} <=> $ts{$b} } keys %tags :
	sort { $ts{$b} <=> $ts{$a} } keys %tags;
;# ++piroli ↑ここまで追加

} else {
	@list = $sort_order eq 'ascend' ?
	sort { $tags{$a} <=> $tags{$b} } keys %tags :
	sort { $tags{$b} <=> $tags{$a} } keys %tags;
}

 これにより sort_by オプションで、タグが登録された記事の更新日でソートが行えます。 また、他同様 sort_order による昇順/降順指定もそのまま有効です。

スライダコントロール

 タグの使用頻度に応じてタグの表示・非表示を切り替えることができますが、 この部分を Google Maps で使われている左端のズームコントローラを真似て(というか見た目そのまんま) スライダコントロール風にしてみました。

ブラウザによってイベントハンドラの登録方法が異なるようで、 全てのブラウザについて動作の保証が取れていません。 特にスライダ部分の動作が大きくことなるようです (Netscape7.1…動作、FireFox…動作、IE6.0…一部動作、Opera…全くダメ)  うぁ〜…手間だなぁ(´・ω・`)

ソース

 TagCloud ページのインデックスアーカイブテンプレートから抜粋を示します。
<script type="text/javascript">
//	"JavaScript de Sugoi TagCloud" v.0.90
//			Programmed by Piroli YUKARINOMIYA (Open MagicVox)
//			@see http://www.magicvox.net/archive/2005/09241727.php
//			Original concept by ogawa (Ogawa::Memoranda)
//			@see http://as-is.net/blog/archives/001027.html

// Division ticks of slider controller
var division = 10;

// min count of tag appearance (cf. to retrieve with MTTags)
var count_min = <MTTags lastn="1" sort_by="count" sort_order="ascend"><$MTTagCount$></MTTags>;
// max count of tag appearance (cf. to retrieve with MTTags)
var count_max = <MTTags lastn="1" sort_by="count" sort_order="descend"><$MTTagCount$></MTTags>;

////////////////////////////////////////////////////////////////////////
function E (id) { return document.getElementById (id); }

////////////////////////////////////////////////////////////////////////
// Retrieve the font size by linear compensation
// mapping count in [count_min, count_max] -> [size_min, size_max]
function calcFontSize (count) {
	var size_min = 12;			// min font size on count_max
	var size_max = 28;			// max font size on count_min
	return (size_max - size_min) * (count - count_min) / (count_max - count_min) + size_min;
}

// Retrieve the normalized count of tag appearance
// mapping count in [count_min, count_max] -> [1, division]
function calcNormalizeCount (count) {
	return division * (count - count_min) / (count_max - count_min) + 1;
}

// Retrieve style by freshness
function calcRefreshTime (diff) {
	if (diff <=  3) return 'diff1';
	if (diff <= 10) return 'diff2';
	if (90 <= diff) return 'diff4';
	if (30 <= diff) return 'diff3';
	return null;
}

// Initialize the tag cloud named 'name'
var tags = null;
function initCloud (name)
{
	tags = new Array();
	var tagsNode = E(name);
	var childNodes = tagsNode.childNodes;
	var now = (new Date()).getTime();
	for (var i = 0; i < childNodes.length; i++) {
		var e = childNodes.item (i);
		if (e.nodeName.match (/li/i)) {
			var s = e.title.split (':');
			e.style.fontSize = calcFontSize (s[1]) + 'px';
			var d = s[2].split ('-');
			var diff = (now - (new Date(d[0], d[1] - 1, d[2])).getTime()) / 86400000;
			var style = calcRefreshTime (diff);
			if (style)
				e.className = style;
			tags.push ([ e, calcNormalizeCount (s[1]) ]);
		}
	}
}

// Filter tags by coff value
var coff = 0;					// cut-off value (memo. set his initial value here)
function refreshCoff () {
	if (tags)
		for (var i = 0; i < tags.length; i++) {
			var tag = tags[i];
			tag[0].style.visibility = tag[1] <= coff ? 'hidden' : 'visible';
		}
}

////////////////////////////////////////////////////////////////////////
// Slider control like Gooooooogle
function getSliderPos (tick) {
	return tick * 8 + 21;
}

function alignSlider (pos_y) {
	return parseInt ((pos_y - 21) / 8 + 0.5);
}

function limitSlider (pos_y) {
	var limit_min = getSliderPos (0);
	var limit_max = getSliderPos (division);
	return pos_y < limit_min ? limit_min : (limit_max < pos_y ? limit_max : pos_y);
}

function moveSlider (tick) {
	coff = tick < 0 ? 0 : (division < tick ? division : tick);
	E('ctrl-minus').style.marginTop = (division * 8 + 36) + 'px';
	E('ctrl-slider').style.marginTop = getSliderPos (coff)  + 'px';
	refreshCoff ();
}

function initSlider () {
	// Show slider controller
	var parts = '/image/ctrl/';	// directory path of slider images
	document.write ('<div id="controller">');
	document.write ('<div id="shadow"><img src="'+parts+'dslidertopshadow.png"><br />');
	for (var i = 1; i < division; i++)
		document.write ('<img src="'+parts+'dsliderbarshadow.png"><br />');
	document.write ('<img src="'+parts+'dsliderbottomshadow.png"></div>');
	document.write ('<div id="base"><img src="'+parts+'dslidertop.png"><br />');
	for (var i = 1; i < division; i++)
		document.write ('<img src="'+parts+'dsliderbar.png"><br />');
	document.write ('<img src="'+parts+'dsliderbottom.png"></div>');
	document.write ('<img id="ctrl-plus" src="'+parts+'zoom-plus.png">');
	document.write ('<img id="ctrl-minus" src="'+parts+'zoom-minus.png">');
	document.write ('<img id="ctrl-slider" src="'+parts+'slider.png">');
	document.write ('</div>');
	moveSlider (coff);

	// Event handling of inc/dec buttons
	E("ctrl-plus").onclick = function () { moveSlider (coff - 1); }
	E("ctrl-minus").onclick = function () { moveSlider (coff + 1); }

	// Event handling of clicking the slider base
	E("base").onclick = function (e) { moveSlider (alignSlider (e.layerY - 6)); }

	// Event handling of D&D of slider
	// '06/03/04, pirolix, 諦めたorz 
	var grabObj = null;
	var slider = E("ctrl-slider");
	slider.onmousedown = function (e) {}
	slider.onmousemove = function (e) {}
	slider.onmouseup = function (e) {}
}

////////////////////////////////////////////////////////////////////////
function selectCloud (index) {
	// see <ul id="...">s below
	var clouds = ['tags_name', 'tags_date', 'tags_count'];
	for (var i = 0; i < clouds.length; i++)
		E(clouds [i]).style.display = 'none';
	initCloud (clouds [index]);
	E(clouds [index]).style.display = 'block';
	refreshCoff ();

}
</script>



<style type="text/css">
	ul.tags {
		margin: 0px;
		margin-left: 30px;
		padding: 0px;
		display: none; /* hidden at first */
	}
	.tags a {
		text-decoration: none;
	}
	.tags li {
		display: inline;
		padding: 4px;
		word-break: keep-all;
	}
	.tags li.diff1 a { color: #f50; }
	.tags li.diff2 a { color: #900; }
	.tags li a { color: #000; }
	.tags li.diff3 a { color: #77a; }
	.tags li.diff4 a { color: #bbf; }

	#controller { margin-left: 4px; }

	#controller #shadow { position: absolute; }
	#controller #base { position: absolute; cursor: pointer; }
	#controller #ctrl-plus { position: absolute; padding-left: 2px; margin-top: 0px; cursor: pointer; }
	#controller #ctrl-minus { position: absolute; padding-left: 2px; margin-top: 100px; cursor: pointer; }
	#controller #ctrl-slider { position: absolute; padding-left: 1px; margin-top: 0px; cursor: pointer; }
</style>



<p>
タグを
<input type="button" class="ctrl_button" onclick="selectCloud (0);" value="辞書順">
<input type="button" class="ctrl_button" onclick="selectCloud (1);" value="更新日">
<input type="button" class="ctrl_button" onclick="selectCloud (2);" value="使用頻度">
で並べ替える
</p>

<div id="tag_cloud">

<script type="text/javascript">initSlider ();</script>

<ul class="tags" id="tags_name">
<MTTags sort_by="tag" sort_order="ascend">
<li title="<$MTTag$>:<$MTTagCount$>:<$MTTagDate format="%Y-%m-%d"$>"><a href="/tag/<$MTTag encode_url="1"$>" title="<$MTTag$> [<$MTTagCount$>] (<$MTTagDate format="%Y/%m/%d"$>)"><$MTTag$></a></li></MTTags>
</ul>

<ul class="tags" id="tags_date"><MTTags sort_by="date" sort_order="descend">
<li title="<$MTTag$>:<$MTTagCount$>:<$MTTagDate format="%Y-%m-%d"$>"><a href="/tag/<$MTTag encode_url="1"$>" title="<$MTTag$> [<$MTTagCount$>] (<$MTTagDate format="%Y/%m/%d"$>)"><$MTTag$></a></li></MTTags>
</ul>

<ul class="tags" id="tags_count">
<MTTags sort_by="count" sort_order="descend">
<li title="<$MTTag$>:<$MTTagCount$>:<$MTTagDate format="%Y-%m-%d"$>"><a href="/tag/<$MTTag encode_url="1"$>" title="<$MTTag$> [<$MTTagCount$>] (<$MTTagDate format="%Y/%m/%d"$>)"><$MTTag$></a></li></MTTags>
</ul>

<script type="text/javascript">selectCloud (0);</script>

</div><!--tag_cloud-->

JavaScript の後半部分はスライダコントロール関連のコードです。 IEとOperaでは一部(または全部)で不具合が出ているので修正の必要があります ('05/09/26 追記)

この記事を Delicious に追加する   このエントリーをはてなブックマークに追加  


この記事を読んだ人はこんな記事も読んでいます記事リコメンデーションについて

カバー画像:タグクラウドのフォントサイズの計算式について

関連記事/トラックバック (全 1 件中、最新 5 件まで表示しています)

eternalheart.com のスクリーンショット
タイトル
TagCloudのページを作る
Trackbacked at
2006/02/24 22:55
from
eternalheart.com
概要
上のメニューバーの右のほうにTagsっていうリンクがあるんですけど、やっとのこと...

この記事にトラックバックを送るには?

寄せられたコメント (全 7 件中、最新 5 件まで表示しています)

Posted by
のののの
at
2008/10/29 11:36
ID
lIOUQYvw
ありがとうございます。
ファイルを早速ダウンロードしてテストしてみましたが、今度はテンプレートのエラーとなりました。
ただ、エラー表示された内容のようなタグが無いのに表示され…。
いただいたファイルの中身を見てみたのですが、かなり改造されているんですね。
プログラマでは無いしがないWeb屋の私には中々解読が手強そうです(^^;
とりあえず、何とかテンプレートを修正して使用させていただきたいと思います。
Posted by
ぴろり ◆OLEEi.VOX.ぴろり ◆OLEEi.VOX.
at
2008/10/28 22:16
ID
kyD4Lst6
更新日でのタグのソートは,私が勝手にプラグインを改造した結果,実現できるようになった機能でした。
TagwireプラグインはとりあえずMT4.x系でも騙し騙し(?)使えているっぽいので,テンプレートの書き方の問題のような気がします>エラー
現在,このサイトで使っているTagwireプラグインを置いておきますので参考にしてください。
http://www.magicvox.net/cgi-bin/mt/plugins/tagwire/tagwire.pl
Posted by
のののの
at
2008/10/28 18:00
ID
U64huDew
申し訳ございません、大事なことを書き忘れていました。
MTのバージョンは3.2です。
やはりこれだけ古いバージョンだと厳しいんでしょうかね…。
事情によりバージョンUpができないので、何とか対応できたらと考えているのですが。
Posted by
のののの
at
2008/10/28 17:26
ID
U64huDew
はじめまして。
更新日でソート改造を行ってみたのですが、下記のようなエラーが発生してしまい、再構築ができませんでした。
Undefined subroutine &MT::Plugin::Tagwire::entries called at lib/MT/Builder.pm line 159. 
非常に面倒なテンプレートにしてしまっているために、何とか更新日でソートをしたいと考えているのですが、エラーの原因・回避策をご教授いただけないでしょうか…。
Posted by
ぴろり ◆OLEEi.VOX.ぴろり ◆OLEEi.VOX.
at
2008/04/15 08:45
ID
S82uqLMc
ご指摘ありがとうございましたm(_ _)m
説明文はMT標準のタグ機能に倣うように書いているんですが,
タグ機能はOgawa::memorandaのTagwireプラグインを未だに使っているために,
説明と実際が違うことがよくあるんですよ…(直せよ
MTは下位互換性についてはかなり善処している方だと思いますが,
やっぱり未だに移行できない部分というのもあるもんです。

コメントを投稿する

 
 (必須, 匿名可, 公開, トリップが使えます)
 (必須, 匿名可, 非公開, Gravatar に対応しています)
 (必須)
スパム コメント防止のため「投稿確認」欄に ランダムな数字 CAPTCHAについて を入力してから送信してください。お手数ですがご協力のほど宜しくお願いいたします。