broadlistening-viewer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +108 -0
- data/exe/broadlistening-viewer +20 -0
- data/js/shared/blv.css +102 -0
- data/js/shared/chart_manager.js +504 -0
- data/js/shared/colors.js +81 -0
- data/js/shared/decidim_core_shim.js +67 -0
- data/js/shared/fullscreen_modal.js +108 -0
- data/js/shared/i18n.js +37 -0
- data/js/shared/plotly_shim.js +4034 -0
- data/js/shared/scatter_chart.js +250 -0
- data/js/shared/settings_dialog.js +154 -0
- data/js/shared/toolbar.js +102 -0
- data/js/shared/treemap_chart.js +202 -0
- data/js/shared/types.js +0 -0
- data/js/shared/utils.js +43 -0
- data/lib/broadlistening/viewer/assets/app.css +2 -0
- data/lib/broadlistening/viewer/assets/broadlistening-view.js +4201 -0
- data/lib/broadlistening/viewer/assets/i18n/ja.json +46 -0
- data/lib/broadlistening/viewer/assets/shared/_visualization_body.html.erb +33 -0
- data/lib/broadlistening/viewer/assets/template.html.erb +28 -0
- data/lib/broadlistening/viewer/renderer.rb +88 -0
- data/lib/broadlistening/viewer/version.rb +7 -0
- data/lib/broadlistening/viewer.rb +20 -0
- metadata +68 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"no_data": "データがありません。",
|
|
4
|
+
"close": "閉じる",
|
|
5
|
+
"cancel": "キャンセル",
|
|
6
|
+
"apply": "適用",
|
|
7
|
+
"items_count": "%{count}件",
|
|
8
|
+
"opinions_count": "%{count}件の意見",
|
|
9
|
+
"initialization_error": "チャートの初期化に失敗しました。"
|
|
10
|
+
},
|
|
11
|
+
"treemap": {
|
|
12
|
+
"text_template": "%{label}<br>%{value:,}件<br>%{percentEntry:.2%}"
|
|
13
|
+
},
|
|
14
|
+
"toolbar": {
|
|
15
|
+
"all": "全体",
|
|
16
|
+
"all_title": "全体",
|
|
17
|
+
"density": "濃い意見",
|
|
18
|
+
"density_title": "濃い意見",
|
|
19
|
+
"density_disabled_title": "この設定条件では抽出できませんでした",
|
|
20
|
+
"treemap": "ツリー",
|
|
21
|
+
"treemap_title": "ツリーマップ",
|
|
22
|
+
"settings": "設定",
|
|
23
|
+
"settings_title": "表示設定",
|
|
24
|
+
"fullscreen_title": "フルスクリーン"
|
|
25
|
+
},
|
|
26
|
+
"settings": {
|
|
27
|
+
"title": "表示設定",
|
|
28
|
+
"max_density_label": "上位何%の意見グループを表示するか",
|
|
29
|
+
"min_value_label": "意見グループのサンプル数の最小数"
|
|
30
|
+
},
|
|
31
|
+
"breadcrumb": {
|
|
32
|
+
"viewing": "表示中:",
|
|
33
|
+
"all": "全て"
|
|
34
|
+
},
|
|
35
|
+
"cluster": {
|
|
36
|
+
"click_to_expand": "クリックしてサブクラスターを表示",
|
|
37
|
+
"back_to_all": "全てのクラスターに戻る"
|
|
38
|
+
},
|
|
39
|
+
"template": {
|
|
40
|
+
"analyzed_comments": "%{count}件のコメントを分析",
|
|
41
|
+
"overview": "概要",
|
|
42
|
+
"chart_help": "ドラッグで移動、スクロールでズームできます。ツリーマップではクラスタをクリックして詳細を確認できます。",
|
|
43
|
+
"cluster_overview": "クラスター概要",
|
|
44
|
+
"cluster_count": "%{count}件の意見"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div id="broadlistening-view-i18n" hidden data-messages='<%= i18n_json %>'></div>
|
|
2
|
+
|
|
3
|
+
<script id="<%= data_id %>" type="application/json"><%= report_json %></script>
|
|
4
|
+
<div data-broadlistening-view-manager data-data-source="<%= data_id %>" id="<%= container_id %>"></div>
|
|
5
|
+
<p class="text-center text-gray-500 text-sm mt-2">
|
|
6
|
+
<%= i18n_hash.dig("template", "chart_help") %>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<% if clusters.any? -%>
|
|
10
|
+
<section class="mt-8" id="cluster-overview-section">
|
|
11
|
+
<h3 class="mb-4"><%= i18n_hash.dig("template", "cluster_overview") %></h3>
|
|
12
|
+
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-4" id="cluster-grid">
|
|
13
|
+
<% clusters.each_with_index do |cluster, index| -%>
|
|
14
|
+
<div class="card p-4 hover:shadow-md transition-shadow cursor-pointer"
|
|
15
|
+
style="border-left: 4px solid <%= cluster_color.call(index) %>;"
|
|
16
|
+
data-cluster-id="<%= cluster["id"] %>">
|
|
17
|
+
<div class="flex items-center gap-3 mb-2">
|
|
18
|
+
<span class="inline-block w-3 h-3 rounded-full flex-shrink-0" style="background-color: <%= cluster_color.call(index) %>;"></span>
|
|
19
|
+
<span class="font-semibold text-sm line-clamp-2"><%= cluster["label"] %></span>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="flex items-center gap-2 mb-2">
|
|
22
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium" style="background-color: <%= cluster_color.call(index) %>20; color: <%= cluster_color.call(index) %>;">
|
|
23
|
+
<%= (i18n_hash.dig("template", "cluster_count") || "%{count}件の意見").gsub("%{count}", (cluster["value"] || 0).to_s) %>
|
|
24
|
+
</span>
|
|
25
|
+
</div>
|
|
26
|
+
<% if cluster["takeaway"] && !cluster["takeaway"].empty? -%>
|
|
27
|
+
<p class="text-gray-2 text-sm line-clamp-3"><%= cluster["takeaway"] %></p>
|
|
28
|
+
<% end -%>
|
|
29
|
+
</div>
|
|
30
|
+
<% end -%>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
<% end -%>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%= title %></title>
|
|
7
|
+
<style><%= css %></style>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="max-w-[1200px] mx-auto p-4">
|
|
11
|
+
<div class="text-center">
|
|
12
|
+
<h1><%= title %></h1>
|
|
13
|
+
<p class="text-gray-500"><%= i18n_hash.dig("template", "analyzed_comments")&.gsub("%{count}", comment_count.to_s) || "#{comment_count}件のコメントを分析" %></p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<% if overview && !overview.empty? %>
|
|
17
|
+
<section class="bg-gray-50 p-6 rounded-lg mb-8">
|
|
18
|
+
<h3 class="mt-0 mb-4"><%= i18n_hash.dig("template", "overview") || "概要" %></h3>
|
|
19
|
+
<div><%= overview_html %></div>
|
|
20
|
+
</section>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<%= visualization_body_html %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<script><%= js %></script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "erb"
|
|
5
|
+
|
|
6
|
+
module Broadlistening
|
|
7
|
+
module Viewer
|
|
8
|
+
class Renderer
|
|
9
|
+
CLUSTER_COLORS = %w[#7ac943 #3fa9f5 #ff7997 #ffcc5c #845ec2 #00c9a7 #ff6f61 #6c5ce7 #fdcb6e #74b9ff].freeze
|
|
10
|
+
|
|
11
|
+
def self.assets_path
|
|
12
|
+
File.expand_path("assets", __dir__)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(title: "分析結果")
|
|
16
|
+
@title = title
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render(json_str)
|
|
20
|
+
data = JSON.parse(json_str)
|
|
21
|
+
title = @title
|
|
22
|
+
css = load_asset("app.css")
|
|
23
|
+
js = load_asset("broadlistening-view.js")
|
|
24
|
+
report_json = data.to_json
|
|
25
|
+
i18n_json = load_asset("i18n/ja.json")
|
|
26
|
+
i18n_hash = JSON.parse(i18n_json)
|
|
27
|
+
overview = data["overview"] || ""
|
|
28
|
+
overview_html = overview.split("\n").reject(&:empty?).map { |p| "<p>#{p}</p>" }.join("\n")
|
|
29
|
+
comment_count = data["comment_num"] || data.dig("config", "comment_num") || 0
|
|
30
|
+
clusters = (data["clusters"] || [])
|
|
31
|
+
.select { |c| c["level"] == 1 }
|
|
32
|
+
.sort_by { |c| -(c["value"] || 0) }
|
|
33
|
+
cluster_color = ->(index) { CLUSTER_COLORS[index % CLUSTER_COLORS.length] }
|
|
34
|
+
|
|
35
|
+
# Render visualization body partial
|
|
36
|
+
data_id = "report-data"
|
|
37
|
+
container_id = "chart-container"
|
|
38
|
+
visualization_body_str = load_asset("shared/_visualization_body.html.erb")
|
|
39
|
+
visualization_body_html = ERB.new(visualization_body_str, trim_mode: "-").result(binding)
|
|
40
|
+
|
|
41
|
+
# Render main template
|
|
42
|
+
template_str = load_asset("template.html.erb")
|
|
43
|
+
ERB.new(template_str, trim_mode: "-").result(binding)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def save(json_str, output_path)
|
|
47
|
+
html = render(json_str)
|
|
48
|
+
File.write(output_path, html)
|
|
49
|
+
output_path
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def load_asset(name)
|
|
55
|
+
File.read(File.join(self.class.assets_path, name))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Keep the top-level function for backward compatibility (used by ruby.wasm site mode)
|
|
62
|
+
def render_html(json_str, template_str, css_str, js_str, i18n_str, page_title = "分析結果", visualization_body_erb_str = nil)
|
|
63
|
+
data = JSON.parse(json_str)
|
|
64
|
+
title = page_title
|
|
65
|
+
css = css_str
|
|
66
|
+
js = js_str
|
|
67
|
+
report_json = data.to_json
|
|
68
|
+
i18n_json = i18n_str
|
|
69
|
+
i18n_hash = JSON.parse(i18n_str)
|
|
70
|
+
overview = data["overview"] || ""
|
|
71
|
+
overview_html = overview.split("\n").reject(&:empty?).map { |p| "<p>#{p}</p>" }.join("\n")
|
|
72
|
+
comment_count = data["comment_num"] || data.dig("config", "comment_num") || 0
|
|
73
|
+
clusters = (data["clusters"] || [])
|
|
74
|
+
.select { |c| c["level"] == 1 }
|
|
75
|
+
.sort_by { |c| -(c["value"] || 0) }
|
|
76
|
+
cluster_colors = Broadlistening::Viewer::Renderer::CLUSTER_COLORS
|
|
77
|
+
cluster_color = ->(index) { cluster_colors[index % cluster_colors.length] }
|
|
78
|
+
|
|
79
|
+
# Render visualization body partial
|
|
80
|
+
data_id = "report-data"
|
|
81
|
+
container_id = "chart-container"
|
|
82
|
+
visualization_body_str = visualization_body_erb_str || File.read(
|
|
83
|
+
File.join(File.expand_path("assets/shared", File.dirname(__FILE__)), "_visualization_body.html.erb")
|
|
84
|
+
)
|
|
85
|
+
visualization_body_html = ERB.new(visualization_body_str, trim_mode: "-").result(binding)
|
|
86
|
+
|
|
87
|
+
ERB.new(template_str, trim_mode: "-").result(binding)
|
|
88
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "viewer/version"
|
|
4
|
+
require_relative "viewer/renderer"
|
|
5
|
+
|
|
6
|
+
module Broadlistening
|
|
7
|
+
module Viewer
|
|
8
|
+
def self.root
|
|
9
|
+
File.expand_path("../..", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.js_path
|
|
13
|
+
File.join(root, "js")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.assets_path
|
|
17
|
+
File.join(root, "lib", "broadlistening", "viewer", "assets")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: broadlistening-viewer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- takahashim
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-02-11 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Generates interactive HTML visualizations from Broadlistening JSON output
|
|
13
|
+
email:
|
|
14
|
+
- takahashimm@gmail.com
|
|
15
|
+
executables:
|
|
16
|
+
- broadlistening-viewer
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- exe/broadlistening-viewer
|
|
23
|
+
- js/shared/blv.css
|
|
24
|
+
- js/shared/chart_manager.js
|
|
25
|
+
- js/shared/colors.js
|
|
26
|
+
- js/shared/decidim_core_shim.js
|
|
27
|
+
- js/shared/fullscreen_modal.js
|
|
28
|
+
- js/shared/i18n.js
|
|
29
|
+
- js/shared/plotly_shim.js
|
|
30
|
+
- js/shared/scatter_chart.js
|
|
31
|
+
- js/shared/settings_dialog.js
|
|
32
|
+
- js/shared/toolbar.js
|
|
33
|
+
- js/shared/treemap_chart.js
|
|
34
|
+
- js/shared/types.js
|
|
35
|
+
- js/shared/utils.js
|
|
36
|
+
- lib/broadlistening/viewer.rb
|
|
37
|
+
- lib/broadlistening/viewer/assets/app.css
|
|
38
|
+
- lib/broadlistening/viewer/assets/broadlistening-view.js
|
|
39
|
+
- lib/broadlistening/viewer/assets/i18n/ja.json
|
|
40
|
+
- lib/broadlistening/viewer/assets/shared/_visualization_body.html.erb
|
|
41
|
+
- lib/broadlistening/viewer/assets/template.html.erb
|
|
42
|
+
- lib/broadlistening/viewer/renderer.rb
|
|
43
|
+
- lib/broadlistening/viewer/version.rb
|
|
44
|
+
homepage: https://github.com/takahashim/broadlistening-ruby-viewer
|
|
45
|
+
licenses:
|
|
46
|
+
- AGPL-3.0-only
|
|
47
|
+
metadata:
|
|
48
|
+
homepage_uri: https://github.com/takahashim/broadlistening-ruby-viewer
|
|
49
|
+
source_code_uri: https://github.com/takahashim/broadlistening-ruby-viewer
|
|
50
|
+
rubygems_mfa_required: 'true'
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: 3.2.0
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements: []
|
|
65
|
+
rubygems_version: 3.6.2
|
|
66
|
+
specification_version: 4
|
|
67
|
+
summary: Standalone viewer for Broadlistening analysis results
|
|
68
|
+
test_files: []
|