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.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Broadlistening
4
+ module Viewer
5
+ VERSION = "0.2.0"
6
+ end
7
+ 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: []