diff --git a/css/style.css b/css/style.css index 712656f..50b795d 100644 --- a/css/style.css +++ b/css/style.css @@ -123,6 +123,9 @@ button { .content { min-width: 0; } .tabs { + position: sticky; + top: 20px; + z-index: 5; display: flex; flex-wrap: wrap; gap: 6px; @@ -142,7 +145,7 @@ button { background: var(--surface); border-radius: 10px; box-shadow: 0 1px 3px rgba(0, 0, 0, .06); - scroll-margin-top: 20px; + scroll-margin-top: 84px; } .card-bar { @@ -198,14 +201,39 @@ button { } @media (max-width: 880px) { - .app-layout { display: block; padding: 16px 12px 40px; } - .sidebar { position: static; margin-bottom: 12px; } - .company-sidebar { padding: 7px; } + .app-layout { display: block; padding: 0 12px 40px; } + .sidebar { margin-bottom: 12px; } + .company-sidebar { + position: sticky; + top: 0; + z-index: 7; + margin: 0 -12px; + padding: 7px 12px; + border-width: 0 0 1px; + border-radius: 0; + background: rgba(245, 244, 241, .96); + backdrop-filter: blur(10px); + } .company-sidebar .sidebar-title { display: none; } .company-list { display: flex; overflow-x: auto; } .company-button { flex: 0 0 auto; width: auto; } - .tabs { overflow-x: auto; flex-wrap: nowrap; } + .tabs { + top: 50px; + z-index: 6; + overflow-x: auto; + overflow-y: hidden; + flex-wrap: nowrap; + margin: 0 -12px 18px; + padding: 5px 12px; + border-width: 0 0 1px; + border-radius: 0; + background: rgba(245, 244, 241, .96); + backdrop-filter: blur(10px); + scrollbar-width: none; + } + .tabs::-webkit-scrollbar { display: none; } .tab-button { flex: 0 0 auto; } + .card { scroll-margin-top: 120px; } .card-bar { align-items: flex-start; flex-wrap: wrap; } .export-button { margin-left: auto; } .chart { min-width: 560px; } diff --git a/data/ppmt/ppmt_assets.csv b/data/ppmt/ppmt_assets.csv index b8458dc..7773d95 100644 --- a/data/ppmt/ppmt_assets.csv +++ b/data/ppmt/ppmt_assets.csv @@ -1,13 +1,13 @@ -period,total_assets,cash_like,receivables,investment,operating_assets -2020H1,17.89,8.22,0.41,0.46,8.8 -2020H2,69.71,56.8,0.78,0.67,11.45 -2021H1,76.41,57.67,1.35,1.78,15.6 -2021H2,83.24,52.65,1.71,4.11,24.77 -2022H1,85.9,51.63,1.32,5.53,27.42 -2022H2,85.8,50.42,1.94,5.55,27.89 -2023H1,90.65,55.4,2.09,5.75,27.42 -2023H2,99.69,59.63,3.21,5.87,30.97 -2024H1,110.17,70.1,2.64,5.5,31.93 -2024H2,148.71,96.2,4.78,5.6,42.13 -2025H1,213.57,137.66,9.72,5.4,60.79 -2025H2,321.01,172.25,9.21,4.95,132.04 +period,total_assets,cash_like,receivables,investment,operating_assets +2020H1,17.89,8.22,0.41,0.46,8.80 +2020H2,69.71,56.80,0.78,0.67,11.45 +2021H1,76.41,57.67,1.35,1.78,15.60 +2021H2,83.24,52.65,1.71,4.11,24.77 +2022H1,85.90,51.63,1.32,5.53,27.42 +2022H2,85.80,50.42,1.94,5.55,27.89 +2023H1,90.65,55.40,2.09,5.75,27.42 +2023H2,99.69,59.63,3.21,5.87,30.97 +2024H1,110.17,70.10,2.64,5.50,31.93 +2024H2,148.71,96.20,4.78,5.60,42.13 +2025H1,213.57,137.66,9.72,5.40,60.79 +2025H2,321.01,172.25,9.21,4.95,132.04 diff --git a/data/ppmt/ppmt_assets_annual.csv b/data/ppmt/ppmt_assets_annual.csv new file mode 100644 index 0000000..18a170c --- /dev/null +++ b/data/ppmt/ppmt_assets_annual.csv @@ -0,0 +1,7 @@ +period,total_assets,cash_like,receivables,investment,operating_assets +2020FY,69.71,56.80,0.78,0.67,11.45 +2021FY,83.24,52.65,1.71,4.11,24.77 +2022FY,85.80,50.42,1.94,5.55,27.89 +2023FY,99.69,59.63,3.21,5.87,30.97 +2024FY,148.71,96.20,4.78,5.60,42.13 +2025FY,321.01,172.25,9.21,4.95,132.04 diff --git a/data/ppmt/ppmt_assets_halfyear.csv b/data/ppmt/ppmt_assets_halfyear.csv new file mode 100644 index 0000000..7773d95 --- /dev/null +++ b/data/ppmt/ppmt_assets_halfyear.csv @@ -0,0 +1,13 @@ +period,total_assets,cash_like,receivables,investment,operating_assets +2020H1,17.89,8.22,0.41,0.46,8.80 +2020H2,69.71,56.80,0.78,0.67,11.45 +2021H1,76.41,57.67,1.35,1.78,15.60 +2021H2,83.24,52.65,1.71,4.11,24.77 +2022H1,85.90,51.63,1.32,5.53,27.42 +2022H2,85.80,50.42,1.94,5.55,27.89 +2023H1,90.65,55.40,2.09,5.75,27.42 +2023H2,99.69,59.63,3.21,5.87,30.97 +2024H1,110.17,70.10,2.64,5.50,31.93 +2024H2,148.71,96.20,4.78,5.60,42.13 +2025H1,213.57,137.66,9.72,5.40,60.79 +2025H2,321.01,172.25,9.21,4.95,132.04 diff --git a/data/ppmt/ppmt_cash_conversion.csv b/data/ppmt/ppmt_cash_conversion.csv index 2bb056f..06df66d 100644 --- a/data/ppmt/ppmt_cash_conversion.csv +++ b/data/ppmt/ppmt_cash_conversion.csv @@ -1,7 +1,19 @@ period,operating_cash_flow,net_profit,cash_conversion +2020H1,1.22,1.41,86.5 +2020H2,5.81,3.82,152.1 2020FY,7.03,5.23,134.4 +2021H1,4.42,3.59,123.1 +2021H2,3.37,4.96,68.1 2021FY,7.79,8.55,91.2 +2022H1,3.74,3.33,112.2 +2022H2,5.17,1.43,362.2 2022FY,8.91,4.76,187.3 +2023H1,10.92,4.77,228.9 +2023H2,8.98,6.12,146.9 2023FY,19.91,10.89,182.8 +2024H1,19.05,9.64,197.6 +2024H2,30.49,23.44,130.1 2024FY,49.54,33.08,149.7 +2025H1,59.77,46.82,127.7 +2025H2,48.88,83.30,58.7 2025FY,108.65,130.12,83.5 diff --git a/data/ppmt/ppmt_cash_conversion_annual.csv b/data/ppmt/ppmt_cash_conversion_annual.csv new file mode 100644 index 0000000..2bb056f --- /dev/null +++ b/data/ppmt/ppmt_cash_conversion_annual.csv @@ -0,0 +1,7 @@ +period,operating_cash_flow,net_profit,cash_conversion +2020FY,7.03,5.23,134.4 +2021FY,7.79,8.55,91.2 +2022FY,8.91,4.76,187.3 +2023FY,19.91,10.89,182.8 +2024FY,49.54,33.08,149.7 +2025FY,108.65,130.12,83.5 diff --git a/data/ppmt/ppmt_cash_conversion_halfyear.csv b/data/ppmt/ppmt_cash_conversion_halfyear.csv new file mode 100644 index 0000000..9376886 --- /dev/null +++ b/data/ppmt/ppmt_cash_conversion_halfyear.csv @@ -0,0 +1,13 @@ +period,operating_cash_flow,net_profit,cash_conversion +2020H1,1.22,1.41,86.5 +2020H2,5.81,3.82,152.1 +2021H1,4.42,3.59,123.1 +2021H2,3.37,4.96,68.1 +2022H1,3.74,3.33,112.2 +2022H2,5.17,1.43,362.2 +2023H1,10.92,4.77,228.9 +2023H2,8.98,6.12,146.9 +2024H1,19.05,9.64,197.6 +2024H2,30.49,23.44,130.1 +2025H1,59.77,46.82,127.7 +2025H2,48.88,83.30,58.7 diff --git a/data/ppmt/ppmt_cashflow_composition.csv b/data/ppmt/ppmt_cashflow_composition.csv index bff9a00..e02e529 100644 --- a/data/ppmt/ppmt_cashflow_composition.csv +++ b/data/ppmt/ppmt_cashflow_composition.csv @@ -1,7 +1,19 @@ period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase +2020H1,1.22,-0.48,4.24,4.98 +2020H2,5.81,-1.69,44.47,48.59 2020FY,7.03,-2.17,48.70,53.57 +2021H1,4.42,-44.98,-1.21,-41.78 +2021H2,3.37,38.50,-3.15,38.73 2021FY,7.79,-6.48,-4.36,-3.05 +2022H1,3.74,-42.10,-4.81,-43.17 +2022H2,5.17,-4.89,-6.97,-6.68 2022FY,8.91,-46.98,-11.77,-49.85 +2023H1,10.92,0.82,-5.58,6.16 +2023H2,8.98,1.52,-2.83,7.67 2023FY,19.91,2.34,-8.42,13.83 +2024H1,19.05,3.34,-7.16,15.23 +2024H2,30.49,-3.25,-2.43,24.82 2024FY,49.54,0.09,-9.59,40.05 +2025H1,59.77,12.34,-14.05,58.07 +2025H2,48.88,-22.23,-3.62,23.02 2025FY,108.65,-9.89,-17.67,81.09 diff --git a/data/ppmt/ppmt_cashflow_composition_annual.csv b/data/ppmt/ppmt_cashflow_composition_annual.csv new file mode 100644 index 0000000..bff9a00 --- /dev/null +++ b/data/ppmt/ppmt_cashflow_composition_annual.csv @@ -0,0 +1,7 @@ +period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase +2020FY,7.03,-2.17,48.70,53.57 +2021FY,7.79,-6.48,-4.36,-3.05 +2022FY,8.91,-46.98,-11.77,-49.85 +2023FY,19.91,2.34,-8.42,13.83 +2024FY,49.54,0.09,-9.59,40.05 +2025FY,108.65,-9.89,-17.67,81.09 diff --git a/data/ppmt/ppmt_cashflow_composition_halfyear.csv b/data/ppmt/ppmt_cashflow_composition_halfyear.csv new file mode 100644 index 0000000..23f652c --- /dev/null +++ b/data/ppmt/ppmt_cashflow_composition_halfyear.csv @@ -0,0 +1,13 @@ +period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase +2020H1,1.22,-0.48,4.24,4.98 +2020H2,5.81,-1.69,44.47,48.59 +2021H1,4.42,-44.98,-1.21,-41.78 +2021H2,3.37,38.50,-3.15,38.73 +2022H1,3.74,-42.10,-4.81,-43.17 +2022H2,5.17,-4.89,-6.97,-6.68 +2023H1,10.92,0.82,-5.58,6.16 +2023H2,8.98,1.52,-2.83,7.67 +2024H1,19.05,3.34,-7.16,15.23 +2024H2,30.49,-3.25,-2.43,24.82 +2025H1,59.77,12.34,-14.05,58.07 +2025H2,48.88,-22.23,-3.62,23.02 diff --git a/data/ppmt/ppmt_category.csv b/data/ppmt/ppmt_category.csv index d40bca1..7506402 100644 --- a/data/ppmt/ppmt_category.csv +++ b/data/ppmt/ppmt_category.csv @@ -1,6 +1,7 @@ -period,plush,figure,mega,other -品类,毛绒,手办,MEGA,衍生品及其他 -2024H1,4.46105,26.56781,5.8613,8.68815 -2024H2,23.85973,42.79306,10.9765,7.16989 -2025H1,61.39236,51.75875,10.07147,15.54018 -2025H2,125.68838,68.46694,9.08935,29.19309 +period,plush,figure,mega,other +2024H1,4.46,26.57,5.86,8.69 +2024H2,23.86,42.79,10.98,7.17 +2024FY,28.32,69.36,16.84,15.86 +2025H1,61.39,51.76,10.07,15.54 +2025H2,125.69,68.47,9.09,29.19 +2025FY,187.08,120.23,19.16,44.73 diff --git a/data/ppmt/ppmt_category_annual.csv b/data/ppmt/ppmt_category_annual.csv new file mode 100644 index 0000000..4bcd928 --- /dev/null +++ b/data/ppmt/ppmt_category_annual.csv @@ -0,0 +1,3 @@ +period,plush,figure,mega,other +2024FY,28.32,69.36,16.84,15.86 +2025FY,187.08,120.23,19.16,44.73 diff --git a/data/ppmt/ppmt_category_halfyear.csv b/data/ppmt/ppmt_category_halfyear.csv new file mode 100644 index 0000000..d26efe8 --- /dev/null +++ b/data/ppmt/ppmt_category_halfyear.csv @@ -0,0 +1,5 @@ +period,plush,figure,mega,other +2024H1,4.46,26.57,5.86,8.69 +2024H2,23.86,42.79,10.98,7.17 +2025H1,61.39,51.76,10.07,15.54 +2025H2,125.69,68.47,9.09,29.19 diff --git a/data/ppmt/ppmt_channel_revenue.csv b/data/ppmt/ppmt_channel_revenue.csv index 5352701..b8ab580 100644 --- a/data/ppmt/ppmt_channel_revenue.csv +++ b/data/ppmt/ppmt_channel_revenue.csv @@ -1,14 +1,20 @@ period,retail,online,robot_store,wholesale,total_revenue,revenue_yoy 2020H1,3.13,3.34,1.05,0.64,8.18,50.5 +2020H2,6.89,6.18,2.23,1.44,16.96,48.7 2020FY,10.02,9.52,3.29,2.07,25.13,49.3 2021H1,6.75,6.78,2.28,1.91,17.73,116.8 +2021H2,9.98,12.01,2.42,2.53,27.18,60.3 2021FY,16.73,18.79,4.70,4.45,44.91,78.7 2022H1,9.26,10.13,2.08,2.12,23.59,33.1 +2022H2,9.00,9.07,1.86,2.65,22.59,-16.9 2022FY,18.27,19.20,3.94,4.77,46.17,2.8 2023H1,13.44,8.74,2.96,2.99,28.14,19.3 +2023H2,17.18,9.92,3.10,4.68,34.87,54.4 2023FY,30.62,18.66,6.06,7.67,63.01,36.5 2024H1,23.65,13.43,3.68,4.82,45.58,62.0 +2024H2,44.01,28.10,4.63,8.06,84.80,143.2 2024FY,67.66,41.53,8.31,12.88,130.38,106.9 2025H1,69.08,54.98,8.21,6.50,138.76,204.4 +2025H2,103.46,109.36,8.69,10.93,232.44,174.1 2025FY,172.54,164.34,16.89,17.42,371.20,184.7 2026H1,91.12,62.12,9.51,8.98,171.73,23.8 diff --git a/data/ppmt/ppmt_channel_revenue_annual.csv b/data/ppmt/ppmt_channel_revenue_annual.csv new file mode 100644 index 0000000..fd3323d --- /dev/null +++ b/data/ppmt/ppmt_channel_revenue_annual.csv @@ -0,0 +1,7 @@ +period,retail,online,robot_store,wholesale,total_revenue,revenue_yoy +2020FY,10.02,9.52,3.29,2.07,25.13,49.3 +2021FY,16.73,18.79,4.70,4.45,44.91,78.7 +2022FY,18.27,19.20,3.94,4.77,46.17,2.8 +2023FY,30.62,18.66,6.06,7.67,63.01,36.5 +2024FY,67.66,41.53,8.31,12.88,130.38,106.9 +2025FY,172.54,164.34,16.89,17.42,371.20,184.7 diff --git a/data/ppmt/ppmt_channel_revenue_halfyear.csv b/data/ppmt/ppmt_channel_revenue_halfyear.csv new file mode 100644 index 0000000..980702c --- /dev/null +++ b/data/ppmt/ppmt_channel_revenue_halfyear.csv @@ -0,0 +1,14 @@ +period,retail,online,robot_store,wholesale,total_revenue,revenue_yoy +2020H1,3.13,3.34,1.05,0.64,8.18,50.5 +2020H2,6.89,6.18,2.23,1.44,16.96,48.7 +2021H1,6.75,6.78,2.28,1.91,17.73,116.8 +2021H2,9.98,12.01,2.42,2.53,27.18,60.3 +2022H1,9.26,10.13,2.08,2.12,23.59,33.1 +2022H2,9.00,9.07,1.86,2.65,22.59,-16.9 +2023H1,13.44,8.74,2.96,2.99,28.14,19.3 +2023H2,17.18,9.92,3.10,4.68,34.87,54.4 +2024H1,23.65,13.43,3.68,4.82,45.58,62.0 +2024H2,44.01,28.10,4.63,8.06,84.80,143.2 +2025H1,69.08,54.98,8.21,6.50,138.76,204.4 +2025H2,103.46,109.36,8.69,10.93,232.44,174.1 +2026H1,91.12,62.12,9.51,8.98,171.73,23.8 diff --git a/data/ppmt/ppmt_core_margin.csv b/data/ppmt/ppmt_core_margin.csv index e80d704..59587b7 100644 --- a/data/ppmt/ppmt_core_margin.csv +++ b/data/ppmt/ppmt_core_margin.csv @@ -1,14 +1,20 @@ -period,core_margin -2020H1,22.6 -2020H2,27.2 -2021H1,25.8 -2021H2,24.4 -2022H1,15.0 -2022H2,10.8 -2023H1,17.4 -2023H2,18.3 -2024H1,24.8 -2024H2,31.5 -2025H1,41.8 -2025H2,45.6 -2026H1,41.4 +period,core_margin +2020H1,22.6 +2020H2,29.4 +2020FY,27.2 +2021H1,25.8 +2021H2,23.5 +2021FY,24.4 +2022H1,15.0 +2022H2,6.3 +2022FY,10.8 +2023H1,17.4 +2023H2,19.0 +2023FY,18.3 +2024H1,24.8 +2024H2,35.1 +2024FY,31.5 +2025H1,41.8 +2025H2,47.8 +2025FY,45.6 +2026H1,41.4 diff --git a/data/ppmt/ppmt_core_margin_annual.csv b/data/ppmt/ppmt_core_margin_annual.csv new file mode 100644 index 0000000..eb400f6 --- /dev/null +++ b/data/ppmt/ppmt_core_margin_annual.csv @@ -0,0 +1,7 @@ +period,core_margin +2020FY,27.2 +2021FY,24.4 +2022FY,10.8 +2023FY,18.3 +2024FY,31.5 +2025FY,45.6 diff --git a/data/ppmt/ppmt_core_margin_halfyear.csv b/data/ppmt/ppmt_core_margin_halfyear.csv new file mode 100644 index 0000000..0b3e5ef --- /dev/null +++ b/data/ppmt/ppmt_core_margin_halfyear.csv @@ -0,0 +1,14 @@ +period,core_margin +2020H1,22.6 +2020H2,29.4 +2021H1,25.8 +2021H2,23.5 +2022H1,15.0 +2022H2,6.3 +2023H1,17.4 +2023H2,19.0 +2024H1,24.8 +2024H2,35.1 +2025H1,41.8 +2025H2,47.8 +2026H1,41.4 diff --git a/data/ppmt/ppmt_core_profit.csv b/data/ppmt/ppmt_core_profit.csv index f3214f0..2c24544 100644 --- a/data/ppmt/ppmt_core_profit.csv +++ b/data/ppmt/ppmt_core_profit.csv @@ -1,14 +1,20 @@ -period,core_profit,yoy -2020H1,1.9,25.0 -2020H2,6.8,17.1 -2021H1,4.6,147.0 -2021H2,11.0,60.1 -2022H1,3.5,-22.5 -2022H2,5.0,-54.5 -2023H1,4.9,38.1 -2023H2,11.5,131.3 -2024H1,11.3,131.5 -2024H2,41.1,256.9 -2025H1,58.0,412.2 -2025H2,169.1,311.4 -2026H1,71.1,22.6 +period,core_profit,yoy +2020H1,1.85,25.2 +2020H2,4.99,14.4 +2020FY,6.84,17.1 +2021H1,4.58,147.3 +2021H2,6.37,27.7 +2021FY,10.95,60.1 +2022H1,3.54,-22.5 +2022H2,1.43,-77.6 +2022FY,4.98,-54.6 +2023H1,4.89,37.9 +2023H2,6.63,363.8 +2023FY,11.52,131.6 +2024H1,11.31,131.5 +2024H2,29.79,349.1 +2024FY,41.10,256.8 +2025H1,57.98,412.4 +2025H2,111.14,273.1 +2025FY,169.12,311.5 +2026H1,71.08,22.6 diff --git a/data/ppmt/ppmt_core_profit_annual.csv b/data/ppmt/ppmt_core_profit_annual.csv new file mode 100644 index 0000000..b453115 --- /dev/null +++ b/data/ppmt/ppmt_core_profit_annual.csv @@ -0,0 +1,7 @@ +period,core_profit,yoy +2020FY,6.84,17.1 +2021FY,10.95,60.1 +2022FY,4.98,-54.6 +2023FY,11.52,131.6 +2024FY,41.10,256.8 +2025FY,169.12,311.5 diff --git a/data/ppmt/ppmt_core_profit_halfyear.csv b/data/ppmt/ppmt_core_profit_halfyear.csv new file mode 100644 index 0000000..7d1e3d1 --- /dev/null +++ b/data/ppmt/ppmt_core_profit_halfyear.csv @@ -0,0 +1,14 @@ +period,core_profit,yoy +2020H1,1.85,25.2 +2020H2,4.99,14.4 +2021H1,4.58,147.3 +2021H2,6.37,27.7 +2022H1,3.54,-22.5 +2022H2,1.43,-77.6 +2023H1,4.89,37.9 +2023H2,6.63,363.8 +2024H1,11.31,131.5 +2024H2,29.79,349.1 +2025H1,57.98,412.4 +2025H2,111.14,273.1 +2026H1,71.08,22.6 diff --git a/data/ppmt/ppmt_expense_structure.csv b/data/ppmt/ppmt_expense_structure.csv index fa1684a..3128ea3 100644 --- a/data/ppmt/ppmt_expense_structure.csv +++ b/data/ppmt/ppmt_expense_structure.csv @@ -1,7 +1,20 @@ period,sales_expense,admin_expense,financial_expense,four_expense_rate +2020H1,2.23,1.25,0.04,43.1 +2020H2,4.07,1.55,0.05,33.4 2020FY,6.30,2.80,0.09,36.6 +2021H1,4.20,2.40,-0.06,36.9 +2021H2,6.86,3.18,-0.02,36.9 2021FY,11.06,5.58,-0.07,36.9 +2022H1,6.93,3.23,-0.07,42.8 +2022H2,7.78,3.64,-0.22,49.5 2022FY,14.71,6.86,-0.29,46.1 +2023H1,8.78,3.31,-0.63,40.7 +2023H2,11.26,3.76,-0.88,40.5 2023FY,20.05,7.07,-1.52,40.6 +2024H1,13.53,4.34,-0.84,37.4 +2024H2,22.97,5.13,-0.80,32.2 2024FY,36.50,9.47,-1.63,34.0 +2025H1,31.93,7.70,-0.66,28.1 +2025H2,48.90,10.00,-0.11,25.3 2025FY,80.82,17.70,-0.76,26.3 +2026H1,39.31,9.27,-0.11,28.2 diff --git a/data/ppmt/ppmt_expense_structure_annual.csv b/data/ppmt/ppmt_expense_structure_annual.csv new file mode 100644 index 0000000..fa1684a --- /dev/null +++ b/data/ppmt/ppmt_expense_structure_annual.csv @@ -0,0 +1,7 @@ +period,sales_expense,admin_expense,financial_expense,four_expense_rate +2020FY,6.30,2.80,0.09,36.6 +2021FY,11.06,5.58,-0.07,36.9 +2022FY,14.71,6.86,-0.29,46.1 +2023FY,20.05,7.07,-1.52,40.6 +2024FY,36.50,9.47,-1.63,34.0 +2025FY,80.82,17.70,-0.76,26.3 diff --git a/data/ppmt/ppmt_expense_structure_halfyear.csv b/data/ppmt/ppmt_expense_structure_halfyear.csv new file mode 100644 index 0000000..74e3ff0 --- /dev/null +++ b/data/ppmt/ppmt_expense_structure_halfyear.csv @@ -0,0 +1,14 @@ +period,sales_expense,admin_expense,financial_expense,four_expense_rate +2020H1,2.23,1.25,0.04,43.1 +2020H2,4.07,1.55,0.05,33.4 +2021H1,4.20,2.40,-0.06,36.9 +2021H2,6.86,3.18,-0.02,36.9 +2022H1,6.93,3.23,-0.07,42.8 +2022H2,7.78,3.64,-0.22,49.5 +2023H1,8.78,3.31,-0.63,40.7 +2023H2,11.26,3.76,-0.88,40.5 +2024H1,13.53,4.34,-0.84,37.4 +2024H2,22.97,5.13,-0.80,32.2 +2025H1,31.93,7.70,-0.66,28.1 +2025H2,48.90,10.00,-0.11,25.3 +2026H1,39.31,9.27,-0.11,28.2 diff --git a/data/ppmt/ppmt_growth.csv b/data/ppmt/ppmt_growth.csv index 9111c91..c50cf6b 100644 --- a/data/ppmt/ppmt_growth.csv +++ b/data/ppmt/ppmt_growth.csv @@ -1,14 +1,20 @@ -period,non_gaap_net_profit,gross_profit,revenue,cost -2020H1,23.7,61.0,50.6,33.3 -2020H2,16.2,46.2,49.3,55.0 -2021H1,154.6,109.6,116.7,131.0 -2021H2,69.5,73.1,78.7,88.5 -2022H1,-13.6,22.6,33.1,50.6 -2022H2,-42.7,-3.8,2.8,13.3 -2023H1,42.3,23.9,19.3,12.9 -2023H2,107.5,45.5,36.5,24.1 -2024H1,90.3,71.9,62.0,47.0 -2024H2,185.7,125.4,106.9,77.7 -2025H1,362.7,234.4,204.4,151.1 -2025H2,284.5,207.4,184.7,139.1 -2026H1,9.5,22.6,23.8,26.5 +period,non_gaap_net_profit,gross_profit,revenue,cost +2020H1,24.4,61.4,50.5,33.6 +2020H2,33.1,39.6,48.7,67.0 +2020FY,30.9,46.2,49.3,55.0 +2021H1,208.0,109.4,116.8,130.5 +2021H2,26.1,54.8,60.3,69.5 +2021FY,69.6,73.1,78.7,88.4 +2022H1,-13.5,22.7,33.1,50.8 +2022H2,-65.2,-21.8,-16.9,-9.5 +2022FY,-42.7,-3.8,2.8,13.3 +2023H1,42.3,23.9,19.3,12.9 +2023H2,232.1,68.7,54.4,35.6 +2023FY,107.6,45.6,36.5,24.2 +2024H1,90.1,71.9,62.0,46.9 +2024H2,264.1,167.3,143.2,103.7 +2024FY,185.9,125.4,106.9,77.7 +2025H1,362.8,234.4,204.4,151.1 +2025H2,251.0,193.7,174.1,131.9 +2025FY,284.5,207.4,184.7,139.1 +2026H1,9.5,22.6,23.8,26.5 diff --git a/data/ppmt/ppmt_growth_annual.csv b/data/ppmt/ppmt_growth_annual.csv new file mode 100644 index 0000000..e281415 --- /dev/null +++ b/data/ppmt/ppmt_growth_annual.csv @@ -0,0 +1,7 @@ +period,non_gaap_net_profit,gross_profit,revenue,cost +2020FY,30.9,46.2,49.3,55.0 +2021FY,69.6,73.1,78.7,88.4 +2022FY,-42.7,-3.8,2.8,13.3 +2023FY,107.6,45.6,36.5,24.2 +2024FY,185.9,125.4,106.9,77.7 +2025FY,284.5,207.4,184.7,139.1 diff --git a/data/ppmt/ppmt_growth_halfyear.csv b/data/ppmt/ppmt_growth_halfyear.csv new file mode 100644 index 0000000..993b3eb --- /dev/null +++ b/data/ppmt/ppmt_growth_halfyear.csv @@ -0,0 +1,14 @@ +period,non_gaap_net_profit,gross_profit,revenue,cost +2020H1,24.4,61.4,50.5,33.6 +2020H2,33.1,39.6,48.7,67.0 +2021H1,208.0,109.4,116.8,130.5 +2021H2,26.1,54.8,60.3,69.5 +2022H1,-13.5,22.7,33.1,50.8 +2022H2,-65.2,-21.8,-16.9,-9.5 +2023H1,42.3,23.9,19.3,12.9 +2023H2,232.1,68.7,54.4,35.6 +2024H1,90.1,71.9,62.0,46.9 +2024H2,264.1,167.3,143.2,103.7 +2025H1,362.8,234.4,204.4,151.1 +2025H2,251.0,193.7,174.1,131.9 +2026H1,9.5,22.6,23.8,26.5 diff --git a/data/ppmt/ppmt_ip.csv b/data/ppmt/ppmt_ip.csv index 34e1879..c671eaa 100644 --- a/data/ppmt/ppmt_ip.csv +++ b/data/ppmt/ppmt_ip.csv @@ -1,9 +1,9 @@ -ip,2021H1,2021H2,2022H1,2022H2,2023H1,2023H2,2024H1,2024H2,2025H1,2025H2 -THE MONSTERS,1.45789,1.58642,1.34887,1.28115,1.59829,2.08042,6.26806,24.13858,48.14017,93.47047 -MOLLY,2.039,5.01198,4.04347,3.97808,4.11433,6.08872,7.8216,13.11012,13.57248,15.39836 -SKULLPANDA,1.83042,4.12214,4.61766,3.89845,5.26383,4.98405,5.74562,7.337,12.20504,23.19038 -CRYBABY,0,0,0,0,0.26603,0.44546,3.49382,8.15503,12.1817,17.10836 -DIMOO,2.04993,3.61742,2.98442,2.79506,3.62032,3.75634,3.77838,5.31723,11.05135,16.7156 -HIRONO-小野,0,0,0.37538,1.0323,1.09352,2.41849,2.45247,4.81278,7.2842,10.06815 -HACIPUPU,0,0,0,0.42797,0.69171,0.82587,0.94991,1.29297,3.32122,5.15899 -星星人,0,0,0,0,0,0,0,0,3.89363,16.66217 +ip,2021H1,2021H2,2021FY,2022H1,2022H2,2022FY,2023H1,2023H2,2023FY,2024H1,2024H2,2024FY,2025H1,2025H2,2025FY +THE MONSTERS,1.45789,1.58642,3.04431,1.34887,1.28115,2.63002,1.59829,2.08042,3.67871,6.26806,24.13858,30.406640000000003,48.14017,93.47047,141.61064 +MOLLY,2.039,5.01198,7.050980000000001,4.04347,3.97808,8.02155,4.11433,6.08872,10.203050000000001,7.8216,13.11012,20.93172,13.57248,15.39836,28.970840000000003 +SKULLPANDA,1.83042,4.12214,5.95256,4.61766,3.89845,8.51611,5.26383,4.98405,10.247879999999999,5.74562,7.337,13.082619999999999,12.20504,23.19038,35.39542 +CRYBABY,0,0,0,0,0,0,0.26603,0.44546,0.71149,3.49382,8.15503,11.64885,12.1817,17.10836,29.29006 +DIMOO,2.04993,3.61742,5.66735,2.98442,2.79506,5.7794799999999995,3.62032,3.75634,7.376659999999999,3.77838,5.31723,9.09561,11.05135,16.7156,27.766949999999998 +HIRONO-小野,0,0,0,0.37538,1.0323,1.40768,1.09352,2.41849,3.51201,2.45247,4.81278,7.26525,7.2842,10.06815,17.35235 +HACIPUPU,0,0,0,0,0.42797,0.42797,0.69171,0.82587,1.5175800000000002,0.94991,1.29297,2.24288,3.32122,5.15899,8.48021 +星星人,0,0,0,0,0,0,0,0,0,0,0,0,3.89363,16.66217,20.555799999999998 diff --git a/data/ppmt/ppmt_ip_annual.csv b/data/ppmt/ppmt_ip_annual.csv new file mode 100644 index 0000000..a8a53e1 --- /dev/null +++ b/data/ppmt/ppmt_ip_annual.csv @@ -0,0 +1,9 @@ +ip,2021FY,2022FY,2023FY,2024FY,2025FY +THE MONSTERS,3.04431,2.63002,3.67871,30.406640000000003,141.61064 +MOLLY,7.050980000000001,8.02155,10.203050000000001,20.93172,28.970840000000003 +SKULLPANDA,5.95256,8.51611,10.247879999999999,13.082619999999999,35.39542 +CRYBABY,0,0,0.71149,11.64885,29.29006 +DIMOO,5.66735,5.7794799999999995,7.376659999999999,9.09561,27.766949999999998 +HIRONO-小野,0,1.40768,3.51201,7.26525,17.35235 +HACIPUPU,0,0.42797,1.5175800000000002,2.24288,8.48021 +星星人,0,0,0,0,20.555799999999998 diff --git a/data/ppmt/ppmt_ip_ex_monsters.csv b/data/ppmt/ppmt_ip_ex_monsters.csv index ebb4619..bc28f90 100644 --- a/data/ppmt/ppmt_ip_ex_monsters.csv +++ b/data/ppmt/ppmt_ip_ex_monsters.csv @@ -1,12 +1,17 @@ period,revenue,yoy 2021H1,16.27, 2021H2,25.59, +2021FY,41.86, 2022H1,22.24,36.7 2022H2,21.30,-16.8 +2022FY,43.54,4.0 2023H1,26.54,19.3 2023H2,32.79,53.9 +2023FY,59.33,36.3 2024H1,39.31,48.1 2024H2,60.66,85.0 +2024FY,99.97,68.5 2025H1,90.62,130.5 2025H2,138.97,129.1 +2025FY,229.59,129.7 2026H1,127.19,40.4 diff --git a/data/ppmt/ppmt_ip_ex_monsters_annual.csv b/data/ppmt/ppmt_ip_ex_monsters_annual.csv new file mode 100644 index 0000000..31b5a0d --- /dev/null +++ b/data/ppmt/ppmt_ip_ex_monsters_annual.csv @@ -0,0 +1,6 @@ +period,revenue,yoy +2021FY,41.86, +2022FY,43.54,4.0 +2023FY,59.33,36.3 +2024FY,99.97,68.5 +2025FY,229.59,129.7 diff --git a/data/ppmt/ppmt_ip_ex_monsters_halfyear.csv b/data/ppmt/ppmt_ip_ex_monsters_halfyear.csv new file mode 100644 index 0000000..ebb4619 --- /dev/null +++ b/data/ppmt/ppmt_ip_ex_monsters_halfyear.csv @@ -0,0 +1,12 @@ +period,revenue,yoy +2021H1,16.27, +2021H2,25.59, +2022H1,22.24,36.7 +2022H2,21.30,-16.8 +2023H1,26.54,19.3 +2023H2,32.79,53.9 +2024H1,39.31,48.1 +2024H2,60.66,85.0 +2025H1,90.62,130.5 +2025H2,138.97,129.1 +2026H1,127.19,40.4 diff --git a/data/ppmt/ppmt_ip_halfyear.csv b/data/ppmt/ppmt_ip_halfyear.csv new file mode 100644 index 0000000..84835dc --- /dev/null +++ b/data/ppmt/ppmt_ip_halfyear.csv @@ -0,0 +1,9 @@ +ip,2021H1,2021H2,2022H1,2022H2,2023H1,2023H2,2024H1,2024H2,2025H1,2025H2 +THE MONSTERS,1.45789,1.58642,1.34887,1.28115,1.59829,2.08042,6.26806,24.13858,48.14017,93.47047 +MOLLY,2.039,5.01198,4.04347,3.97808,4.11433,6.08872,7.8216,13.11012,13.57248,15.39836 +SKULLPANDA,1.83042,4.12214,4.61766,3.89845,5.26383,4.98405,5.74562,7.337,12.20504,23.19038 +CRYBABY,0,0,0,0,0.26603,0.44546,3.49382,8.15503,12.1817,17.10836 +DIMOO,2.04993,3.61742,2.98442,2.79506,3.62032,3.75634,3.77838,5.31723,11.05135,16.7156 +HIRONO-小野,0,0,0.37538,1.0323,1.09352,2.41849,2.45247,4.81278,7.2842,10.06815 +HACIPUPU,0,0,0,0.42797,0.69171,0.82587,0.94991,1.29297,3.32122,5.15899 +星星人,0,0,0,0,0,0,0,0,3.89363,16.66217 diff --git a/data/ppmt/ppmt_ip_share_history.csv b/data/ppmt/ppmt_ip_share_history.csv index 9a4dad6..3c4d4dc 100644 --- a/data/ppmt/ppmt_ip_share_history.csv +++ b/data/ppmt/ppmt_ip_share_history.csv @@ -1,12 +1,12 @@ -ip,2021H1,2021H2,2022H1,2022H2,2023H1,2023H2,2024H1,2024H2,2025H1,2025H2,2026H1 -THE MONSTERS,1.46,1.59,1.35,1.28,1.60,2.08,6.27,24.14,48.14,93.47,44.54 -星星人,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,3.89,16.66,26.50 -授权IP(非独家),2.88,3.33,3.01,2.50,4.22,6.14,6.95,9.06,15.25,18.57,17.71 -CRYBABY,0.00,0.00,0.00,0.00,0.27,0.45,3.49,8.16,12.18,17.11,16.33 -DIMOO,2.05,3.62,2.98,2.80,3.62,3.76,3.78,5.32,11.05,16.72,16.19 -SKULLPANDA,1.83,4.12,4.62,3.90,5.26,4.98,5.75,7.34,12.21,23.19,15.51 -其他艺术家IP,1.43,2.47,4.37,3.00,4.54,5.44,6.37,7.72,10.64,14.00,11.74 -HIRONO-小野,0.00,0.00,0.38,1.03,1.09,2.42,2.45,4.81,7.28,10.07,10.09 -MOLLY,2.04,5.01,4.04,3.98,4.11,6.09,7.82,13.11,13.57,15.40,9.01 -其他IP(含已停单列),4.14,4.51,0.81,1.86,1.15,1.37,0.95,3.74,3.32,5.16,2.98 -外采及其他,1.90,2.53,2.03,2.24,2.28,2.15,1.75,1.41,1.23,2.09,1.12 +ip,2021H1,2021H2,2021FY,2022H1,2022H2,2022FY,2023H1,2023H2,2023FY,2024H1,2024H2,2024FY,2025H1,2025H2,2025FY,2026H1 +THE MONSTERS,1.46,1.59,3.05,1.35,1.28,2.63,1.6,2.08,3.68,6.27,24.14,30.41,48.14,93.47,141.61,44.54 +星星人,0,0,0,0,0,0,0,0,0,0,0,0,3.89,16.66,20.55,26.5 +授权IP(非独家),2.88,3.33,6.21,3.01,2.5,5.51,4.22,6.14,10.36,6.95,9.06,16.01,15.25,18.57,33.82,17.71 +CRYBABY,0,0,0,0,0,0,0.27,0.45,0.72,3.49,8.16,11.65,12.18,17.11,29.29,16.33 +DIMOO,2.05,3.62,5.67,2.98,2.8,5.779999999999999,3.62,3.76,7.38,3.78,5.32,9.1,11.05,16.72,27.77,16.19 +SKULLPANDA,1.83,4.12,5.95,4.62,3.9,8.52,5.26,4.98,10.24,5.75,7.34,13.09,12.21,23.19,35.400000000000006,15.51 +其他艺术家IP,1.43,2.47,3.9000000000000004,4.37,3,7.37,4.54,5.44,9.98,6.37,7.72,14.09,10.64,14,24.64,11.74 +HIRONO-小野,0,0,0,0.38,1.03,1.4100000000000001,1.09,2.42,3.51,2.45,4.81,7.26,7.28,10.07,17.35,10.09 +MOLLY,2.04,5.01,7.05,4.04,3.98,8.02,4.11,6.09,10.2,7.82,13.11,20.93,13.57,15.4,28.97,9.01 +其他IP(含已停单列),4.14,4.51,8.649999999999999,0.81,1.86,2.67,1.15,1.37,2.52,0.95,3.74,4.69,3.32,5.16,8.48,2.98 +外采及其他,1.9,2.53,4.43,2.03,2.24,4.27,2.28,2.15,4.43,1.75,1.41,3.16,1.23,2.09,3.32,1.12 diff --git a/data/ppmt/ppmt_ip_share_history_annual.csv b/data/ppmt/ppmt_ip_share_history_annual.csv new file mode 100644 index 0000000..7101311 --- /dev/null +++ b/data/ppmt/ppmt_ip_share_history_annual.csv @@ -0,0 +1,12 @@ +ip,2021FY,2022FY,2023FY,2024FY,2025FY +THE MONSTERS,3.05,2.63,3.68,30.41,141.61 +星星人,0,0,0,0,20.55 +授权IP(非独家),6.21,5.51,10.36,16.01,33.82 +CRYBABY,0,0,0.72,11.65,29.29 +DIMOO,5.67,5.779999999999999,7.38,9.1,27.77 +SKULLPANDA,5.95,8.52,10.24,13.09,35.400000000000006 +其他艺术家IP,3.9000000000000004,7.37,9.98,14.09,24.64 +HIRONO-小野,0,1.4100000000000001,3.51,7.26,17.35 +MOLLY,7.05,8.02,10.2,20.93,28.97 +其他IP(含已停单列),8.649999999999999,2.67,2.52,4.69,8.48 +外采及其他,4.43,4.27,4.43,3.16,3.32 diff --git a/data/ppmt/ppmt_ip_share_history_halfyear.csv b/data/ppmt/ppmt_ip_share_history_halfyear.csv new file mode 100644 index 0000000..c630948 --- /dev/null +++ b/data/ppmt/ppmt_ip_share_history_halfyear.csv @@ -0,0 +1,12 @@ +ip,2021H1,2021H2,2022H1,2022H2,2023H1,2023H2,2024H1,2024H2,2025H1,2025H2,2026H1 +THE MONSTERS,1.46,1.59,1.35,1.28,1.6,2.08,6.27,24.14,48.14,93.47,44.54 +星星人,0,0,0,0,0,0,0,0,3.89,16.66,26.5 +授权IP(非独家),2.88,3.33,3.01,2.5,4.22,6.14,6.95,9.06,15.25,18.57,17.71 +CRYBABY,0,0,0,0,0.27,0.45,3.49,8.16,12.18,17.11,16.33 +DIMOO,2.05,3.62,2.98,2.8,3.62,3.76,3.78,5.32,11.05,16.72,16.19 +SKULLPANDA,1.83,4.12,4.62,3.9,5.26,4.98,5.75,7.34,12.21,23.19,15.51 +其他艺术家IP,1.43,2.47,4.37,3,4.54,5.44,6.37,7.72,10.64,14,11.74 +HIRONO-小野,0,0,0.38,1.03,1.09,2.42,2.45,4.81,7.28,10.07,10.09 +MOLLY,2.04,5.01,4.04,3.98,4.11,6.09,7.82,13.11,13.57,15.4,9.01 +其他IP(含已停单列),4.14,4.51,0.81,1.86,1.15,1.37,0.95,3.74,3.32,5.16,2.98 +外采及其他,1.9,2.53,2.03,2.24,2.28,2.15,1.75,1.41,1.23,2.09,1.12 diff --git a/data/ppmt/ppmt_liquidity_working_capital.csv b/data/ppmt/ppmt_liquidity_working_capital.csv index 2e98396..ff98b77 100644 --- a/data/ppmt/ppmt_liquidity_working_capital.csv +++ b/data/ppmt/ppmt_liquidity_working_capital.csv @@ -1,14 +1,14 @@ period,cash,total_liabilities,net_working_capital 2020H1,8.22,5.47,8.66 -2020FY,56.80,8.40,55.91 +2020H2,56.80,8.40,55.91 2021H1,57.67,13.26,55.92 -2021FY,52.65,15.04,56.65 +2021H2,52.65,15.04,56.65 2022H1,51.63,14.76,58.16 -2022FY,50.42,16.15,54.85 +2022H2,50.42,16.15,54.85 2023H1,55.40,17.59,57.33 -2023FY,59.63,21.88,59.50 +2023H2,59.63,21.88,59.50 2024H1,70.10,25.65,66.47 -2024FY,96.20,39.86,88.66 +2024H2,96.20,39.86,88.66 2025H1,137.66,69.27,119.27 -2025FY,172.25,94.49,177.46 +2025H2,172.25,94.49,177.46 2026H1,139.62,77.40,174.20 diff --git a/data/ppmt/ppmt_liquidity_working_capital_annual.csv b/data/ppmt/ppmt_liquidity_working_capital_annual.csv new file mode 100644 index 0000000..c3344dc --- /dev/null +++ b/data/ppmt/ppmt_liquidity_working_capital_annual.csv @@ -0,0 +1,7 @@ +period,cash,total_liabilities,net_working_capital +2020FY,56.80,8.40,55.91 +2021FY,52.65,15.04,56.65 +2022FY,50.42,16.15,54.85 +2023FY,59.63,21.88,59.50 +2024FY,96.20,39.86,88.66 +2025FY,172.25,94.49,177.46 diff --git a/data/ppmt/ppmt_liquidity_working_capital_halfyear.csv b/data/ppmt/ppmt_liquidity_working_capital_halfyear.csv new file mode 100644 index 0000000..ff98b77 --- /dev/null +++ b/data/ppmt/ppmt_liquidity_working_capital_halfyear.csv @@ -0,0 +1,14 @@ +period,cash,total_liabilities,net_working_capital +2020H1,8.22,5.47,8.66 +2020H2,56.80,8.40,55.91 +2021H1,57.67,13.26,55.92 +2021H2,52.65,15.04,56.65 +2022H1,51.63,14.76,58.16 +2022H2,50.42,16.15,54.85 +2023H1,55.40,17.59,57.33 +2023H2,59.63,21.88,59.50 +2024H1,70.10,25.65,66.47 +2024H2,96.20,39.86,88.66 +2025H1,137.66,69.27,119.27 +2025H2,172.25,94.49,177.46 +2026H1,139.62,77.40,174.20 diff --git a/data/ppmt/ppmt_margins.csv b/data/ppmt/ppmt_margins.csv index 704e364..5909755 100644 --- a/data/ppmt/ppmt_margins.csv +++ b/data/ppmt/ppmt_margins.csv @@ -1,14 +1,20 @@ -period,gross_margin,sales_expense_rate,admin_expense_rate,non_gaap_net_margin -2020H1,65.23,27.27,15.33,17.24 -2020H2,63.42,25.07,11.14,23.52 -2021H1,63.02,23.68,13.52,24.53 -2021H2,61.43,24.63,12.41,22.31 -2022H1,58.1,29.39,13.68,15.94 -2022H2,57.49,31.85,14.86,12.43 -2023H1,60.36,31.21,11.77,19.01 -2023H2,61.32,31.82,11.23,18.9 -2024H1,64.05,29.69,9.53,22.33 -2024H2,66.79,28.0,7.26,26.1 -2025H1,70.34,23.01,5.55,33.94 -2025H2,72.1,21.77,4.77,35.25 -2026H1,69.68,22.89,5.4,30.02 +period,gross_margin,sales_expense_rate,admin_expense_rate,non_gaap_net_margin +2020H1,65.23,27.27,15.33,17.28 +2020H2,62.55,24.00,9.12,26.49 +2020FY,63.42,25.07,11.14,23.49 +2021H1,63.02,23.68,13.52,24.55 +2021H2,60.39,25.25,11.69,20.84 +2021FY,61.43,24.63,12.41,22.30 +2022H1,58.10,29.39,13.68,15.95 +2022H2,56.86,34.43,16.10,8.73 +2022FY,57.49,31.85,14.86,12.42 +2023H1,60.36,31.21,11.77,19.03 +2023H2,62.10,32.30,10.78,18.79 +2023FY,61.32,31.82,11.23,18.89 +2024H1,64.05,29.69,9.53,22.33 +2024H2,68.26,27.09,6.05,28.13 +2024FY,66.79,28.00,7.26,26.10 +2025H1,70.34,23.01,5.55,33.94 +2025H2,73.15,21.04,4.30,36.03 +2025FY,72.10,21.77,4.77,35.25 +2026H1,69.68,22.89,5.40,30.02 diff --git a/data/ppmt/ppmt_margins_annual.csv b/data/ppmt/ppmt_margins_annual.csv new file mode 100644 index 0000000..50cc4a8 --- /dev/null +++ b/data/ppmt/ppmt_margins_annual.csv @@ -0,0 +1,7 @@ +period,gross_margin,sales_expense_rate,admin_expense_rate,non_gaap_net_margin +2020FY,63.42,25.07,11.14,23.49 +2021FY,61.43,24.63,12.41,22.30 +2022FY,57.49,31.85,14.86,12.42 +2023FY,61.32,31.82,11.23,18.89 +2024FY,66.79,28.00,7.26,26.10 +2025FY,72.10,21.77,4.77,35.25 diff --git a/data/ppmt/ppmt_margins_halfyear.csv b/data/ppmt/ppmt_margins_halfyear.csv new file mode 100644 index 0000000..51480ca --- /dev/null +++ b/data/ppmt/ppmt_margins_halfyear.csv @@ -0,0 +1,14 @@ +period,gross_margin,sales_expense_rate,admin_expense_rate,non_gaap_net_margin +2020H1,65.23,27.27,15.33,17.28 +2020H2,62.55,24.00,9.12,26.49 +2021H1,63.02,23.68,13.52,24.55 +2021H2,60.39,25.25,11.69,20.84 +2022H1,58.10,29.39,13.68,15.95 +2022H2,56.86,34.43,16.10,8.73 +2023H1,60.36,31.21,11.77,19.03 +2023H2,62.10,32.30,10.78,18.79 +2024H1,64.05,29.69,9.53,22.33 +2024H2,68.26,27.09,6.05,28.13 +2025H1,70.34,23.01,5.55,33.94 +2025H2,73.15,21.04,4.30,36.03 +2026H1,69.68,22.89,5.40,30.02 diff --git a/data/ppmt/ppmt_profit.csv b/data/ppmt/ppmt_profit.csv index 34dfbb2..3c814aa 100644 --- a/data/ppmt/ppmt_profit.csv +++ b/data/ppmt/ppmt_profit.csv @@ -1,18 +1,20 @@ -period,revenue,gross_profit,gross_margin,net_profit -2026H1,171.73,119.66,69.67999999999999,50.38 -2025H2,371.2,267.65,72.1,127.76 -2025H1,138.76,97.61,70.34,45.74 -2024H2,130.38,87.08,66.79,31.25 -2024H1,45.58,29.19,64.05,9.21 -2023H2,63.01,38.64,61.31999999999999,10.82 -2023H1,28.14,16.98,60.36,4.77 -2022H2,46.17,26.55,57.489999999999995,4.76 -2022H1,23.59,13.7,58.099999999999994,3.33 -2021H2,44.91,27.59,61.42999999999999,8.54 -2021H1,17.73,11.17,63.019999999999996,3.59 -2020H2,25.13,15.94,63.42,5.24 -2020H1,8.18,5.33,65.23,1.41 -2019H2,16.83,10.9,64.77000000000001,4.51 -2019H1,5.43,3.31,60.84,1.14 -2018H2,5.15,2.98,57.92,1 -2017H2,1.58,0.75,47.61,0.02 +period,revenue,gross_profit,gross_margin,net_profit +2020H1,8.18,5.33,65.23,1.41 +2020H2,16.96,10.61,62.55,3.82 +2020FY,25.13,15.94,63.42,5.23 +2021H1,17.73,11.17,63.02,3.59 +2021H2,27.18,16.42,60.39,4.96 +2021FY,44.91,27.59,61.43,8.55 +2022H1,23.59,13.70,58.10,3.33 +2022H2,22.59,12.84,56.86,1.43 +2022FY,46.17,26.55,57.49,4.76 +2023H1,28.14,16.98,60.36,4.77 +2023H2,34.87,21.66,62.10,6.12 +2023FY,63.01,38.64,61.32,10.89 +2024H1,45.58,29.19,64.05,9.64 +2024H2,84.80,57.89,68.26,23.44 +2024FY,130.38,87.08,66.79,33.08 +2025H1,138.76,97.61,70.34,46.82 +2025H2,232.44,170.04,73.15,83.30 +2025FY,371.20,267.65,72.10,130.12 +2026H1,171.73,119.66,69.68,51.00 diff --git a/data/ppmt/ppmt_profit_annual.csv b/data/ppmt/ppmt_profit_annual.csv new file mode 100644 index 0000000..2103ab8 --- /dev/null +++ b/data/ppmt/ppmt_profit_annual.csv @@ -0,0 +1,7 @@ +period,revenue,gross_profit,gross_margin,net_profit +2020FY,25.13,15.94,63.42,5.23 +2021FY,44.91,27.59,61.43,8.55 +2022FY,46.17,26.55,57.49,4.76 +2023FY,63.01,38.64,61.32,10.89 +2024FY,130.38,87.08,66.79,33.08 +2025FY,371.20,267.65,72.10,130.12 diff --git a/data/ppmt/ppmt_profit_halfyear.csv b/data/ppmt/ppmt_profit_halfyear.csv new file mode 100644 index 0000000..0af33ca --- /dev/null +++ b/data/ppmt/ppmt_profit_halfyear.csv @@ -0,0 +1,14 @@ +period,revenue,gross_profit,gross_margin,net_profit +2020H1,8.18,5.33,65.23,1.41 +2020H2,16.96,10.61,62.55,3.82 +2021H1,17.73,11.17,63.02,3.59 +2021H2,27.18,16.42,60.39,4.96 +2022H1,23.59,13.70,58.10,3.33 +2022H2,22.59,12.84,56.86,1.43 +2023H1,28.14,16.98,60.36,4.77 +2023H2,34.87,21.66,62.10,6.12 +2024H1,45.58,29.19,64.05,9.64 +2024H2,84.80,57.89,68.26,23.44 +2025H1,138.76,97.61,70.34,46.82 +2025H2,232.44,170.04,73.15,83.30 +2026H1,171.73,119.66,69.68,51.00 diff --git a/docs/方案设计.md b/docs/方案设计.md index 9f92f56..31fb3b9 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -147,7 +147,9 @@ js/main.js + js/ui.js ← 解析引用、渲染三栏、导出和 } ``` -页面仅在声明多个视图时显示切换按钮,并将选择写入 URL query。当前泡泡玛特图表没有强制启用这一功能;基础能力已保留,等真实数据需要时再登记。 +页面仅在声明多个视图时显示切换按钮,并将选择写入 URL query;尚未启用多视图的图表维持单一 CSV,不显示控制器。 + +泡泡玛特已在 15 张连续图启用该能力:盈利能力、营收到利润的增速对比、核心利润及同比增速、核心利润率、利润率与费用率拆解、资产结构演变、主要 IP 收入趋势、各 IP 收入占比、除 THE MONSTERS 外 IP 收入及同比、品类收入结构、经营现金流与净利润、现金/负债/净营运资本、三大现金流与现金净增加、费用结构与费用率、渠道收入结构与增速。默认显示“全部”,并提供“年度 / 半年”;其原始披露没有可靠 Q1/Q3 数据,故不展示“季度”。其中 `FY` 为全年、`H1/H2` 为单独半年,流量类指标的 H2 在数据归一化阶段由 `FY − H1` 生成,比率与同比也在数据层重算;资产负债表类则为期末余额,H2 对应年末时点而非累计金额。 --- diff --git a/js/charts/ppmt_assets.js b/js/charts/ppmt_assets.js index 5b967bb..abc729b 100644 --- a/js/charts/ppmt_assets.js +++ b/js/charts/ppmt_assets.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { - PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle, + PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle, + financialViewLabel, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -8,15 +9,15 @@ import { * 泡泡玛特资产结构演变图 * 堆叠柱状图展示资产配置,折线图展示总资产增长 */ -export function assetsChart(rows, { width, height }) { +export function assetsChart(rows, { width, height, view = 'all' }) { const periods = col(rows, 'period'); return { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特资产结构演变(2020H1-2026H1)', - subtext: '单位:亿元', + text: `泡泡玛特资产结构演变(${financialViewLabel(view)})`, + subtext: '单位:亿元;资产负债表为期末余额,H2 为年末时点而非全年累计', }, legend: { ...legendStyle, @@ -29,7 +30,7 @@ export function assetsChart(rows, { width, height }) { axisPointer: { type: 'shadow' }, textStyle: { fontFamily: FONT }, }, - xAxis: { ...periodAxis, data: periods }, + xAxis: { ...standalonePeriodAxis, data: periods }, yAxis: [ { ...axisCommon, diff --git a/js/charts/ppmt_cash_conversion.js b/js/charts/ppmt_cash_conversion.js index 43237f6..d90693f 100644 --- a/js/charts/ppmt_cash_conversion.js +++ b/js/charts/ppmt_cash_conversion.js @@ -1,19 +1,23 @@ import { col } from '../csv.js'; import { - FINANCIAL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle, + FINANCIAL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle, + financialViewDisclosure, financialViewLabel, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; /** 泡泡玛特经营现金流与净利润:观察利润的现金兑现能力。 */ -export function cashConversionChart(rows, { width, height }) { +export function cashConversionChart(rows, { width, height, view = 'all' }) { const periods = col(rows, 'period'); + const conversionMax = Math.max(...col(rows, 'cash_conversion').filter(Number.isFinite)); + const conversionAxisMax = Math.max(200, Math.ceil((conversionMax * 1.1) / 100) * 100); + const barWidth = view === 'all' ? 14 : view === 'halfyear' ? 24.96 : 31.2; return { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特经营现金流与净利润(年度)', - subtext: '现金转换率 = 经营活动现金流净额 / 净利润;单位:亿元', + text: `泡泡玛特经营现金流与净利润(${financialViewLabel(view)})`, + subtext: ['现金转换率 = 经营活动现金流净额 / 净利润;单位:亿元', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: { ...legendStyle, @@ -27,7 +31,7 @@ export function cashConversionChart(rows, { width, height }) { textStyle: { fontFamily: FONT }, valueFormatter: (value) => `${value.toFixed(1)}`, }, - xAxis: { ...periodAxis, data: periods }, + xAxis: { ...standalonePeriodAxis, data: periods }, yAxis: [ { ...axisCommon, @@ -48,8 +52,8 @@ export function cashConversionChart(rows, { width, height }) { nameRotate: -90, nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT }, min: 0, - max: 200, - interval: 50, + max: conversionAxisMax, + interval: conversionAxisMax / 4, axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' }, splitLine: { show: false }, }, @@ -59,7 +63,7 @@ export function cashConversionChart(rows, { width, height }) { name: '经营活动现金流净额', type: 'bar', data: col(rows, 'operating_cash_flow'), - barWidth: 31.2, + barWidth, itemStyle: { color: FINANCIAL_COLORS.operatingCashFlow }, label: { show: true, position: 'top', color: PALETTE.ink, fontFamily: FONT, fontSize: 11, formatter: (p) => p.value.toFixed(1) }, }, @@ -67,7 +71,7 @@ export function cashConversionChart(rows, { width, height }) { name: '净利润', type: 'bar', data: col(rows, 'net_profit'), - barWidth: 31.2, + barWidth, itemStyle: { color: FINANCIAL_COLORS.netProfit }, label: { show: true, position: 'top', color: PALETTE.ink, fontFamily: FONT, fontSize: 11, formatter: (p) => p.value.toFixed(1) }, }, @@ -85,7 +89,7 @@ export function cashConversionChart(rows, { width, height }) { ], graphic: [ ...watermarks(width, height), - footnote('数据来源:泡泡玛特合并现金流量表、利润表(年度口径)| @思考的Joey'), + footnote('数据来源:泡泡玛特合并现金流量表、利润表;H2为单独半年 | @思考的Joey'), ], }; } diff --git a/js/charts/ppmt_cashflow_composition.js b/js/charts/ppmt_cashflow_composition.js index e323166..2993b50 100644 --- a/js/charts/ppmt_cashflow_composition.js +++ b/js/charts/ppmt_cashflow_composition.js @@ -1,12 +1,14 @@ import { col } from '../csv.js'; import { - FINANCIAL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend, + FINANCIAL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend, + financialViewDisclosure, financialViewLabel, lineDataWithSafeLabels, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; /** 泡泡玛特三大现金流:展示经营造血、投资投入与筹资变化。 */ -export function cashflowCompositionChart(rows, { width, height }) { +export function cashflowCompositionChart(rows, { width, height, view = 'all' }) { + const barWidth = view === 'all' ? 10 : view === 'halfyear' ? 18.2 : 26; const netCashIncrease = lineDataWithSafeLabels(rows, { lineKey: 'net_cash_increase', barKeys: ['operating_cash_flow', 'investing_cash_flow', 'financing_cash_flow'], @@ -16,8 +18,8 @@ export function cashflowCompositionChart(rows, { width, height }) { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特三大现金流与现金净增加(年度)', - subtext: '单位:亿元;正值为流入,负值为流出', + text: `泡泡玛特三大现金流与现金净增加(${financialViewLabel(view)})`, + subtext: ['单位:亿元;正值为流入,负值为流出', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, // 不手工指定 icon,让 ECharts 按系列类型生成:柱为矩形、折线为短线 + 空心圆。 legend: standardLegend(['经营活动现金流', '投资活动现金流', '筹资活动现金流', '现金净增加']), @@ -28,7 +30,7 @@ export function cashflowCompositionChart(rows, { width, height }) { textStyle: { fontFamily: FONT }, valueFormatter: (value) => `${value.toFixed(1)} 亿元`, }, - xAxis: { ...periodAxis, data: col(rows, 'period') }, + xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') }, yAxis: { ...axisCommon, type: 'value', @@ -38,9 +40,9 @@ export function cashflowCompositionChart(rows, { width, height }) { nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT }, }, series: [ - { name: '经营活动现金流', type: 'bar', data: col(rows, 'operating_cash_flow'), barWidth: 26, itemStyle: { color: FINANCIAL_COLORS.operatingCashFlow } }, - { name: '投资活动现金流', type: 'bar', data: col(rows, 'investing_cash_flow'), barWidth: 26, itemStyle: { color: FINANCIAL_COLORS.investingCashFlow } }, - { name: '筹资活动现金流', type: 'bar', data: col(rows, 'financing_cash_flow'), barWidth: 26, itemStyle: { color: FINANCIAL_COLORS.financingCashFlow } }, + { name: '经营活动现金流', type: 'bar', data: col(rows, 'operating_cash_flow'), barWidth, itemStyle: { color: FINANCIAL_COLORS.operatingCashFlow } }, + { name: '投资活动现金流', type: 'bar', data: col(rows, 'investing_cash_flow'), barWidth, itemStyle: { color: FINANCIAL_COLORS.investingCashFlow } }, + { name: '筹资活动现金流', type: 'bar', data: col(rows, 'financing_cash_flow'), barWidth, itemStyle: { color: FINANCIAL_COLORS.financingCashFlow } }, { ...lineSeries({ name: '现金净增加', @@ -55,7 +57,7 @@ export function cashflowCompositionChart(rows, { width, height }) { ], graphic: [ ...watermarks(width, height), - footnote('数据来源:泡泡玛特合并现金流量表(年度口径)| @思考的Joey'), + footnote('数据来源:泡泡玛特合并现金流量表;H2为单独半年 | @思考的Joey'), ], }; } diff --git a/js/charts/ppmt_category.js b/js/charts/ppmt_category.js index 377f3f0..370cccf 100644 --- a/js/charts/ppmt_category.js +++ b/js/charts/ppmt_category.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { - PALETTE, FONT, axisCommon, categoryAxis, titleStyle, legendStyle, + PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle, + financialViewDisclosure, financialViewLabel, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -8,7 +9,7 @@ import { * 泡泡玛特品类收入结构图 * 堆叠柱状图展示各品类的收入占比和增长 */ -export function categoryChart(rows, { width, height }) { +export function categoryChart(rows, { width, height, view = 'all' }) { // 过滤掉表头行("品类"那一行) const dataRows = rows.filter(r => r.period && r.period !== '品类'); const periods = col(dataRows, 'period'); @@ -17,8 +18,8 @@ export function categoryChart(rows, { width, height }) { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特品类收入结构(2024H1-2025H2)', - subtext: '单位:亿元', + text: `泡泡玛特品类收入结构(${financialViewLabel(view)})`, + subtext: ['单位:亿元;H2为下半年单期', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: { ...legendStyle, @@ -41,14 +42,7 @@ export function categoryChart(rows, { width, height }) { return result; }, }, - xAxis: { - ...categoryAxis, - data: periods, - axisLabel: { - ...categoryAxis.axisLabel, - formatter: (value) => String(value).replace(/^20/, ''), - }, - }, + xAxis: { ...standalonePeriodAxis, data: periods }, yAxis: { ...axisCommon, type: 'value', diff --git a/js/charts/ppmt_channel_revenue.js b/js/charts/ppmt_channel_revenue.js index 59afad1..754a2f4 100644 --- a/js/charts/ppmt_channel_revenue.js +++ b/js/charts/ppmt_channel_revenue.js @@ -1,11 +1,12 @@ import { col } from '../csv.js'; import { - CHANNEL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle, + CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle, + financialViewDisclosure, financialViewLabel, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; /** 泡泡玛特渠道收入结构与增速。 */ -export function channelRevenueChart(rows, { width, height }) { +export function channelRevenueChart(rows, { width, height, view = 'all' }) { const channels = [ { key: 'retail', name: '零售店', color: CHANNEL_COLORS.retail }, { key: 'online', name: '线上', color: CHANNEL_COLORS.online }, @@ -17,8 +18,8 @@ export function channelRevenueChart(rows, { width, height }) { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特渠道收入结构与增速', - subtext: '渠道收入为堆叠柱;折线为营业收入同比增速', + text: `泡泡玛特渠道收入结构与增速(${financialViewLabel(view)})`, + subtext: ['渠道收入为堆叠柱;折线为营业收入同比增速', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: { ...legendStyle, @@ -32,7 +33,7 @@ export function channelRevenueChart(rows, { width, height }) { textStyle: { fontFamily: FONT }, valueFormatter: (value) => value.toFixed(1), }, - xAxis: { ...periodAxis, data: col(rows, 'period') }, + xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') }, yAxis: [ { ...axisCommon, diff --git a/js/charts/ppmt_core_margin.js b/js/charts/ppmt_core_margin.js index cf5d4e7..b04909e 100644 --- a/js/charts/ppmt_core_margin.js +++ b/js/charts/ppmt_core_margin.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { - PALETTE, FONT, axisCommon, periodAxis, titleStyle, + PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, + financialViewDisclosure, financialViewLabel, lineSeries, standardLegend, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -15,15 +16,15 @@ import { */ const LINE_COLOR = '#7B2D42'; -export function ppmtCoreMarginChart(rows, { width, height }) { +export function ppmtCoreMarginChart(rows, { width, height, view = 'all' }) { const pct = (p) => `${p.value.toFixed(1)}%`; return { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特核心利润率(20H1-26H1)', - subtext: '核心利润率 = 毛利率 − 销售费用率 − 管理费用率,剔除汇兑与投资等非主业扰动', + text: `泡泡玛特核心利润率(${financialViewLabel(view)})`, + subtext: ['核心利润率 = 毛利率 − 销售费用率 − 管理费用率,剔除汇兑与投资等非主业扰动', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: standardLegend(['核心利润率'], { itemGap: 32 }), grid: { left: 78, right: 60, top: LAYOUT.gridTop, bottom: 90 }, @@ -32,7 +33,7 @@ export function ppmtCoreMarginChart(rows, { width, height }) { valueFormatter: (v) => `${v.toFixed(1)}%`, textStyle: { fontFamily: FONT }, }, - xAxis: { ...periodAxis, data: col(rows, 'period') }, + xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') }, yAxis: { ...axisCommon, type: 'value', diff --git a/js/charts/ppmt_core_profit.js b/js/charts/ppmt_core_profit.js index c7b3589..fc5da19 100644 --- a/js/charts/ppmt_core_profit.js +++ b/js/charts/ppmt_core_profit.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { - PALETTE, FINANCIAL_COLORS, FONT, axisCommon, periodAxis, titleStyle, legendStyle, + PALETTE, FINANCIAL_COLORS, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle, + financialViewDisclosure, financialViewLabel, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -9,7 +10,7 @@ import { * 核心利润 = 毛利 - 销售费用 - 管理费用,反映主业盈利能力去除营销和管理成本后的净额。 * 柱状图展示绝对值(亿元),折线图展示同比增速(%)。 */ -export function ppmtCoreProfitChart(rows, { width, height }) { +export function ppmtCoreProfitChart(rows, { width, height, view = 'all' }) { const periods = col(rows, 'period'); const core = col(rows, 'core_profit'); const yoy = col(rows, 'yoy'); @@ -18,8 +19,8 @@ export function ppmtCoreProfitChart(rows, { width, height }) { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特核心利润及同比增速(20H1-26H1)', - subtext: '核心利润 = 毛利 - 销售费用 - 管理费用,单位:亿元', + text: `泡泡玛特核心利润及同比增速(${financialViewLabel(view)})`, + subtext: ['核心利润 = 毛利 - 销售费用 - 管理费用,单位:亿元', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: { ...legendStyle, @@ -49,7 +50,7 @@ export function ppmtCoreProfitChart(rows, { width, height }) { return result; }, }, - xAxis: { ...periodAxis, data: periods }, + xAxis: { ...standalonePeriodAxis, data: periods }, yAxis: [ { // 左轴:核心利润(亿元) diff --git a/js/charts/ppmt_expense_structure.js b/js/charts/ppmt_expense_structure.js index 129948e..0427a37 100644 --- a/js/charts/ppmt_expense_structure.js +++ b/js/charts/ppmt_expense_structure.js @@ -1,17 +1,18 @@ import { col } from '../csv.js'; import { - PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle, + PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle, + financialViewDisclosure, financialViewLabel, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; /** 泡泡玛特期间费用结构:观察销售投入、管理成本与费用率的规模效应。 */ -export function expenseStructureChart(rows, { width, height }) { +export function expenseStructureChart(rows, { width, height, view = 'all' }) { return { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特费用结构与费用率(年度)', - subtext: '销售、管理、财务费用;折线为四项费用率', + text: `泡泡玛特费用结构与费用率(${financialViewLabel(view)})`, + subtext: ['销售、管理、财务费用;折线为四项费用率', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: { ...legendStyle, @@ -25,7 +26,7 @@ export function expenseStructureChart(rows, { width, height }) { textStyle: { fontFamily: FONT }, valueFormatter: (value) => value.toFixed(1), }, - xAxis: { ...periodAxis, data: col(rows, 'period') }, + xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') }, yAxis: [ { ...axisCommon, @@ -68,7 +69,7 @@ export function expenseStructureChart(rows, { width, height }) { ], graphic: [ ...watermarks(width, height), - footnote('数据来源:泡泡玛特合并利润表(年度口径)| @思考的Joey'), + footnote('数据来源:泡泡玛特合并利润表;H2为单独半年 | @思考的Joey'), ], }; } diff --git a/js/charts/ppmt_growth.js b/js/charts/ppmt_growth.js index e11ca4f..82a154f 100644 --- a/js/charts/ppmt_growth.js +++ b/js/charts/ppmt_growth.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { - PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend, + PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend, + financialViewDisclosure, financialViewLabel, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -43,15 +44,15 @@ function labeledData(rows, key) { }); } -export function ppmtGrowthChart(rows, { width, height }) { +export function ppmtGrowthChart(rows, { width, height, view = 'all' }) { const pct = (p) => `${p.value}%`; return { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特营收到利润的增速对比(2020H1-2026H1)', - subtext: '同比增速(%),净利增速显著高于营收增速体现利润弹性', + text: `泡泡玛特营收到利润的增速对比(${financialViewLabel(view)})`, + subtext: ['同比增速(%),净利增速显著高于营收增速体现利润弹性', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: standardLegend(['非GAAP归母净利', '毛利润', '营业收入', '营业成本'], { itemGap: 34 }), grid: { left: 84, right: 56, top: LAYOUT.gridTop, bottom: 80 }, @@ -60,7 +61,7 @@ export function ppmtGrowthChart(rows, { width, height }) { valueFormatter: (v) => `${v}%`, textStyle: { fontFamily: FONT }, }, - xAxis: { ...periodAxis, data: col(rows, 'period') }, + xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') }, yAxis: { ...axisCommon, type: 'value', @@ -105,7 +106,7 @@ export function ppmtGrowthChart(rows, { width, height }) { })), graphic: [ ...watermarks(width, height), - footnote('数据来源:泡泡玛特财报,2020H1/H2非GAAP净利用归母净利替代 | @思考的Joey'), + footnote('数据来源:泡泡玛特财报;2020H1/H2非GAAP净利用归母净利替代 | @思考的Joey'), ], }; } diff --git a/js/charts/ppmt_ip.js b/js/charts/ppmt_ip.js index 71d9029..2b9c86d 100644 --- a/js/charts/ppmt_ip.js +++ b/js/charts/ppmt_ip.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { PALETTE, FONT, axisCommon, categoryAxis, titleStyle, legendStyle, + financialViewDisclosure, financialViewLabel, shortStandalonePeriod, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -8,7 +9,7 @@ import { * 泡泡玛特主要IP收入趋势图 * 多条折线展示各IP的收入变化 */ -export function ipChart(rows, { width, height }) { +export function ipChart(rows, { width, height, view = 'all' }) { // 提取时期(从第一行的列名) const periods = Object.keys(rows[0]).filter(k => k !== 'ip'); @@ -42,8 +43,8 @@ export function ipChart(rows, { width, height }) { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特主要IP收入趋势(2021H1-2025H2)', - subtext: '单位:亿元', + text: `泡泡玛特主要IP收入趋势(${financialViewLabel(view)})`, + subtext: ['单位:亿元;H2为单独半年', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: { ...legendStyle, @@ -62,7 +63,7 @@ export function ipChart(rows, { width, height }) { data: periods, axisLabel: { ...categoryAxis.axisLabel, - formatter: (value) => String(value).replace(/^20/, ''), + formatter: shortStandalonePeriod, }, }, yAxis: { diff --git a/js/charts/ppmt_ip_ex_monsters.js b/js/charts/ppmt_ip_ex_monsters.js index 273e018..822bf0c 100644 --- a/js/charts/ppmt_ip_ex_monsters.js +++ b/js/charts/ppmt_ip_ex_monsters.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { - PALETTE, FONT, axisCommon, categoryAxis, titleStyle, labelStyle, + PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, labelStyle, + financialViewDisclosure, financialViewLabel, barSeries, lineSeries, standardLegend, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -34,13 +35,13 @@ const BAR_COLOR = '#176B52'; // 墨翡翠 const LINE_COLOR = '#7A3FE0'; // 鲜亮紫 const LINE_ON_BAR = '#DCC9FF'; // 压在翡翠柱面上时用的浅紫(对比度约 6:1) -const Y_MAX = 160; // 左轴上限 const YOY_MIN = -50; // 右轴下限 const YOY_MAX = 150; // 右轴上限 -export function ppmtIpExMonstersChart(rows, { width, height }) { +export function ppmtIpExMonstersChart(rows, { width, height, view = 'all' }) { const periods = col(rows, 'period'); const revenue = col(rows, 'revenue'); + const yMax = Math.max(...revenue) > 160 ? 300 : 160; // 前两期无同比基数,CSV 里留空;parseCSV 会给出 '',统一转成 null const yoy = col(rows, 'yoy').map((v) => (v === '' ? null : v)); @@ -50,7 +51,7 @@ export function ppmtIpExMonstersChart(rows, { width, height }) { const labelGap = (8 + 13) / plotH; // labelStyle 的 distance + 字高,换成占比 const yoyData = yoy.map((v, i) => { if (v === null) return null; - const barFrac = revenue[i] / Y_MAX; + const barFrac = revenue[i] / yMax; const lineFrac = (v - YOY_MIN) / (YOY_MAX - YOY_MIN); // 标签整体(底边到顶边)都落在柱子高度以内 → 压在柱面上 const onBar = lineFrac + labelGap < barFrac; @@ -61,8 +62,8 @@ export function ppmtIpExMonstersChart(rows, { width, height }) { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特除 THE MONSTERS 外的IP收入及同比(21H1-26H1)', - subtext: 'IP收入合计 − THE MONSTERS,单位:亿元;H2 为下半年单期,非全年累计', + text: `泡泡玛特除 THE MONSTERS 外的IP收入及同比(${financialViewLabel(view)})`, + subtext: ['IP收入合计 − THE MONSTERS,单位:亿元;H2为下半年单期,非全年累计', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: standardLegend(['除MONSTERS外收入', '同比增速'], { itemGap: 32 }), grid: { left: 78, right: 128, top: LAYOUT.gridTop, bottom: 90 }, @@ -82,12 +83,7 @@ export function ppmtIpExMonstersChart(rows, { width, height }) { return out; }, }, - xAxis: { - ...categoryAxis, - data: periods, - // 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」 - axisLabel: { ...categoryAxis.axisLabel, formatter: (v) => v.replace(/^20/, '') }, - }, + xAxis: { ...standalonePeriodAxis, data: periods }, yAxis: [ { ...axisCommon, @@ -97,9 +93,8 @@ export function ppmtIpExMonstersChart(rows, { width, height }) { nameGap: 50, nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT }, min: 0, - // 定死 160:留出顶部余量,最高柱(139.0)不会顶到绘图区上沿 - max: Y_MAX, - interval: 20, + max: yMax, + interval: yMax === 160 ? 20 : 50, }, { type: 'value', diff --git a/js/charts/ppmt_ip_share_history.js b/js/charts/ppmt_ip_share_history.js index 0bbd54d..9d41d00 100644 --- a/js/charts/ppmt_ip_share_history.js +++ b/js/charts/ppmt_ip_share_history.js @@ -1,5 +1,6 @@ import { PALETTE, FONT, axisCommon, categoryAxis, titleStyle, LEGEND_ITEM_WIDTH, + financialViewDisclosure, financialViewLabel, shortStandalonePeriod, barSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -38,7 +39,7 @@ const COLORS = { const LEGEND_WIDTH = 160; const LEGEND_RIGHT = 20; -export function ppmtIpShareHistoryChart(rows, { width, height }) { +export function ppmtIpShareHistoryChart(rows, { width, height, view = 'all' }) { const periods = Object.keys(rows[0]).filter((k) => k !== 'ip'); const names = rows.map((r) => r.ip); const totals = periods.map((p) => rows.reduce((s, r) => s + r[p], 0)); @@ -49,8 +50,8 @@ export function ppmtIpShareHistoryChart(rows, { width, height }) { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特各IP半年收入占比(21H1-26H1)', - subtext: '占当期IP收入合计的比重;H2 为下半年单期,非全年累计', + text: `泡泡玛特各IP收入占比(${financialViewLabel(view)})`, + subtext: ['占当期IP收入合计的比重;H2为下半年单期,非全年累计', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: { orient: 'vertical', @@ -89,7 +90,7 @@ export function ppmtIpShareHistoryChart(rows, { width, height }) { ...categoryAxis, data: periods, // 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」 - axisLabel: { ...categoryAxis.axisLabel, formatter: (v) => v.replace(/^20/, '') }, + axisLabel: { ...categoryAxis.axisLabel, formatter: shortStandalonePeriod }, }, yAxis: { ...axisCommon, diff --git a/js/charts/ppmt_liquidity_working_capital.js b/js/charts/ppmt_liquidity_working_capital.js index abf5516..298780b 100644 --- a/js/charts/ppmt_liquidity_working_capital.js +++ b/js/charts/ppmt_liquidity_working_capital.js @@ -1,19 +1,20 @@ import { col } from '../csv.js'; import { - PALETTE, FINANCIAL_COLORS, FONT, axisCommon, periodAxis, titleStyle, legendStyle, + PALETTE, FINANCIAL_COLORS, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle, + financialViewLabel, lineDataWithSafeLabels, lineSeries, barSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; /** 泡泡玛特现金、负债与净营运资本:观察流动性与资金占用。 */ -export function liquidityWorkingCapitalChart(rows, { width, height }) { +export function liquidityWorkingCapitalChart(rows, { width, height, view = 'all' }) { const periods = col(rows, 'period'); return { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特现金、负债与净营运资本', - subtext: '净营运资本 = 流动资产 − 流动负债;单位:亿元', + text: `泡泡玛特现金、负债与净营运资本(${financialViewLabel(view)})`, + subtext: '净营运资本 = 流动资产 − 流动负债;单位:亿元;H2 为年末时点', }, legend: { ...legendStyle, @@ -27,7 +28,7 @@ export function liquidityWorkingCapitalChart(rows, { width, height }) { textStyle: { fontFamily: FONT }, valueFormatter: (value) => `${value.toFixed(1)} 亿元`, }, - xAxis: { ...periodAxis, data: periods }, + xAxis: { ...standalonePeriodAxis, data: periods }, yAxis: { ...axisCommon, type: 'value', diff --git a/js/charts/ppmt_margins.js b/js/charts/ppmt_margins.js index 25b9cb8..f7b6571 100644 --- a/js/charts/ppmt_margins.js +++ b/js/charts/ppmt_margins.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { - PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend, + PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend, + financialViewDisclosure, financialViewLabel, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -54,15 +55,15 @@ function labeledData(rows, key) { }); } -export function ppmtMarginsChart(rows, { width, height }) { +export function ppmtMarginsChart(rows, { width, height, view = 'all' }) { const pct = (p) => `${p.value}%`; return { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特利润率与费用率拆解(2020H1-2026H1)', - subtext: '毛利率抬升叠加费用率下降,共同推动净利率走高(H1为半年,H2为全年累计)', + text: `泡泡玛特利润率与费用率拆解(${financialViewLabel(view)})`, + subtext: ['毛利率抬升叠加费用率下降,共同推动净利率走高', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: standardLegend(SERIES.map((series) => series.name)), grid: { left: 78, right: 56, top: LAYOUT.gridTop, bottom: 80 }, @@ -71,7 +72,7 @@ export function ppmtMarginsChart(rows, { width, height }) { valueFormatter: (v) => `${v}%`, textStyle: { fontFamily: FONT }, }, - xAxis: { ...periodAxis, data: col(rows, 'period') }, + xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') }, yAxis: { ...axisCommon, type: 'value', diff --git a/js/charts/ppmt_profit.js b/js/charts/ppmt_profit.js index 877b7fb..9719fe2 100644 --- a/js/charts/ppmt_profit.js +++ b/js/charts/ppmt_profit.js @@ -1,6 +1,7 @@ import { col } from '../csv.js'; import { - PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle, + PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle, + financialViewDisclosure, financialViewLabel, lineSeries, footnote, watermarks, LAYOUT, } from '../theme.js'; @@ -8,19 +9,19 @@ import { * 泡泡玛特盈利能力分析图 * 柱状图展示收入和利润,折线图展示毛利率 */ -export function profitChart(rows, { width, height }) { - const periods = col(rows, 'period').reverse(); // 按时间正序 - const revenue = col(rows, 'revenue').reverse(); - const grossProfit = col(rows, 'gross_profit').reverse(); - const netProfit = col(rows, 'net_profit').reverse(); - const grossMargin = col(rows, 'gross_margin').reverse(); +export function profitChart(rows, { width, height, view = 'all' }) { + const periods = col(rows, 'period'); + const revenue = col(rows, 'revenue'); + const grossProfit = col(rows, 'gross_profit'); + const netProfit = col(rows, 'net_profit'); + const grossMargin = col(rows, 'gross_margin'); return { backgroundColor: '#fff', title: { ...titleStyle, - text: '泡泡玛特盈利能力分析(2022H2-2026H1)', - subtext: '营收、毛利、净利润及毛利率变化', + text: `泡泡玛特盈利能力分析(${financialViewLabel(view)})`, + subtext: ['营收、毛利、净利润及毛利率变化', financialViewDisclosure(view)].filter(Boolean).join('\n'), }, legend: { ...legendStyle, @@ -33,7 +34,7 @@ export function profitChart(rows, { width, height }) { axisPointer: { type: 'shadow' }, textStyle: { fontFamily: FONT }, }, - xAxis: { ...periodAxis, data: periods }, + xAxis: { ...standalonePeriodAxis, data: periods }, yAxis: [ { ...axisCommon, @@ -122,7 +123,7 @@ export function profitChart(rows, { width, height }) { ], graphic: [ ...watermarks(width, height), - footnote('数据来源:泡泡玛特财报(累计数据)| @思考的Joey'), + footnote('数据来源:泡泡玛特财报;H2为单独半年 | @思考的Joey'), ], }; } diff --git a/js/main.js b/js/main.js index 95d544e..e01345f 100644 --- a/js/main.js +++ b/js/main.js @@ -11,6 +11,7 @@ import { const instances = new Map(); const activeViews = new Map(); +const chartRenderVersions = new Map(); let manifest; let shell; let state; @@ -106,25 +107,47 @@ function chartSource(chart) { return chart.views[view]; } -async function renderChart(chart, version) { +async function renderChart(chart, version, chartVersion) { const el = document.getElementById(`chart-${chart.instanceId}`); if (!el) return; const source = chartSource(chart); + const view = chart.views + ? activeViews.get(chart.viewKey) || chart.defaultView + : undefined; try { const rows = await loadCSV(`${source}?t=${cacheToken}`); - if (version !== renderVersion || !document.body.contains(el)) return; + if (version !== renderVersion || chartVersion !== chartRenderVersions.get(chart.instanceId) || !document.body.contains(el)) return; const instance = echarts.init(el, null, { renderer: 'canvas' }); instances.set(chart.instanceId, instance); const { width, height } = el.getBoundingClientRect(); - instance.setOption(getChartBuilder(chart.key)(rows, { width, height }), true); + instance.setOption(getChartBuilder(chart.key)(rows, { width, height, view }), true); } catch (error) { if (version === renderVersion) showChartError(chart.instanceId, error.message); console.error(`渲染图表失败:${chart.id}`, error); } } +function renderSingleChart(chart) { + const previous = instances.get(chart.instanceId); + previous?.dispose(); + instances.delete(chart.instanceId); + const chartVersion = (chartRenderVersions.get(chart.instanceId) || 0) + 1; + chartRenderVersions.set(chart.instanceId, chartVersion); + renderChart(chart, renderVersion, chartVersion); +} + +function updateViewControls(chart, view) { + const card = document.getElementById(`card-${chart.instanceId}`); + if (!card) return; + card.querySelectorAll('.view-button').forEach((button) => { + button.dataset.active = String(button.dataset.view === view); + }); + const source = card.querySelector('.src'); + if (source) source.textContent = chartSource(chart); +} + function exportPNG(chartId, filename) { const instance = instances.get(chartId); if (!instance) return; @@ -167,12 +190,19 @@ function renderCurrent() { renderCards(shell.cards, charts, activeViews, { onExport: exportPNG, onSelectView: (chart, view) => { + if (activeViews.get(chart.viewKey) === view) return; activeViews.set(chart.viewKey, view); - updateHash({ companyId: company.id, tabId: tab.id, chartId: chart.id, view }); + state = normalizeState({ companyId: company.id, tabId: tab.id, chartId: chart.id, view }); + history.pushState(null, '', serializeHash(state)); + updateViewControls(chart, view); + renderSingleChart(chart); }, }); renderAnchors(shell.chartNav, charts, selectAnchor); - Promise.all(charts.map((chart) => renderChart(chart, version))); + charts.forEach((chart) => { + chartRenderVersions.set(chart.instanceId, (chartRenderVersions.get(chart.instanceId) || 0) + 1); + }); + Promise.all(charts.map((chart) => renderChart(chart, version, chartRenderVersions.get(chart.instanceId)))); } async function start() { diff --git a/js/theme.js b/js/theme.js index e4e4e46..8072b8f 100644 --- a/js/theme.js +++ b/js/theme.js @@ -108,6 +108,32 @@ export const periodAxis = { }, }; +/** + * 独立期间轴:FY 为全年,H1/H2 为单独半年。 + * 用于已拆分累计值的多时间维度图,避免把 H2 错写为全年。 + */ +export function shortStandalonePeriod(label) { + return String(label) + .replace(/^20(\d{2})FY$/, '$1全年') + .replace(/^20(\d{2})/, '$1'); +} + +export const standalonePeriodAxis = { + ...categoryAxis, + axisLabel: { + ...categoryAxis.axisLabel, + formatter: shortStandalonePeriod, + }, +}; + +export function financialViewLabel(view) { + return ({ all: '全部期间', annual: '年度', halfyear: '半年', quarter: '季度' })[view] || '全部期间'; +} + +export function financialViewDisclosure(view) { + return ''; +} + export const titleStyle = { left: 'center', top: 18, diff --git a/js/ui.js b/js/ui.js index 913083a..fb116ad 100644 --- a/js/ui.js +++ b/js/ui.js @@ -79,13 +79,15 @@ export function renderCards(container, charts, activeViews, { onExport, onSelect if (viewNames.length > 1) { const switcher = element('div', 'view-switcher'); - const labels = { annual: '年度', halfyear: '半年', quarter: '季度' }; + const labels = { all: '全部', annual: '年度', halfyear: '半年', quarter: '季度' }; viewNames.forEach((view) => { - switcher.append(button(labels[view] || view, { + const viewButton = button(labels[view] || view, { active: view === activeView, onClick: () => onSelectView(chart, view), className: 'view-button', - })); + }); + viewButton.dataset.view = view; + switcher.append(viewButton); }); bar.append(switcher); } diff --git a/manifest.json b/manifest.json index 3566b47..5e45b10 100644 --- a/manifest.json +++ b/manifest.json @@ -4,28 +4,28 @@ "id": "ppmt", "name": "泡泡玛特", "chartCatalog": { - "ppmt-profit": { "key": "ppmt-profit", "title": "盈利能力", "csv": "data/ppmt/ppmt_profit.csv", "height": 680, "filename": "泡泡玛特盈利能力" }, + "ppmt-profit": { "key": "ppmt-profit", "title": "盈利能力", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_profit.csv", "annual": "data/ppmt/ppmt_profit_annual.csv", "halfyear": "data/ppmt/ppmt_profit_halfyear.csv" }, "height": 680, "filename": "泡泡玛特盈利能力" }, "ppmt-sankey": { "key": "ppmt-sankey", "title": "利润流向", "csv": "data/ppmt/ppmt_sankey_2026h1.csv", "height": 800, "filename": "泡泡玛特利润流向2026H1" }, - "ppmt-growth": { "key": "ppmt-growth", "title": "营收到利润的增速对比", "csv": "data/ppmt/ppmt_growth.csv", "height": 700, "filename": "泡泡玛特营收到利润增速对比" }, - "ppmt-core-profit": { "key": "ppmt-core-profit", "title": "核心利润及同比增速", "csv": "data/ppmt/ppmt_core_profit.csv", "height": 720, "filename": "泡泡玛特核心利润及同比增速" }, - "ppmt-core-margin": { "key": "ppmt-core-margin", "title": "核心利润率", "csv": "data/ppmt/ppmt_core_margin.csv", "height": 680, "filename": "泡泡玛特核心利润率" }, + "ppmt-growth": { "key": "ppmt-growth", "title": "营收到利润的增速对比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_growth.csv", "annual": "data/ppmt/ppmt_growth_annual.csv", "halfyear": "data/ppmt/ppmt_growth_halfyear.csv" }, "height": 700, "filename": "泡泡玛特营收到利润增速对比" }, + "ppmt-core-profit": { "key": "ppmt-core-profit", "title": "核心利润及同比增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_core_profit.csv", "annual": "data/ppmt/ppmt_core_profit_annual.csv", "halfyear": "data/ppmt/ppmt_core_profit_halfyear.csv" }, "height": 720, "filename": "泡泡玛特核心利润及同比增速" }, + "ppmt-core-margin": { "key": "ppmt-core-margin", "title": "核心利润率", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_core_margin.csv", "annual": "data/ppmt/ppmt_core_margin_annual.csv", "halfyear": "data/ppmt/ppmt_core_margin_halfyear.csv" }, "height": 680, "filename": "泡泡玛特核心利润率" }, "ppmt-bridge": { "key": "ppmt-bridge", "title": "净利率同比拆解", "csv": "data/ppmt/ppmt_margin_bridge.csv", "height": 700, "filename": "泡泡玛特净利率同比拆解" }, - "ppmt-margins": { "key": "ppmt-margins", "title": "利润率与费用率拆解", "csv": "data/ppmt/ppmt_margins.csv", "height": 680, "filename": "泡泡玛特利润率与费用率拆解" }, - "ppmt-assets": { "key": "ppmt-assets", "title": "资产结构演变", "csv": "data/ppmt/ppmt_assets.csv", "height": 680, "filename": "泡泡玛特资产结构" }, - "ppmt-ip": { "key": "ppmt-ip", "title": "主要 IP 收入趋势", "csv": "data/ppmt/ppmt_ip.csv", "height": 720, "filename": "泡泡玛特IP收入趋势" }, + "ppmt-margins": { "key": "ppmt-margins", "title": "利润率与费用率拆解", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_margins.csv", "annual": "data/ppmt/ppmt_margins_annual.csv", "halfyear": "data/ppmt/ppmt_margins_halfyear.csv" }, "height": 680, "filename": "泡泡玛特利润率与费用率拆解" }, + "ppmt-assets": { "key": "ppmt-assets", "title": "资产结构演变", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_assets.csv", "annual": "data/ppmt/ppmt_assets_annual.csv", "halfyear": "data/ppmt/ppmt_assets_halfyear.csv" }, "height": 680, "filename": "泡泡玛特资产结构" }, + "ppmt-ip": { "key": "ppmt-ip", "title": "主要 IP 收入趋势", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip.csv", "annual": "data/ppmt/ppmt_ip_annual.csv", "halfyear": "data/ppmt/ppmt_ip_halfyear.csv" }, "height": 720, "filename": "泡泡玛特IP收入趋势" }, "ppmt-ip-share": { "key": "ppmt-ip-share", "title": "主要 IP 收入占比", "csv": "data/ppmt/ppmt_ip_share_2026h1.csv", "height": 720, "filename": "泡泡玛特IP收入占比2026H1" }, - "ppmt-ip-share-history": { "key": "ppmt-ip-share-history", "title": "各 IP 半年收入占比", "csv": "data/ppmt/ppmt_ip_share_history.csv", "height": 700, "filename": "泡泡玛特各IP半年收入占比" }, - "ppmt-ip-ex-monsters": { "key": "ppmt-ip-ex-monsters", "title": "除 THE MONSTERS 外的 IP 收入及同比", "csv": "data/ppmt/ppmt_ip_ex_monsters.csv", "height": 700, "filename": "泡泡玛特除MONSTERS外IP收入及同比" }, - "ppmt-category": { "key": "ppmt-category", "title": "品类收入结构", "csv": "data/ppmt/ppmt_category.csv", "height": 680, "filename": "泡泡玛特品类结构" }, + "ppmt-ip-share-history": { "key": "ppmt-ip-share-history", "title": "各 IP 半年收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip_share_history.csv", "annual": "data/ppmt/ppmt_ip_share_history_annual.csv", "halfyear": "data/ppmt/ppmt_ip_share_history_halfyear.csv" }, "height": 700, "filename": "泡泡玛特各IP半年收入占比" }, + "ppmt-ip-ex-monsters": { "key": "ppmt-ip-ex-monsters", "title": "除 THE MONSTERS 外的 IP 收入及同比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip_ex_monsters.csv", "annual": "data/ppmt/ppmt_ip_ex_monsters_annual.csv", "halfyear": "data/ppmt/ppmt_ip_ex_monsters_halfyear.csv" }, "height": 700, "filename": "泡泡玛特除MONSTERS外IP收入及同比" }, + "ppmt-category": { "key": "ppmt-category", "title": "品类收入结构", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_category.csv", "annual": "data/ppmt/ppmt_category_annual.csv", "halfyear": "data/ppmt/ppmt_category_halfyear.csv" }, "height": 680, "filename": "泡泡玛特品类结构" }, "ppmt-region-channel": { "key": "ppmt-region-channel", "title": "区域渠道分布", "csv": "data/ppmt/ppmt_region_channel_2026h1.csv", "height": 680, "filename": "泡泡玛特区域渠道分布2026H1" }, "ppmt-region-yoy": { "key": "ppmt-region-yoy", "title": "区域渠道同比增速", "csv": "data/ppmt/ppmt_region_channel_yoy.csv", "height": 620, "filename": "泡泡玛特区域渠道同比增速" }, "ppmt-region-yoy-timeseries": { "key": "ppmt-region-yoy-timeseries", "title": "区域渠道同比增速趋势", "csv": "data/ppmt/ppmt_region_channel_yoy_timeseries.csv", "height": 760, "filename": "泡泡玛特区域渠道同比增速趋势" }, - "ppmt-cash-conversion": { "key": "ppmt-cash-conversion", "title": "经营现金流与净利润(年度)", "csv": "data/ppmt/ppmt_cash_conversion.csv", "height": 700, "filename": "泡泡玛特经营现金流与净利润" }, - "ppmt-liquidity-working-capital": { "key": "ppmt-liquidity-working-capital", "title": "现金、负债与净营运资本", "csv": "data/ppmt/ppmt_liquidity_working_capital.csv", "height": 720, "filename": "泡泡玛特现金负债与净营运资本" }, - "ppmt-cashflow-composition": { "key": "ppmt-cashflow-composition", "title": "三大现金流与现金净增加(年度)", "csv": "data/ppmt/ppmt_cashflow_composition.csv", "height": 700, "filename": "泡泡玛特三大现金流与现金净增加" }, + "ppmt-cash-conversion": { "key": "ppmt-cash-conversion", "title": "经营现金流与净利润", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_cash_conversion.csv", "annual": "data/ppmt/ppmt_cash_conversion_annual.csv", "halfyear": "data/ppmt/ppmt_cash_conversion_halfyear.csv" }, "height": 700, "filename": "泡泡玛特经营现金流与净利润" }, + "ppmt-liquidity-working-capital": { "key": "ppmt-liquidity-working-capital", "title": "现金、负债与净营运资本", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_liquidity_working_capital.csv", "annual": "data/ppmt/ppmt_liquidity_working_capital_annual.csv", "halfyear": "data/ppmt/ppmt_liquidity_working_capital_halfyear.csv" }, "height": 720, "filename": "泡泡玛特现金负债与净营运资本" }, + "ppmt-cashflow-composition": { "key": "ppmt-cashflow-composition", "title": "三大现金流与现金净增加", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_cashflow_composition.csv", "annual": "data/ppmt/ppmt_cashflow_composition_annual.csv", "halfyear": "data/ppmt/ppmt_cashflow_composition_halfyear.csv" }, "height": 700, "filename": "泡泡玛特三大现金流与现金净增加" }, "ppmt-working-capital-days": { "key": "ppmt-working-capital-days", "title": "营运资本效率", "csv": "data/ppmt/ppmt_working_capital_days.csv", "height": 700, "filename": "泡泡玛特营运资本效率" }, - "ppmt-expense-structure": { "key": "ppmt-expense-structure", "title": "费用结构与费用率(年度)", "csv": "data/ppmt/ppmt_expense_structure.csv", "height": 700, "filename": "泡泡玛特费用结构与费用率" }, - "ppmt-channel-revenue": { "key": "ppmt-channel-revenue", "title": "渠道收入结构与增速", "csv": "data/ppmt/ppmt_channel_revenue.csv", "height": 720, "filename": "泡泡玛特渠道收入结构与增速" } + "ppmt-expense-structure": { "key": "ppmt-expense-structure", "title": "费用结构与费用率", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_expense_structure.csv", "annual": "data/ppmt/ppmt_expense_structure_annual.csv", "halfyear": "data/ppmt/ppmt_expense_structure_halfyear.csv" }, "height": 700, "filename": "泡泡玛特费用结构与费用率" }, + "ppmt-channel-revenue": { "key": "ppmt-channel-revenue", "title": "渠道收入结构与增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_revenue.csv", "annual": "data/ppmt/ppmt_channel_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_channel_revenue_halfyear.csv" }, "height": 720, "filename": "泡泡玛特渠道收入结构与增速" } }, "tabs": [ { "id": "overview", "name": "概览", "chartRefs": ["ppmt-growth", "ppmt-profit", "ppmt-ip-share", "ppmt-region-channel"] }, diff --git a/package.json b/package.json index 8826a1f..94e0a88 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,5 @@ "echarts": "5.5.1", "vite": "^8.2.2" }, - "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be" + "packageManager": "pnpm@11.24.0" } diff --git a/scripts/normalize_ppmt_financials.mjs b/scripts/normalize_ppmt_financials.mjs index 4250da2..2c05e84 100644 --- a/scripts/normalize_ppmt_financials.mjs +++ b/scripts/normalize_ppmt_financials.mjs @@ -258,3 +258,389 @@ writeCSV('ppmt_channel_revenue.csv', [ ], channelRows); console.log(`已生成现金转换 ${cashConversionRows.length}、流动性 ${liquidityRows.length}、现金流 ${cashflowCompositionRows.length}、营运资本 ${workingCapitalDaysRows.length}、费用 ${expenseRows.length}、渠道 ${channelRows.length} 行数据。`); + +// 多维度图表:FY 为全年,H1/H2 为独立半年。所有拆分和同比均在数据层完成。 +const VIEW_YEARS = Array.from({ length: 8 }, (_, index) => 2019 + index); +const OUTPUT_START_YEAR = 2020; +const HUNDRED_MILLION = 100000000; + +function isComplete(values) { + return values && Object.values(values).every((value) => Number.isFinite(value)); +} + +function subtractPeriod(fullYear, firstHalf) { + if (!isComplete(fullYear) || !isComplete(firstHalf)) return null; + return Object.fromEntries(Object.keys(fullYear).map((key) => [key, fullYear[key] - firstHalf[key]])); +} + +function buildViews(readPeriod) { + const views = { all: [], annual: [], halfyear: [] }; + VIEW_YEARS.forEach((year) => { + const h1 = readPeriod(year, 'H1'); + const fy = readPeriod(year, 'FY'); + const h2 = subtractPeriod(fy, h1); + if (h1) { + const row = { period: `${year}H1`, ...h1 }; + views.halfyear.push(row); + views.all.push(row); + } + if (h2) { + const row = { period: `${year}H2`, ...h2 }; + views.halfyear.push(row); + views.all.push(row); + } + if (fy) { + const row = { period: `${year}FY`, ...fy }; + views.annual.push(row); + views.all.push(row); + } + }); + return views; +} + +function addPeriods(firstHalf, secondHalf) { + if (!isComplete(firstHalf) || !isComplete(secondHalf)) return null; + return Object.fromEntries(Object.keys(firstHalf).map((key) => [key, firstHalf[key] + secondHalf[key]])); +} + +// IP、品类等运营数据的 H2 已是单独半年,年度值应由 H1 + H2 汇总。 +function buildViewsFromHalfYears(readPeriod) { + const views = { all: [], annual: [], halfyear: [] }; + VIEW_YEARS.forEach((year) => { + const h1 = readPeriod(year, 'H1'); + const h2 = readPeriod(year, 'H2'); + const fy = addPeriods(h1, h2); + if (h1) { + const row = { period: `${year}H1`, ...h1 }; + views.halfyear.push(row); + views.all.push(row); + } + if (h2) { + const row = { period: `${year}H2`, ...h2 }; + views.halfyear.push(row); + views.all.push(row); + } + if (fy) { + const row = { period: `${year}FY`, ...fy }; + views.annual.push(row); + views.all.push(row); + } + }); + return views; +} + +function yoyFor(rows, key, outputKey = key) { + const byPeriod = new Map(rows.map((row) => [row.period, row])); + return rows.map((row) => { + const year = Number(row.period.slice(0, 4)); + const prior = byPeriod.get(`${year - 1}${row.period.slice(4)}`); + return { + ...row, + [outputKey]: prior && prior[key] !== 0 ? (row[key] / prior[key] - 1) * 100 : null, + }; + }); +} + +function toOutputRows(rows, headers, digits = {}) { + return rows + .filter((row) => Number(row.period.slice(0, 4)) >= OUTPUT_START_YEAR) + .map((row) => headers.map((header) => { + if (header === 'period') return row.period; + const value = row[header]; + return Number.isFinite(value) ? value.toFixed(digits[header] ?? 2) : ''; + })); +} + +function writeViewFiles(base, headers, views, digits) { + const filenames = { + all: `${base}.csv`, + annual: `${base}_annual.csv`, + halfyear: `${base}_halfyear.csv`, + }; + Object.entries(filenames).forEach(([view, filename]) => { + writeCSV(filename, headers, toOutputRows(views[view], headers, digits)); + }); +} + +const incomeDateIndex = new Map(income.dates.map((date, index) => [date, index])); +const incomeColumns = { + revenue: row(income, '一、营业总收入'), + cost: row(income, '营业成本'), + sales_expense: row(income, '减:销售费用'), + admin_expense: row(income, '管理费用'), + net_profit: row(income, '三、净利润'), + non_gaap_net_profit: row(income, '非国际公认会计准则净利润'), +}; + +function incomePeriod(year, type) { + const date = `${year}-${type === 'H1' ? '06-30' : '12-31'}`; + const index = incomeDateIndex.get(date); + if (index === undefined) return null; + const result = Object.fromEntries(Object.entries(incomeColumns).map(([key, values]) => [key, values[index]])); + // 2020 年非 GAAP 净利未披露,沿用现有图表的归母净利替代口径。 + result.non_gaap_net_profit ??= result.net_profit; + return isComplete(result) ? result : null; +} + +const cashflowDateIndex = new Map(cashflow.dates.map((date, index) => [date, index])); +const cashflowColumns = { + operating_cash_flow: row(cashflow, '经营活动产生的现金流量净额'), + investing_cash_flow: row(cashflow, '投资活动产生的现金流量净额'), + financing_cash_flow: row(cashflow, '筹资活动产生的现金流量净额'), + net_cash_increase: row(cashflow, '现金及现金等价物的净增加额'), +}; + +function cashflowPeriod(year, type) { + const date = `${year}-${type === 'H1' ? '06-30' : '12-31'}`; + const index = cashflowDateIndex.get(date); + if (index === undefined) return null; + const result = Object.fromEntries(Object.entries(cashflowColumns).map(([key, values]) => [key, values[index]])); + return isComplete(result) ? result : null; +} + +function inBillion(values) { + return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, value / HUNDRED_MILLION])); +} + +const growthViews = buildViews((year, type) => { + const values = incomePeriod(year, type); + return values && { + non_gaap_net_profit: values.non_gaap_net_profit, + gross_profit: values.revenue - values.cost, + revenue: values.revenue, + cost: values.cost, + }; +}); +Object.keys(growthViews).forEach((view) => { + ['non_gaap_net_profit', 'gross_profit', 'revenue', 'cost'].forEach((key) => { + growthViews[view] = yoyFor(growthViews[view], key); + }); +}); +writeViewFiles('ppmt_growth', ['period', 'non_gaap_net_profit', 'gross_profit', 'revenue', 'cost'], growthViews, { + non_gaap_net_profit: 1, gross_profit: 1, revenue: 1, cost: 1, +}); + +const coreProfitViews = buildViews((year, type) => { + const values = incomePeriod(year, type); + return values && { core_profit: (values.revenue - values.cost - values.sales_expense - values.admin_expense) / HUNDRED_MILLION }; +}); +Object.keys(coreProfitViews).forEach((view) => { + coreProfitViews[view] = yoyFor(coreProfitViews[view], 'core_profit', 'yoy'); +}); +writeViewFiles('ppmt_core_profit', ['period', 'core_profit', 'yoy'], coreProfitViews, { core_profit: 2, yoy: 1 }); + +const marginAmountViews = buildViews((year, type) => { + const values = incomePeriod(year, type); + return values && { + revenue: values.revenue, + gross_profit: values.revenue - values.cost, + sales_expense: values.sales_expense, + admin_expense: values.admin_expense, + non_gaap_net_profit: values.non_gaap_net_profit, + }; +}); +const marginsViews = Object.fromEntries(Object.entries(marginAmountViews).map(([view, rows]) => [ + view, + rows.map((values) => { + if (values.revenue === 0) return values; + return { + period: values.period, + gross_margin: values.gross_profit / values.revenue * 100, + sales_expense_rate: values.sales_expense / values.revenue * 100, + admin_expense_rate: values.admin_expense / values.revenue * 100, + non_gaap_net_margin: values.non_gaap_net_profit / values.revenue * 100, + }; + }), +])); +writeViewFiles('ppmt_margins', ['period', 'gross_margin', 'sales_expense_rate', 'admin_expense_rate', 'non_gaap_net_margin'], marginsViews, { + gross_margin: 2, sales_expense_rate: 2, admin_expense_rate: 2, non_gaap_net_margin: 2, +}); + +const cashConversionViews = buildViews((year, type) => { + const flow = cashflowPeriod(year, type); + const incomeValues = incomePeriod(year, type); + if (!flow || !incomeValues) return null; + return inBillion({ + operating_cash_flow: flow.operating_cash_flow, + net_profit: incomeValues.net_profit, + }); +}); +Object.values(cashConversionViews).forEach((rows) => rows.forEach((row) => { + row.cash_conversion = row.net_profit === 0 ? null : row.operating_cash_flow / row.net_profit * 100; +})); +writeViewFiles('ppmt_cash_conversion', ['period', 'operating_cash_flow', 'net_profit', 'cash_conversion'], cashConversionViews, { + operating_cash_flow: 2, net_profit: 2, cash_conversion: 1, +}); + +const cashflowCompositionViews = buildViews((year, type) => { + const values = cashflowPeriod(year, type); + return values && inBillion(values); +}); +writeViewFiles('ppmt_cashflow_composition', ['period', 'operating_cash_flow', 'investing_cash_flow', 'financing_cash_flow', 'net_cash_increase'], cashflowCompositionViews, { + operating_cash_flow: 2, investing_cash_flow: 2, financing_cash_flow: 2, net_cash_increase: 2, +}); + +const channelKeysForViews = [ + ['零售店銷售收益', 'retail'], + ['線上銷售收益', 'online'], + ['機器人商店銷售收益', 'robot_store'], + ['批發收益', 'wholesale'], +]; +const revenueDateIndex = new Map(revenueComposition.dates.map((date, index) => [date, index])); +function channelPeriod(year, type) { + const index = revenueDateIndex.get(`${year}-Q${type === 'H1' ? 2 : 4}`); + if (index === undefined) return null; + const values = Object.fromEntries(channelKeysForViews.map(([name, key]) => [ + key, revenueComposition.channels.get(`${name}:收入`)?.[index] ?? null, + ])); + const total = revenueComposition.channels.get('totalRevenue')?.[index] ?? null; + return isComplete({ ...values, total }) ? inBillion({ ...values, total_revenue: total }) : null; +} + +const channelRevenueViews = buildViews(channelPeriod); +Object.keys(channelRevenueViews).forEach((view) => { + channelRevenueViews[view] = yoyFor(channelRevenueViews[view], 'total_revenue', 'revenue_yoy'); +}); +writeViewFiles('ppmt_channel_revenue', ['period', 'retail', 'online', 'robot_store', 'wholesale', 'total_revenue', 'revenue_yoy'], channelRevenueViews, { + retail: 2, online: 2, robot_store: 2, wholesale: 2, total_revenue: 2, revenue_yoy: 1, +}); + +console.log('已生成首批六图的全部、年度、半年 CSV。'); + +const profitViews = buildViews((year, type) => { + const values = incomePeriod(year, type); + return values && inBillion({ + revenue: values.revenue, + gross_profit: values.revenue - values.cost, + net_profit: values.net_profit, + }); +}); +Object.values(profitViews).forEach((rows) => rows.forEach((item) => { + item.gross_margin = item.revenue === 0 ? null : item.gross_profit / item.revenue * 100; +})); +writeViewFiles('ppmt_profit', ['period', 'revenue', 'gross_profit', 'gross_margin', 'net_profit'], profitViews, { + revenue: 2, gross_profit: 2, gross_margin: 2, net_profit: 2, +}); + +const coreMarginViews = buildViews((year, type) => { + const values = incomePeriod(year, type); + return values && { + revenue: values.revenue, + core_profit: values.revenue - values.cost - values.sales_expense - values.admin_expense, + }; +}); +Object.values(coreMarginViews).forEach((rows) => rows.forEach((item) => { + item.core_margin = item.revenue === 0 ? null : item.core_profit / item.revenue * 100; +})); +writeViewFiles('ppmt_core_margin', ['period', 'core_margin'], coreMarginViews, { core_margin: 1 }); + +const expenseAmountViews = buildViews((year, type) => { + const values = incomePeriod(year, type); + if (!values) return null; + const financialExpense = row(income, '财务费用')[incomeDateIndex.get(`${year}-${type === 'H1' ? '06-30' : '12-31'}`)]; + return { + revenue: values.revenue, + sales_expense: values.sales_expense, + admin_expense: values.admin_expense, + financial_expense: financialExpense, + }; +}); +const expenseViews = Object.fromEntries(Object.entries(expenseAmountViews).map(([view, rows]) => [ + view, + rows.map((item) => ({ + ...inBillion(item), + period: item.period, + four_expense_rate: (item.sales_expense + item.admin_expense + item.financial_expense) / item.revenue * 100, + })), +])); +writeViewFiles('ppmt_expense_structure', ['period', 'sales_expense', 'admin_expense', 'financial_expense', 'four_expense_rate'], expenseViews, { + sales_expense: 2, admin_expense: 2, financial_expense: 2, four_expense_rate: 1, +}); + +function readRuntimeRows(filename) { + const [header, ...lines] = fs.readFileSync(path.join(OUTPUT_DIR, filename), 'utf8').trim().split(/\r?\n/); + const headers = header.split(','); + return lines.map((line) => Object.fromEntries(line.split(',').map((cell, index) => { + const value = Number(cell); + return [headers[index], cell !== '' && Number.isFinite(value) ? value : cell]; + }))); +} + +function writeRuntimeViews(base, headers, views, digits = {}) { + writeViewFiles(base, headers, views, digits); +} + +function buildWideIpViews(filename) { + const rows = readRuntimeRows(filename); + const id = Object.keys(rows[0])[0]; + const sourcePeriods = Object.keys(rows[0]).slice(1).filter((period) => /H[12]$/.test(period)); + const years = [...new Set(sourcePeriods.map((period) => period.slice(0, 4)))]; + const halfyear = sourcePeriods; + const annual = years.filter((year) => sourcePeriods.includes(`${year}H1`) && sourcePeriods.includes(`${year}H2`)).map((year) => `${year}FY`); + const all = years.flatMap((year) => [ + ...(sourcePeriods.includes(`${year}H1`) ? [`${year}H1`] : []), + ...(sourcePeriods.includes(`${year}H2`) ? [`${year}H2`, `${year}FY`] : []), + ]); + const valuesFor = (periods) => rows.map((row) => [row[id], ...periods.map((period) => { + if (!period.endsWith('FY')) return row[period]; + return row[`${period.slice(0, 4)}H1`] + row[`${period.slice(0, 4)}H2`]; + })]); + writeCSV(filename, [id, ...all], valuesFor(all)); + writeCSV(filename.replace('.csv', '_annual.csv'), [id, ...annual], valuesFor(annual)); + writeCSV(filename.replace('.csv', '_halfyear.csv'), [id, ...halfyear], valuesFor(halfyear)); +} + +buildWideIpViews('ppmt_ip.csv'); +buildWideIpViews('ppmt_ip_share_history.csv'); + +const ipExMonstersRows = readRuntimeRows('ppmt_ip_ex_monsters.csv'); +function ipExMonstersPeriod(year, type) { + const row = ipExMonstersRows.find((item) => item.period === `${year}${type}`); + return row ? { revenue: row.revenue } : null; +} +const ipExMonstersViews = buildViewsFromHalfYears(ipExMonstersPeriod); +Object.keys(ipExMonstersViews).forEach((view) => { + ipExMonstersViews[view] = yoyFor(ipExMonstersViews[view], 'revenue', 'yoy'); +}); +writeRuntimeViews('ppmt_ip_ex_monsters', ['period', 'revenue', 'yoy'], ipExMonstersViews, { revenue: 2, yoy: 1 }); + +const categoryRows = readRuntimeRows('ppmt_category.csv').filter((item) => item.period !== '品类'); +function categoryPeriod(year, type) { + const row = categoryRows.find((item) => item.period === `${year}${type}`); + return row && { plush: row.plush, figure: row.figure, mega: row.mega, other: row.other }; +} +const categoryViews = buildViewsFromHalfYears(categoryPeriod); +writeRuntimeViews('ppmt_category', ['period', 'plush', 'figure', 'mega', 'other'], categoryViews, { + plush: 2, figure: 2, mega: 2, other: 2, +}); + +// 资产负债表为时点指标:H2 是年末时点,而非“全年累计”。 +function buildSnapshotViews(base, headers, digits) { + const source = readRuntimeRows(`${base}.csv`); + const views = { all: [], annual: [], halfyear: [] }; + VIEW_YEARS.forEach((year) => { + const h1 = source.find((row) => row.period === `${year}H1`); + const yearEnd = source.find((row) => row.period === `${year}H2` || row.period === `${year}FY`); + if (h1) { + const row = { ...h1, period: `${year}H1` }; + views.all.push(row); + views.halfyear.push(row); + } + if (yearEnd) { + const halfyearRow = { ...yearEnd, period: `${year}H2` }; + views.all.push(halfyearRow); + views.halfyear.push(halfyearRow); + views.annual.push({ ...yearEnd, period: `${year}FY` }); + } + }); + writeRuntimeViews(base, headers, views, digits); +} + +buildSnapshotViews('ppmt_assets', ['period', 'total_assets', 'cash_like', 'receivables', 'investment', 'operating_assets'], { + total_assets: 2, cash_like: 2, receivables: 2, investment: 2, operating_assets: 2, +}); +buildSnapshotViews('ppmt_liquidity_working_capital', ['period', 'cash', 'total_liabilities', 'net_working_capital'], { + cash: 2, total_liabilities: 2, net_working_capital: 2, +}); + +console.log('已生成第二批连续图表的全部、年度、半年 CSV。'); diff --git a/test/regress.mjs b/test/regress.mjs index c08dfcd..e9e95ad 100644 --- a/test/regress.mjs +++ b/test/regress.mjs @@ -70,7 +70,7 @@ for (const entry of targets) { if (!csvPath.startsWith(`${ROOT}${path.sep}`)) throw new Error(`CSV 路径越过项目根目录:${entry.csv}`); const rows = parseCSV(fs.readFileSync(csvPath, 'utf8')); const chart = echarts.init(null, null, { renderer: 'svg', ssr: true, width: 1100, height: 720 }); - const option = getChartBuilder(entry.key)(rows, { width: 1100, height: 720 }); + const option = getChartBuilder(entry.key)(rows, { width: 1100, height: 720, view: entry.view || undefined }); chart.setOption(option); const svg = chart.renderToSVGString(); chart.dispose();