diff --git a/app/models/high_charts_builder.rb b/app/models/high_charts_builder.rb index badcf8eb9..30b2f95b4 100644 --- a/app/models/high_charts_builder.rb +++ b/app/models/high_charts_builder.rb @@ -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) @@ -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, diff --git a/app/models/stat.rb b/app/models/stat.rb index 8c9f16128..02443a8cd 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -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も含む) @@ -46,6 +50,17 @@ def annual_dojos_with_historical_data hash[year.to_s] = count end end + + # 各年に新規開設されたDojo数を集計 + def annual_new_dojos_count + (@period.first.year..@period.last.year).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) diff --git a/spec/models/stat_spec.rb b/spec/models/stat_spec.rb index 525615ed8..eb663d832 100644 --- a/spec/models/stat_spec.rb +++ b/spec/models/stat_spec.rb @@ -80,4 +80,197 @@ expect(result).to be_a(LazyHighCharts::HighChart) end end -end \ No newline at end of file + + 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: 'test1@example.com', + 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: 'test2@example.com', + 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: 'test3@example.com', + 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