ニコニコ大百科:グラフ機能を使用すれば、HTML Canvasが使用できないニコニコ大百科でもお絵カキコに頼らずにグラフが描けます。
コピー&ペーストで手持ちのエディタに貼り付け、.htmlで終わる適当なファイル名をつけて保存して下さい。文字コードはUTF-8推奨。
JavaScript部分はKotlin/JS 1.3.71で開発しており、kotlin.jsのランタイムはjsDelivrに依存しています。1.3.71が提供されなくなる未来が不安な人はダウンロードしておきましょう。
import org.w3c.dom.*
import org.w3c.dom.events.MouseEvent
import kotlin.browser.document
import kotlin.dom.clear
import kotlin.dom.createElement
import kotlin.math.*
/** x座標, y座標, (この点を始点とする線分の)色はオプション */
typealias Point = Triple<Double, Double, String?>
/**
* 初期化.
*
* 開発環境 Kotlin/JS 1.3.71
* @author deadbull
*/
fun main() {
/*/ 補正値の初期値を設定
val calibrationField = document.getElementById("thickness") as HTMLInputElement
calibrationField.valueAsNumber = 1.0//1.042*/
// ボタン動作の設定
val convertButton = document.getElementById("convert") as HTMLButtonElement
convertButton.onclick = ::convert
}
/** ボタン押下時の動作. */
fun convert(@Suppress("UNUSED_PARAMETER") event: MouseEvent): dynamic {
val outputArea = document.getElementById("output") as HTMLTextAreaElement
try {
val data = getData()
val node = draw(data)
// コピペ用HTMLソースの表示。
outputArea.textContent = node.outerHTML
} catch (e: Exception) {
outputArea.textContent = e.message
}
return null
}
/** CSV形式テキスト入力からデータを取得. */
fun getData(): List<Point> {
val csvArea = document.getElementById("csvArea") as HTMLTextAreaElement
return csvArea.value.split("\n").filter { it.isNotEmpty() }.map {
val elements = it.split(",")
val color = if (elements.size >= 3) {
elements[2]
} else {
null
}
Triple(elements[0].toDouble(), elements[1].toDouble(), color)
}
}
/**
* 描画.
*
* @return 描画したdiv要素を返す。
*/
fun draw(data: List<Point>): Element {
/** グラフ全体をまとめるdivコンテナ. */
val parentNode = document.createElement("div")
/** グラフの負の領域. */
val negativeNode = document.createElement("div") {
this as HTMLDivElement
this.style.apply {
border = "solid 1px green"
height = "150px"
// 機種によるが、大百科の画面幅は、PC 720px前後、タブレット510px前後、スマホ350px前後。
width = "500px"
}
}
/** グラフの正の領域. */
val positiveNode = negativeNode.cloneNode(deep = true)
// 表示されるHTMLソースの整形用。
negativeNode.innerHTML = "\n"
// 描画
val thicknessField = document.getElementById("thickness") as HTMLInputElement
//** boxの太さ相当分.*/
val thickness = thicknessField.valueAsNumber
/** [Block]のy座標は前の[Block]の下端が基準になる。*/
var offsetY = thickness
for (index in 0..data.size-2) {
val block = Block(data[index], data[index + 1], offsetY, thickness)
negativeNode.innerHTML += block.toString()
offsetY += block.marginY + thickness
}
parentNode.appendChild(positiveNode)
parentNode.appendChild(negativeNode)
document.getElementById("resultBox")!!.apply {
clear()
appendChild(parentNode)
}
return parentNode
}
/**
* 折れ線グラフの各線分を表すdivブロック.
*
* @property color デフォルトは"red"
* @property thickness 線分の太さ
*/
class Block(x1: Double, y1: Double, x2: Double, y2: Double, offsetY: Double,
private val color: String, private val thickness:Double = 1.0) {
/** 線分の長さ. */
private val length: Double
/**
* 線分の傾きというか回転.
*
* 右回りになっている。
* 大百科は弧度法を受け付けない(以降の要素も巻き添えで無効になる)。
* しかも度数法は整数しか受け付けない。
*/
private val rotate: Int
private val dx = x2 - x1
private val dy = y2 - y1
/**
* x方向のmargin.
*
* Elementに横幅があるので引かなければならない上に、
* 回転に対する補正が必要。
*/
private val marginX: Double
/**
* y方向のmargin.
*
* 本来の位置(直前のboxの下端)が基準になるため、
* 前のmarginの値の影響を受け、蓄積する。
*/
internal val marginY: Double
init {
// 小数点以下の処理で隙間が出来るので長めに設定。
length = ceil(sqrt(dx * dx + dy * dy))
rotate = (atan2(-dy, dx) * 180 / PI).roundToInt()
marginX = (x1 + x2 -length) / 2
// marginYを小数点以下4桁に制限して使用することがoffsetYを通してレイアウトに影響している。
val marginYOriginal = -(y1 + y2) / 2 - offsetY
marginY = ((marginYOriginal * 10_000).roundToLong() / 10_000).toDouble()
}
/**
* セカンダリコンストラクタ.
*
* @param point1 始点. 色が設定されている場合はここの色が用いられる。
* @param point2 終点.
* @param offsetY 配置の基準になるy方向下向きの偏位.
*/
constructor(point1: Point, point2: Point, offsetY: Double, thickness: Double)
: this(point1.first, point1.second, point2.first, point2.second,
offsetY, point1.third ?: "red", thickness)
/**
* div要素のHTMLコード文字列に変換.
*
* 大百科側の仕様で、小数点以下4桁までしか使用できない。
* float: leftも検討したが、高さがずれた時と、端での折り返し動作が読めなかったのでやめた。
* それに左方向に線分を向ける時に支障があると思われる。
*/
override fun toString(): String {
return """
<div style="height: ${thickness}px; width:${length.round4()}px; background-color: $color; transform: rotate(${rotate}deg); margin: ${marginY.round4()}px 0 0 ${marginX.round4()}px;"></div>
""".trimIndent()
}
/**
* 小数点以下4桁までしか使用できないことへの対応.
*
* Kotlin/JSではString.formatが使えない。
*/
private fun Double.round4(): String {
val rounder = 10_000L
//四捨五入
val value = (this * rounder).roundToLong()
if (value > Long.MAX_VALUE.toDouble()) {
throw NumberFormatException("値が大きすぎて精度が確保できません")
}
// 負の数の扱いに注意
val intPart = value / rounder
val decimalPart = (value % rounder).absoluteValue
// 丸め誤差が発生する可能性があるのでDoubleにはせず1xxxxの形にしてから文字列を切り出す。
val decimalString = (decimalPart + rounder).toString().substring(1..4)
return "$intPart.$decimalString"
}
}
<!DOCTYPE html>
<html>
<head>
<!-- meta charset="UTF-8" -->
<meta http-equiv="content-language" content="ja">
<meta name="description" content="HTML Canvasを使用せずに線画を作成。">
<meta name="author" content="deadbull">
<title>ニコニコ大百科用グラフHTML生成</title>
</head>
<body>
<ul>
<li>HTML CanvasもCSS positionも使えないので高さ1のdivボックスを使用しています。</li>
<li>浮動小数点表記(例: 1.0e7)は受け付けないので、小数点以下4桁までを推奨。 </li>
<li>横軸は500ピクセル、縦軸は正負の方向に150ピクセルずつ。原点は左辺の中央になっています。</li>
<li>色を変えたい場合はデータの3列目にオプションでその点を始点とする線分の色を指定できます。</li>
<li>ブラウザで有効でも大百科で使えない色指定があることにご注意下さい。</li>
<li>不正な色指定のチェックはしていません。空文字列でも空白文字を色に指定したと出力されます。</li>
<li>サイズの変更は外枠のdivボックス(y>0とy<0の2つある)の変更と元データの加工で対応して下さい。</li>
<li>線分の太さ変更は可能ですが、隙間が発生しやすいのでおすすめはしません。</li>
<li>不連続グラフは不連続区間の色をtranparentにすれば可能。</li>
<li>2つのグラフの同時表示は1つ目のグラフのデータの後ろに2つ目のグラフのデータをつけて間をtansparentにすれば可能。</li>
<li>凡例や各軸の数値などの表示はサポート対象外です。tableを使うなり、重ね合わせを工夫するなりしてみて下さい。</li>
<li>単位区間を短くすれば疑似曲線も描けますが、ファイルサイズが大きくなる(点100個で13kBくらい)ので、サーバー負荷の原因や自己満足にならないよう注意して下さい。</li>
<li>IE11では正しく表示されません。</li>
<li>本ページ及び生成されたHTMLの使用は自己責任となります。ニコニコ大百科運営<!--: そもそも関与していない -->・ページのソースコード作成者は一切の責任を負いません。</li>
</ul>
<p><label for="csvArea">データ入力欄</label><br>
<textarea id="csvArea" cols="20" rows="10" placeholder="ここにCSV形式(n行2列引用符なし)で貼り付ける"
required="required" autofocus="autofocus"></textarea> <label>線分の太さ <input
id="thickness" required="required" value="1.0" step="0.1" size="20"
type="number"></label><br>
<button id="convert" value="" type="button">変換</button></p>
<p><label for="output">HTMLソース</label><br>
<textarea id="output" cols="70" rows="20" placeholder="ここにHTMLもしくはエラーメッセージが表示されます"
readonly="readonly" wrap="soft"></textarea> </p>
<div id="resultBox"> </div>
<script src="https://cdn.jsdelivr.net/npm/kotlin@1.3.71/kotlin.min.js"></script>
<script>(function (root, factory) {
if (typeof define === 'function' && define.amd)
define(['exports', 'kotlin'], factory);
else if (typeof exports === 'object')
factory(module.exports, require('kotlin'));
else {
if (typeof kotlin === 'undefined') {
throw new Error("Error loading module 'LineGraphHTML'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'LineGraphHTML'.");
}root.LineGraphHTML = factory(typeof LineGraphHTML === 'undefined' ? {} : LineGraphHTML, kotlin);
}
}(this, function (_, Kotlin) {
'use strict';
var throwCCE = Kotlin.throwCCE;
var getCallableRef = Kotlin.getCallableRef;
var Exception = Kotlin.kotlin.Exception;
var split = Kotlin.kotlin.text.split_ip8yn$;
var toDouble = Kotlin.kotlin.text.toDouble_pdl1vz$;
var Triple = Kotlin.kotlin.Triple;
var Unit = Kotlin.kotlin.Unit;
var createElement = Kotlin.kotlin.dom.createElement_7cgwi1$;
var ensureNotNull = Kotlin.ensureNotNull;
var clear = Kotlin.kotlin.dom.clear_asww5s$;
var trimIndent = Kotlin.kotlin.text.trimIndent_pdl1vz$;
var L10000 = Kotlin.Long.fromInt(10000);
var roundToLong = Kotlin.kotlin.math.roundToLong_yrwdxr$;
var Long$Companion$MAX_VALUE = Kotlin.Long.MAX_VALUE;
var NumberFormatException = Kotlin.kotlin.NumberFormatException;
var IntRange = Kotlin.kotlin.ranges.IntRange;
var substring = Kotlin.kotlin.text.substring_fc3b62$;
var math = Kotlin.kotlin.math;
var roundToInt = Kotlin.kotlin.math.roundToInt_yrwdxr$;
var Kind_CLASS = Kotlin.Kind.CLASS;
var ArrayList_init = Kotlin.kotlin.collections.ArrayList_init_287e2$;
var collectionSizeOrDefault = Kotlin.kotlin.collections.collectionSizeOrDefault_ba2ldo$;
var ArrayList_init_0 = Kotlin.kotlin.collections.ArrayList_init_ww73n8$;
var Math_0 = Math;
var abs = Kotlin.kotlin.math.abs_s8cxhz$;
function main() {
var tmp$;
var convertButton = Kotlin.isType(tmp$ = document.getElementById('convert'), HTMLButtonElement) ? tmp$ : throwCCE();
convertButton.onclick = getCallableRef('convert', function (event) {
return convert(event);
});
}
function convert(event) {
var tmp$;
var outputArea = Kotlin.isType(tmp$ = document.getElementById('output'), HTMLTextAreaElement) ? tmp$ : throwCCE();
try {
var data = getData();
var node = draw(data);
outputArea.textContent = node.outerHTML;
} catch (e) {
if (Kotlin.isType(e, Exception)) {
outputArea.textContent = e.message;
} else
throw e;
}
return null;
}
function getData() {
var tmp$;
var csvArea = Kotlin.isType(tmp$ = document.getElementById('csvArea'), HTMLTextAreaElement) ? tmp$ : throwCCE();
var $receiver = split(csvArea.value, ['\n']);
var destination = ArrayList_init();
var tmp$_0;
tmp$_0 = $receiver.iterator();
while (tmp$_0.hasNext()) {
var element = tmp$_0.next();
if (element.length > 0)
destination.add_11rb$(element);
}
var destination_0 = ArrayList_init_0(collectionSizeOrDefault(destination, 10));
var tmp$_1;
tmp$_1 = destination.iterator();
while (tmp$_1.hasNext()) {
var item = tmp$_1.next();
var tmp$_2 = destination_0.add_11rb$;
var tmp$_3;
var elements = split(item, [',']);
if (elements.size >= 3) {
tmp$_3 = elements.get_za3lpa$(2);
} else {
tmp$_3 = null;
}
var color = tmp$_3;
tmp$_2.call(destination_0, new Triple(toDouble(elements.get_za3lpa$(0)), toDouble(elements.get_za3lpa$(1)), color));
}
return destination_0;
}
function draw$lambda($receiver) {
var tmp$;
Kotlin.isType(tmp$ = $receiver, HTMLDivElement) ? tmp$ : throwCCE();
var $receiver_0 = $receiver.style;
$receiver_0.border = 'solid 1px green';
$receiver_0.height = '150px';
$receiver_0.width = '500px';
return Unit;
}
function draw(data) {
var tmp$, tmp$_0;
var parentNode = document.createElement('div');
var negativeNode = createElement(document, 'div', draw$lambda);
var positiveNode = negativeNode.cloneNode(true);
negativeNode.innerHTML = '\n';
var thicknessField = Kotlin.isType(tmp$ = document.getElementById('thickness'), HTMLInputElement) ? tmp$ : throwCCE();
var thickness = thicknessField.valueAsNumber;
var offsetY = thickness;
tmp$_0 = data.size - 2 | 0;
for (var index = 0; index <= tmp$_0; index++) {
var block = Block_init(data.get_za3lpa$(index), data.get_za3lpa$(index + 1 | 0), offsetY, thickness);
negativeNode.innerHTML = negativeNode.innerHTML + block.toString();
offsetY += block.marginY_8be2vx$ + thickness;
}
parentNode.appendChild(positiveNode);
parentNode.appendChild(negativeNode);
var $receiver = ensureNotNull(document.getElementById('resultBox'));
clear($receiver);
$receiver.appendChild(parentNode);
return parentNode;
}
function Block(x1, y1, x2, y2, offsetY, color, thickness) {
if (thickness === void 0)
thickness = 1.0;
this.color_0 = color;
this.thickness_0 = thickness;
this.length_0 = 0;
this.rotate_0 = 0;
this.dx_0 = x2 - x1;
this.dy_0 = y2 - y1;
this.marginX_0 = 0;
this.marginY_8be2vx$ = 0;
var x = this.dx_0 * this.dx_0 + this.dy_0 * this.dy_0;
var x_0 = Math_0.sqrt(x);
this.length_0 = Math_0.ceil(x_0);
var y = -this.dy_0;
var x_1 = this.dx_0;
this.rotate_0 = roundToInt(Math_0.atan2(y, x_1) * 180 / math.PI);
this.marginX_0 = (x1 + x2 - this.length_0) / 2;
var marginYOriginal = -(y1 + y2) / 2 - offsetY;
this.marginY_8be2vx$ = roundToLong(marginYOriginal * 10000).div(Kotlin.Long.fromInt(10000)).toNumber();
}
Block.prototype.toString = function () {
return trimIndent('\n' + ' <div style=' + '"' + 'height: ' + this.thickness_0 + 'px; width:' + this.round4_0(this.length_0) + 'px; background-color: ' + this.color_0 + '; transform: rotate(' + this.rotate_0 + 'deg); margin: ' + this.round4_0(this.marginY_8be2vx$) + 'px 0 0 ' + this.round4_0(this.marginX_0) + 'px;' + '"' + '><\/div>' + '\n' + ' ' + '\n' + ' ');
};
Block.prototype.round4_0 = function ($receiver) {
var rounder = L10000;
var value = roundToLong($receiver * rounder.toNumber());
if (value.toNumber() > Long$Companion$MAX_VALUE.toNumber()) {
throw new NumberFormatException('\u5024\u304C\u5927\u304D\u3059\u304E\u3066\u7CBE\u5EA6\u304C\u78BA\u4FDD\u3067\u304D\u307E\u305B\u3093');
}var intPart = value.div(rounder);
var decimalPart = abs(value.modulo(rounder));
var decimalString = substring(decimalPart.add(rounder).toString(), new IntRange(1, 4));
return intPart.toString() + '.' + decimalString;
};
Block.$metadata$ = {
kind: Kind_CLASS,
simpleName: 'Block',
interfaces: []
};
function Block_init(point1, point2, offsetY, thickness, $this) {
$this = $this || Object.create(Block.prototype);
var tmp$;
Block.call($this, point1.first, point1.second, point2.first, point2.second, offsetY, (tmp$ = point1.third) != null ? tmp$ : 'red', thickness);
return $this;
}
_.main = main;
_.convert_tfvzir$ = convert;
_.getData = getData;
_.draw_r0g5qg$ = draw;
_.Block_init_2fmomo$ = Block_init;
_.Block = Block;
main();
Kotlin.defineModule('LineGraphHTML', _);
return _;
}));
//# sourceMappingURL=LineGraphHTML.js.map
</script>
</body>
</html>
急上昇ワード改
最終更新:2025/12/11(木) 04:00
最終更新:2025/12/11(木) 03:00
ウォッチリストに追加しました!
すでにウォッチリストに
入っています。
追加に失敗しました。
ほめた!
ほめるを取消しました。
ほめるに失敗しました。
ほめるの取消しに失敗しました。