Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions app/models/high_charts_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ def global_options
end

def build_annual_dojos(source, lang = 'ja')
data = annual_chart_data_from(source)
data = annual_dojos_chart_data_from(source)
title_text = lang == 'en' ? 'Number of Dojos' : '道場数の推移'

LazyHighCharts::HighChart.new('graph') do |f|
f.title(text: title_text)
f.xAxis(categories: data[:years])
f.series(type: 'column', name: lang == 'en' ? 'New' : '増加数', yAxis: 0, data: data[:increase_nums])
f.series(type: 'column', name: lang == 'en' ? 'New' : '開設数', yAxis: 0, data: data[:increase_nums])
f.series(type: 'line', name: lang == 'en' ? 'Total' : '累積合計', yAxis: 1, data: data[:cumulative_sums])
f.yAxis [
{ title: { text: lang == 'en' ? 'New' : '増加数' }, tickInterval: 15, max: 75 },
{ title: { text: lang == 'en' ? 'New' : '開設数' }, tickInterval: 15, max: 75 },
{ title: { text: lang == 'en' ? 'Total' : '累積合計' }, tickInterval: 50, max: 250, opposite: true }
]
f.chart(width: HIGH_CHARTS_WIDTH, alignTicks: false)
Expand Down Expand Up @@ -71,14 +71,44 @@ def annual_chart_data_from(source)
years = source_array.map(&:first)
counts = source_array.map(&:last)

# 増加数を計算(前年との差分)
increase_nums = counts.each_with_index.map do |count, i|
i == 0 ? count : count - counts[i - 1]
end
# 年間の値として扱う(イベント回数や参加者数用)
increase_nums = counts

# annual_dojos_with_historical_dataからの値は既にその時点での総数
# (累積値として扱う)
cumulative_sums = counts
# 累積合計を計算
cumulative_sums = counts.size.times.map {|i| counts[0..i].sum }

{
years: years,
increase_nums: increase_nums,
cumulative_sums: cumulative_sums
}
end

# 道場数の推移用の特別なデータ処理
# 新規開設数と累積数を表示
def annual_dojos_chart_data_from(source)
# sourceが新しい形式(active_dojosとnew_dojosを含む)の場合
if source.is_a?(Hash) && source.key?(:active_dojos) && source.key?(:new_dojos)
active_array = source[:active_dojos].to_a
new_array = source[:new_dojos].to_a

years = active_array.map(&:first)
cumulative_sums = active_array.map(&:last)
increase_nums = new_array.map(&:last) # 新規開設数を使用
else
# 後方互換性のため、古い形式もサポート
source_array = source.is_a?(Hash) ? source.to_a : source

years = source_array.map(&:first)
counts = source_array.map(&:last)

# 増減数を計算(前年との差分)- 後方互換性のため
increase_nums = counts.each_with_index.map do |count, i|
i == 0 ? count : count - counts[i - 1]
end

cumulative_sums = counts
end

{
years: years,
Expand Down
17 changes: 16 additions & 1 deletion app/models/stat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ def annual_sum_of_participants
def annual_dojos_chart(lang = 'ja')
# 各年末時点でアクティブだったDojoを集計(過去の非アクティブDojoも含む)
# YAMLマスターデータには既にinactivated_atが含まれているため、常にこの方式を使用
HighChartsBuilder.build_annual_dojos(annual_dojos_with_historical_data, lang)
data = {
active_dojos: annual_dojos_with_historical_data,
new_dojos: annual_new_dojos_count
}
HighChartsBuilder.build_annual_dojos(data, lang)
end

# 各年末時点でアクティブだったDojo数を集計(過去の非アクティブDojoも含む)
Expand All @@ -46,6 +50,17 @@ def annual_dojos_with_historical_data
hash[year.to_s] = count
end
end

# 各年に新規開設されたDojo数を集計
def annual_new_dojos_count
(@[email protected]).each_with_object({}) do |year, hash|
start_of_year = Time.zone.local(year).beginning_of_year
end_of_year = Time.zone.local(year).end_of_year
# その年に作成されたDojoの数を集計
count = Dojo.where(created_at: start_of_year..end_of_year).sum(:counter)
hash[year.to_s] = count
end
end

def annual_event_histories_chart(lang = 'ja')
HighChartsBuilder.build_annual_event_histories(annual_count_of_event_histories, lang)
Expand Down
195 changes: 194 additions & 1 deletion spec/models/stat_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,197 @@
expect(result).to be_a(LazyHighCharts::HighChart)
end
end
end

describe 'グラフデータの妥当性検証' do
let(:period) { Date.new(2012, 1, 1)..Date.new(2024, 12, 31) }
let(:stat) { Stat.new(period) }

before do
# テスト用のDojoを作成(複数作成して、一部を非アクティブ化)
dojo1 = Dojo.create!(
name: 'CoderDojo テスト1',
email: '[email protected]',
description: 'テスト用Dojo1の説明',
tags: ['Scratch'],
url: 'https://test1.coderdojo.jp',
created_at: Time.zone.local(2012, 4, 1),
prefecture_id: 13,
is_active: true
)

# 2022年に非アクティブ化される道場
dojo2 = Dojo.create!(
name: 'CoderDojo テスト2',
email: '[email protected]',
description: 'テスト用Dojo2の説明',
tags: ['Python'],
url: 'https://test2.coderdojo.jp',
created_at: Time.zone.local(2019, 1, 1),
prefecture_id: 14,
is_active: false,
inactivated_at: Time.zone.local(2022, 6, 1)
)

# 2023年に非アクティブ化される道場
dojo3 = Dojo.create!(
name: 'CoderDojo テスト3',
email: '[email protected]',
description: 'テスト用Dojo3の説明',
tags: ['Ruby'],
url: 'https://test3.coderdojo.jp',
created_at: Time.zone.local(2020, 1, 1),
prefecture_id: 27,
is_active: false,
inactivated_at: Time.zone.local(2023, 3, 1)
)

# テスト用のイベント履歴を作成(実際のデータパターンに近づける)
# 成長曲線を再現:初期は少なく、徐々に増加、COVID後に減少、その後回復
test_data = {
2012 => { events: 2, participants_per_event: 4 }, # 創成期
2013 => { events: 3, participants_per_event: 4 }, # 徐々に増加
2014 => { events: 5, participants_per_event: 5 },
2015 => { events: 6, participants_per_event: 6 },
2016 => { events: 8, participants_per_event: 6 },
2017 => { events: 12, participants_per_event: 7 }, # 成長期
2018 => { events: 15, participants_per_event: 7 },
2019 => { events: 20, participants_per_event: 8 }, # ピーク
2020 => { events: 16, participants_per_event: 5 }, # COVID影響
2021 => { events: 14, participants_per_event: 4 }, # 低迷継続
2022 => { events: 16, participants_per_event: 5 }, # 回復開始
2023 => { events: 18, participants_per_event: 6 }, # 回復継続
2024 => { events: 18, participants_per_event: 6 } # 安定
}

test_data.each do |year, data|
data[:events].times do |i|
# dojo1のイベント(継続的に活動)
EventHistory.create!(
dojo_id: dojo1.id,
dojo_name: dojo1.name,
service_name: 'connpass',
event_id: "test_#{year}_#{i}",
event_url: "https://test.connpass.com/event/#{year}_#{i}/",
evented_at: Date.new(year, 3, 1) + (i * 2).weeks,
participants: data[:participants_per_event]
)
end
end
end

it '開催回数のグラフに負の値が含まれないこと' do
# テストデータから集計
event_data = stat.annual_count_of_event_histories

# 各年の開催回数が0以上であることを確認
event_data.each do |year, count|
expect(count).to be >= 0, "#{year}年の開催回数が負の値です: #{count}"
end

# 期待される値を確認(明示的なテストデータに基づく)
# annual_count_of_event_historiesは文字列キーを返す
expect(event_data['2012']).to eq(2)
expect(event_data['2013']).to eq(3)
expect(event_data['2014']).to eq(5)
expect(event_data['2015']).to eq(6)
expect(event_data['2016']).to eq(8)
expect(event_data['2017']).to eq(12)
expect(event_data['2018']).to eq(15)
expect(event_data['2019']).to eq(20)
expect(event_data['2020']).to eq(16)
expect(event_data['2021']).to eq(14)
expect(event_data['2022']).to eq(16)
expect(event_data['2023']).to eq(18)
expect(event_data['2024']).to eq(18)

# グラフデータを生成
chart = HighChartsBuilder.build_annual_event_histories(event_data)
series_data = chart.series_data

if series_data
# 年間の開催回数(棒グラフ)が負でないことを確認
annual_counts = series_data.find { |s| s[:type] == 'column' }
if annual_counts && annual_counts[:data]
annual_counts[:data].each_with_index do |count, i|
year = 2012 + i # テストデータの開始年は2012
expect(count).to be >= 0, "#{year}年の開催回数が負の値としてグラフに表示されます: #{count}"
end
end
end
end

it '参加者数のグラフに負の値が含まれないこと' do
# テストデータから集計
participant_data = stat.annual_sum_of_participants

# 各年の参加者数が0以上であることを確認
participant_data.each do |year, count|
expect(count).to be >= 0, "#{year}年の参加者数が負の値です: #{count}"
end

# 期待される値を確認(明示的なテストデータに基づく)
# annual_sum_of_participantsも文字列キーを返す
expect(participant_data['2012']).to eq(8) # 2イベント × 4人
expect(participant_data['2013']).to eq(12) # 3イベント × 4人
expect(participant_data['2014']).to eq(25) # 5イベント × 5人
expect(participant_data['2015']).to eq(36) # 6イベント × 6人
expect(participant_data['2016']).to eq(48) # 8イベント × 6人
expect(participant_data['2017']).to eq(84) # 12イベント × 7人
expect(participant_data['2018']).to eq(105) # 15イベント × 7人
expect(participant_data['2019']).to eq(160) # 20イベント × 8人
expect(participant_data['2020']).to eq(80) # 16イベント × 5人
expect(participant_data['2021']).to eq(56) # 14イベント × 4人
expect(participant_data['2022']).to eq(80) # 16イベント × 5人
expect(participant_data['2023']).to eq(108) # 18イベント × 6人
expect(participant_data['2024']).to eq(108) # 18イベント × 6人

# グラフデータを生成
chart = HighChartsBuilder.build_annual_participants(participant_data)
series_data = chart.series_data

if series_data
# 年間の参加者数(棒グラフ)が負でないことを確認
annual_counts = series_data.find { |s| s[:type] == 'column' }
if annual_counts && annual_counts[:data]
annual_counts[:data].each_with_index do |count, i|
year = 2012 + i # テストデータの開始年は2012
expect(count).to be >= 0, "#{year}年の参加者数が負の値としてグラフに表示されます: #{count}"
end
end
end
end

it '道場数の「開設数」は負の値にならない(新規開設数のため)' do
# 道場数のデータを取得(新しい形式)
dojo_data = {
active_dojos: stat.annual_dojos_with_historical_data,
new_dojos: stat.annual_new_dojos_count
}

# グラフデータを生成
chart = HighChartsBuilder.build_annual_dojos(dojo_data)
series_data = chart.series_data

if series_data
# 開設数(棒グラフ)- 新規開設された道場の数
change_data = series_data.find { |s| s[:type] == 'column' }
if change_data && change_data[:data]
change_data[:data].each_with_index do |value, i|
year = 2012 + i
# 「開設数」は新規開設を意味するため、負の値は論理的に不適切
expect(value).to be >= 0, "#{year}年の「開設数」が負の値です: #{value}。開設数は新規開設された道場数を表すため、0以上である必要があります"
end
end

# 累積数(線グラフ)は常に0以上
total_data = series_data.find { |s| s[:type] == 'line' }
if total_data && total_data[:data]
total_data[:data].each_with_index do |count, i|
year = 2012 + i
expect(count).to be >= 0, "#{year}年の累積道場数が負の値です: #{count}"
end
end
end
end
end
end