PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.10.1
Independent Analytics – WordPress Analytics Plugin v2.10.1
2.15.5 2.15.4 2.15.3 2.15.2 2.15.1 2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 1.19.1 1.2 1.20.0 1.21.0 1.22.0 1.22.1 1.23.0 1.23.1 1.24.0 1.24.1 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.28.1 1.28.2 1.28.3 1.29.0 1.3 1.30.0 1.30.1 1.4 1.5 1.6 1.7 1.8 1.9 2.0.0 2.0.1 2.1.4 2.1.5 2.1.6 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.10 2.11.2 2.11.3 2.11.4 2.11.5 2.11.6 2.11.7 2.11.8 2.11.9 2.12.0 2.12.1 2.12.2 2.13.1 2.13.2 2.13.5 2.13.6 2.14.0 2.14.1 2.14.2 2.14.4 2.14.6 2.14.7 2.14.8 2.14.9 2.2.0 2.2.1 2.3.1 2.3.2 2.4.2 2.4.3 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.7.1 2.7.2 2.7.3 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7
independent-analytics / IAWP / Statistics / Statistics.php
independent-analytics / IAWP / Statistics Last commit date
Intervals 1 year ago Campaign_Statistics.php 2 years ago City_Statistics.php 2 years ago Click_Statistics.php 1 year ago Country_Statistics.php 2 years ago Device_Browser_Statistics.php 2 years ago Device_OS_Statistics.php 2 years ago Device_Type_Statistics.php 2 years ago Page_Statistics.php 2 years ago Referrer_Statistics.php 2 years ago Statistic.php 1 year ago Statistics.php 1 year ago
Statistics.php
374 lines
1 <?php
2
3 namespace IAWP\Statistics;
4
5 use DateInterval;
6 use DatePeriod;
7 use DateTime;
8 use IAWP\Date_Range\Date_Range;
9 use IAWP\Form_Submissions\Form;
10 use IAWP\Illuminate_Builder;
11 use IAWP\Plugin_Group;
12 use IAWP\Query;
13 use IAWP\Query_Taps;
14 use IAWP\Rows\Rows;
15 use IAWP\Statistics\Intervals\Interval;
16 use IAWP\Statistics\Intervals\Intervals;
17 use IAWP\Tables;
18 use IAWP\Utils\Calculations;
19 use IAWP\Utils\CSV;
20 use IAWP\Utils\Timezone;
21 use IAWP\Utils\WordPress_Site_Date_Format_Pattern;
22 use IAWPSCOPED\Illuminate\Database\Query\Builder;
23 use IAWPSCOPED\Illuminate\Database\Query\JoinClause;
24 use IAWPSCOPED\Illuminate\Support\Collection;
25 use IAWPSCOPED\Illuminate\Support\Str;
26 use Throwable;
27 /** @internal */
28 abstract class Statistics
29 {
30 protected $tables = Tables::class;
31 protected $date_range;
32 protected $rows;
33 protected $chart_interval;
34 private $statistics;
35 private $previous_period_statistics;
36 private $statistics_grouped_by_date_interval;
37 private $unfiltered_statistics;
38 private $previous_period_unfiltered_statistic;
39 private $unfiltered_statistics_grouped_by_date_interval;
40 private $statistic_instances;
41 // The biggest flaw here is that it requires two queries for the stats (current and previous) when
42 // that could be one. I think it would also be possible to reuse the rows query and not limit
43 // by 50 and just SUM() up all the stats columns for the quick stats. Maybe that would be faster
44 // even if two queries were still used. Needs testing.
45 public function __construct(Date_Range $date_range, ?Rows $rows = null, ?Interval $chart_interval = null)
46 {
47 $this->date_range = $date_range;
48 $this->rows = $rows;
49 $this->chart_interval = $chart_interval ?? Intervals::default_for($date_range->number_of_days());
50 if (\is_null($rows)) {
51 $this->statistics = $this->query($this->date_range);
52 $this->previous_period_statistics = $this->query($this->date_range->previous_period());
53 $this->statistics_grouped_by_date_interval = $this->query($this->date_range, null, \true);
54 } else {
55 $this->statistics = $this->query($this->date_range, $rows);
56 $this->previous_period_statistics = $this->query($this->date_range->previous_period(), $rows);
57 $this->statistics_grouped_by_date_interval = $this->query($this->date_range, $rows, \true);
58 $this->unfiltered_statistics = $this->query($this->date_range);
59 $this->previous_period_unfiltered_statistic = $this->query($this->date_range->previous_period());
60 $this->unfiltered_statistics_grouped_by_date_interval = $this->query($this->date_range, null, \true);
61 }
62 $this->statistic_instances = $this->make_statistic_instances();
63 $this->statistic_instances = \array_filter($this->statistic_instances, function (\IAWP\Statistics\Statistic $statistic) {
64 return $statistic->is_enabled();
65 });
66 }
67 /**
68 * @return Statistic[]
69 */
70 public function get_statistics() : array
71 {
72 return $this->statistic_instances;
73 }
74 public function get_grouped_statistics()
75 {
76 // This whole thing is a bit of a mess...
77 return Collection::make($this->statistic_instances)->groupBy(function (\IAWP\Statistics\Statistic $item, int $key) {
78 return Plugin_Group::get_plugin_group($item->plugin_group())->name();
79 })->map(function (Collection $group, $plugin_group) {
80 $items = $group->map(function (\IAWP\Statistics\Statistic $item) {
81 if (!$item->is_group_plugin_enabled()) {
82 return null;
83 }
84 return ['id' => $item->id(), 'name' => $item->name()];
85 })->filter();
86 if ($items->isEmpty()) {
87 return null;
88 }
89 return ['name' => $plugin_group, 'items' => $items->toArray()];
90 })->filter()->values()->toArray();
91 }
92 public function get_statistic(string $statistic_id) : ?\IAWP\Statistics\Statistic
93 {
94 foreach ($this->statistic_instances as $statistic_instance) {
95 if ($statistic_instance->id() == $statistic_id) {
96 return $statistic_instance;
97 }
98 }
99 return null;
100 }
101 public function has_filters() : bool
102 {
103 return !\is_null($this->rows);
104 }
105 public function chart_interval() : Interval
106 {
107 return $this->chart_interval;
108 }
109 /**
110 * I'm sure there's more we could do here. If you get a result back where there isn't a full
111 * page of results or where you're not paginating, then you can just count up the rows...
112 *
113 * @return int|null
114 */
115 public function total_number_of_rows() : ?int
116 {
117 $sessions_table = Query::get_table_name(Query::SESSIONS);
118 $views_table = Query::get_table_name(Query::VIEWS);
119 $column = $this->total_table_rows_column() ?? $this->required_column();
120 $query = Illuminate_Builder::new()->selectRaw("COUNT(DISTINCT {$column}) AS total_table_rows")->from("{$sessions_table} AS sessions")->join("{$views_table} AS views", function (JoinClause $join) {
121 $join->on('sessions.session_id', '=', 'views.session_id');
122 })->tap(Query_Taps::tap_authored_content_check(\true))->when(!\is_null($this->rows), function (Builder $query) {
123 $this->rows->attach_filters($query);
124 })->whereBetween('sessions.created_at', [$this->date_range->iso_start(), $this->date_range->iso_end()])->whereBetween('views.viewed_at', [$this->date_range->iso_start(), $this->date_range->iso_end()]);
125 return $query->value('total_table_rows');
126 }
127 public function get_statistics_as_csv() : CSV
128 {
129 $statistics = \array_values(\array_filter($this->get_statistics(), function (\IAWP\Statistics\Statistic $statistic) {
130 return $statistic->is_visible() && $statistic->is_enabled() && $statistic->is_group_plugin_enabled();
131 }));
132 $csv_header = [];
133 $csv_rows = [];
134 // The first column is the date interval label. There's no title for this column.
135 $csv_header[] = '';
136 foreach ($statistics as $statistic) {
137 $csv_header[] = $statistic->name();
138 }
139 $total_rows = \count($statistics[0]->statistic_over_time());
140 for ($i = 0; $i < $total_rows; $i++) {
141 $csv_row = [];
142 // The first row is the date interval label
143 $csv_row[] = $statistics[0]->statistic_over_time()[$i][0]->format(WordPress_Site_Date_Format_Pattern::for_php());
144 foreach ($statistics as $statistic) {
145 $csv_row[] = $statistic->format_value($statistic->statistic_over_time()[$i][1]);
146 }
147 $csv_rows[] = $csv_row;
148 }
149 // Use the summary row to show statistic totals
150 $summary_row = ['Total'];
151 foreach ($statistics as $index => $statistic) {
152 $summary_row[$index + 1] = $statistic->formatted_value();
153 }
154 // Add summary row to the top
155 \array_unshift($csv_rows, $summary_row);
156 $csv = new CSV($csv_header, $csv_rows);
157 return $csv;
158 }
159 /**
160 * Define which id column to use to count up the total table rows. This is only required
161 * for classes that don't have a required column and don't override required_column
162 *
163 * @return string|null
164 */
165 protected function total_table_rows_column() : ?string
166 {
167 return null;
168 }
169 /**
170 * Statistics can require that a column exists in order to be included. As an example, geos
171 * requires visitors.country_code and campaigns requires sessions.campaign_id
172 *
173 * @return string|null
174 */
175 protected function required_column() : ?string
176 {
177 return null;
178 }
179 /**
180 * @return Statistic[]
181 */
182 protected function make_statistic_instances() : array
183 {
184 $statistics = [$this->make_statistic(['id' => 'visitors', 'name' => \__('Visitors', 'independent-analytics'), 'plugin_group' => 'general', 'is_visible_in_dashboard_widget' => \true]), $this->make_statistic(['id' => 'views', 'name' => \__('Views', 'independent-analytics'), 'plugin_group' => 'general', 'is_visible_in_dashboard_widget' => \true]), $this->make_statistic(['id' => 'sessions', 'name' => \__('Sessions', 'independent-analytics'), 'plugin_group' => 'general']), $this->make_statistic(['id' => 'average_session_duration', 'name' => \__('Average Session Duration', 'independent-analytics'), 'plugin_group' => 'general', 'format' => 'time']), $this->make_statistic(['id' => 'bounce_rate', 'name' => \__('Bounce Rate', 'independent-analytics'), 'plugin_group' => 'general', 'format' => 'percent', 'is_growth_good' => \false, 'compute' => function (object $statistics) {
185 return Calculations::percentage($statistics->bounces, $statistics->sessions);
186 }]), $this->make_statistic(['id' => 'views_per_session', 'name' => \__('Views Per Session', 'independent-analytics'), 'plugin_group' => 'general', 'format' => 'decimal', 'compute' => function (object $statistics) {
187 return Calculations::divide($statistics->total_views, $statistics->sessions, 2);
188 }]), $this->make_statistic(['id' => 'clicks', 'name' => \__('Clicks', 'independent-analytics'), 'plugin_group' => 'general', 'requires_pro' => \true]), $this->make_statistic(['id' => 'wc_orders', 'name' => \__('Orders', 'independent-analytics'), 'plugin_group' => 'ecommerce', 'icon' => $this->get_ecommerce_icon()]), $this->make_statistic(['id' => 'wc_gross_sales', 'name' => \__('Gross Sales', 'independent-analytics'), 'plugin_group' => 'ecommerce', 'icon' => $this->get_ecommerce_icon(), 'format' => 'rounded-currency']), $this->make_statistic(['id' => 'wc_refunds', 'name' => \__('Refunds', 'independent-analytics'), 'plugin_group' => 'ecommerce', 'icon' => $this->get_ecommerce_icon()]), $this->make_statistic(['id' => 'wc_refunded_amount', 'name' => \__('Refunded Amount', 'independent-analytics'), 'plugin_group' => 'ecommerce', 'icon' => $this->get_ecommerce_icon(), 'format' => 'rounded-currency']), $this->make_statistic(['id' => 'wc_net_sales', 'name' => \__('Total Sales', 'independent-analytics'), 'plugin_group' => 'ecommerce', 'icon' => $this->get_ecommerce_icon(), 'format' => 'rounded-currency']), $this->make_statistic(['id' => 'wc_conversion_rate', 'name' => \__('Conversion Rate', 'independent-analytics'), 'plugin_group' => 'ecommerce', 'icon' => $this->get_ecommerce_icon(), 'format' => 'percent']), $this->make_statistic(['id' => 'wc_earnings_per_visitor', 'name' => \__('Earnings Per Visitor', 'independent-analytics'), 'plugin_group' => 'ecommerce', 'icon' => $this->get_ecommerce_icon(), 'format' => 'currency']), $this->make_statistic(['id' => 'wc_average_order_volume', 'name' => \__('Average Order Volume', 'independent-analytics'), 'plugin_group' => 'ecommerce', 'icon' => $this->get_ecommerce_icon(), 'format' => 'rounded-currency']), $this->make_statistic(['id' => 'form_submissions', 'name' => \__('Form Submissions', 'independent-analytics'), 'plugin_group' => 'forms']), $this->make_statistic(['id' => 'form_conversion_rate', 'name' => \__('Form Conversion Rate', 'independent-analytics'), 'plugin_group' => 'forms', 'format' => 'percent', 'compute' => function (object $statistics) {
189 return Calculations::percentage($statistics->form_submissions, $statistics->visitors, 2);
190 }])];
191 foreach (Form::get_forms() as $form) {
192 if (!$form->is_plugin_active()) {
193 continue;
194 }
195 $statistics[] = $this->make_statistic(['id' => 'form_submissions_for_' . $form->id(), 'name' => \sprintf(\_x('%s Submissions', 'Title of the contact form', 'independent-analytics'), $form->title()), 'plugin_group' => 'forms', 'is_subgroup_plugin_active' => $form->is_plugin_active(), 'plugin_group_header' => $form->plugin_name(), 'icon' => $form->icon()]);
196 $statistics[] = $this->make_statistic(['id' => 'form_conversion_rate_for_' . $form->id(), 'name' => \sprintf(\_x('%s Conversion Rate', 'Title of the contact form', 'independent-analytics'), $form->title()), 'plugin_group' => 'forms', 'is_subgroup_plugin_active' => $form->is_plugin_active(), 'plugin_group_header' => $form->plugin_name(), 'icon' => $form->icon(), 'format' => 'percent', 'compute' => function (object $statistics) use($form) {
197 $form_submission_id = 'form_submissions_for_' . $form->id();
198 return Calculations::percentage($statistics->{$form_submission_id}, $statistics->visitors, 2);
199 }]);
200 }
201 return $statistics;
202 }
203 protected function make_statistic(array $attributes) : \IAWP\Statistics\Statistic
204 {
205 $statistic_id = $attributes['id'];
206 if (!\array_key_exists('compute', $attributes)) {
207 $attributes['compute'] = function ($statistics, $statistic_id) {
208 return $statistics->{$statistic_id};
209 };
210 }
211 $attributes['statistic'] = $attributes['compute']($this->statistics, $statistic_id);
212 $attributes['previous_period_statistic'] = $attributes['compute']($this->previous_period_statistics, $statistic_id);
213 $attributes['statistic_over_time'] = $this->fill_in_partial_day_range($this->statistics_grouped_by_date_interval, $attributes);
214 $attributes['unfiltered_statistic'] = $this->has_filters() ? $attributes['compute']($this->unfiltered_statistics, $statistic_id) : null;
215 return new \IAWP\Statistics\Statistic($attributes);
216 }
217 protected function query(Date_Range $range, ?Rows $rows = null, bool $is_grouped_by_date_interval = \false)
218 {
219 $utc_offset = Timezone::utc_offset();
220 $site_offset = Timezone::site_offset();
221 $sessions_table = Query::get_table_name(Query::SESSIONS);
222 $views_table = Query::get_table_name(Query::VIEWS);
223 $orders_table = Query::get_table_name(Query::ORDERS);
224 $form_submissions_table = Query::get_table_name(Query::FORM_SUBMISSIONS);
225 $form_submissions_query = Illuminate_Builder::new()->select(['form_id', 'session_id'])->selectRaw('COUNT(*) AS form_submissions')->from($form_submissions_table, 'form_submissions')->whereBetween('created_at', [$range->iso_start(), $range->iso_end()])->groupBy(['form_id', 'session_id']);
226 $session_statistics = Illuminate_Builder::new();
227 $session_statistics->select('sessions.session_id')->selectRaw('COUNT(DISTINCT views.id) AS views')->selectRaw('COUNT(DISTINCT clicks.click_id) AS clicks')->selectRaw('COUNT(DISTINCT orders.order_id) AS orders')->selectRaw('IFNULL(CAST(SUM(orders.total) AS SIGNED), 0) AS gross_sales')->selectRaw('IFNULL(CAST(SUM(orders.total_refunded) AS SIGNED), 0) AS total_refunded')->selectRaw('IFNULL(CAST(SUM(orders.total_refunds) AS SIGNED), 0) AS total_refunds')->selectRaw('IFNULL(CAST(SUM(orders.total - orders.total_refunded) AS SIGNED), 0) AS net_sales')->from("{$sessions_table} AS sessions")->join("{$views_table} AS views", function (JoinClause $join) {
228 $join->on('sessions.session_id', '=', 'views.session_id');
229 })->leftJoin("{$orders_table} AS orders", function (JoinClause $join) {
230 $join->on('views.id', '=', 'orders.initial_view_id')->where('orders.is_included_in_analytics', '=', \true);
231 })->leftJoin("{$this->tables::clicks()} AS clicks", function (JoinClause $join) {
232 $join->on('views.id', '=', 'clicks.view_id');
233 })->tap(Query_Taps::tap_authored_content_check(\true))->when(!\is_null($rows), function (Builder $query) use($rows) {
234 $rows->attach_filters($query);
235 })->whereBetween('sessions.created_at', [$range->iso_start(), $range->iso_end()])->whereBetween('views.viewed_at', [$range->iso_start(), $range->iso_end()])->groupBy('sessions.session_id')->when(!\is_null($this->required_column()), function (Builder $query) {
236 $query->whereNotNull($this->required_column());
237 });
238 $statistics = Illuminate_Builder::new();
239 $statistics->selectRaw('IFNULL(CAST(SUM(sessions.total_views) AS SIGNED), 0) AS total_views')->selectRaw('IFNULL(CAST(SUM(session_statistics.views) AS SIGNED), 0) AS views')->selectRaw('COUNT(DISTINCT sessions.visitor_id) AS visitors')->selectRaw('COUNT(DISTINCT sessions.session_id) AS sessions')->selectRaw('IFNULL(CAST(SUM(session_statistics.clicks) AS SIGNED), 0) AS clicks')->selectRaw('IFNULL(CAST(AVG(TIMESTAMPDIFF(SECOND, sessions.created_at, sessions.ended_at)) AS SIGNED), 0) AS average_session_duration')->selectRaw('COUNT(DISTINCT IF(sessions.final_view_id IS NULL, sessions.session_id, NULL)) AS bounces')->selectRaw('IFNULL(CAST(SUM(session_statistics.orders) AS SIGNED), 0) AS wc_orders')->selectRaw('IFNULL(CAST(SUM(session_statistics.gross_sales) AS SIGNED), 0) AS wc_gross_sales')->selectRaw('IFNULL(CAST(SUM(session_statistics.total_refunds) AS SIGNED), 0) AS wc_refunds')->selectRaw('IFNULL(CAST(SUM(session_statistics.total_refunded) AS SIGNED), 0) AS wc_refunded_amount')->selectRaw('IFNULL(CAST(SUM(session_statistics.net_sales) AS SIGNED), 0) AS wc_net_sales')->selectRaw('IFNULL(SUM(form_submissions.form_submissions), 0) AS form_submissions')->tap(function (Builder $query) {
240 foreach (Form::get_forms() as $form) {
241 $query->selectRaw('IFNULL(SUM(IF(form_submissions.form_id = ?, form_submissions.form_submissions, 0)), 0) AS ' . $form->submissions_column(), [$form->id()]);
242 }
243 })->from("{$sessions_table} AS sessions")->joinSub($session_statistics, 'session_statistics', function (JoinClause $join) {
244 $join->on('sessions.session_id', '=', 'session_statistics.session_id');
245 })->leftJoinSub($form_submissions_query, 'form_submissions', 'sessions.session_id', '=', 'form_submissions.session_id')->whereBetween('sessions.created_at', [$range->iso_start(), $range->iso_end()])->when($is_grouped_by_date_interval, function (Builder $query) use($utc_offset, $site_offset) {
246 if ($this->chart_interval->id() === 'daily') {
247 $query->selectRaw("DATE(CONVERT_TZ(sessions.created_at, '{$utc_offset}', '{$site_offset}')) AS date");
248 } elseif ($this->chart_interval->id() === 'monthly') {
249 $query->selectRaw("DATE_FORMAT(CONVERT_TZ(sessions.created_at, '{$utc_offset}', '{$site_offset}'), '%Y-%m-01 00:00:00') AS date");
250 } elseif ($this->chart_interval->id() === 'weekly') {
251 $day_of_week = \IAWPSCOPED\iawp()->get_option('iawp_dow', 0) + 1;
252 $query->selectRaw("\n IF (\n DAYOFWEEK(CONVERT_TZ(sessions.created_at, '{$utc_offset}', '{$site_offset}')) - {$day_of_week} < 0,\n DATE_FORMAT(SUBDATE(CONVERT_TZ(sessions.created_at, '{$utc_offset}', '{$site_offset}'), DAYOFWEEK(CONVERT_TZ(sessions.created_at, '{$utc_offset}', '{$site_offset}')) - {$day_of_week} + 7), '%Y-%m-%d 00:00:00'),\n DATE_FORMAT(SUBDATE(CONVERT_TZ(sessions.created_at, '{$utc_offset}', '{$site_offset}'), DAYOFWEEK(CONVERT_TZ(sessions.created_at, '{$utc_offset}', '{$site_offset}')) - {$day_of_week}), '%Y-%m-%d 00:00:00')\n ) AS date\n ");
253 } else {
254 $query->selectRaw("DATE_FORMAT(CONVERT_TZ(sessions.created_at, '{$utc_offset}', '{$site_offset}'), '%Y-%m-%d %H:00:00') AS date");
255 }
256 $query->groupByRaw("date");
257 });
258 $outer_query = Illuminate_Builder::new()->selectRaw('statistics.*')->selectRaw('IF(statistics.visitors = 0, 0, (statistics.wc_orders / statistics.visitors) * 100) AS wc_conversion_rate')->selectRaw('IF(statistics.visitors = 0, 0, (statistics.wc_gross_sales - statistics.wc_refunded_amount) / visitors) AS wc_earnings_per_visitor')->selectRaw('IF(statistics.wc_orders = 0, 0, ROUND(CAST(statistics.wc_gross_sales / statistics.wc_orders AS DECIMAL(10, 2)))) AS wc_average_order_volume')->fromSub($statistics, 'statistics');
259 $results = \array_map(function (object $statistic) : object {
260 return $this->clean_up_raw_statistic_row($statistic);
261 }, $outer_query->get()->all());
262 if (!$is_grouped_by_date_interval) {
263 return $results[0];
264 }
265 return $results;
266 }
267 protected function clean_up_raw_statistic_row(object $statistic) : object
268 {
269 if (\property_exists($statistic, 'wc_gross_sales')) {
270 $statistic->wc_gross_sales = \intval($statistic->wc_gross_sales);
271 }
272 if (\property_exists($statistic, 'wc_refunded_amount')) {
273 $statistic->wc_refunded_amount = \intval($statistic->wc_refunded_amount);
274 }
275 if (\property_exists($statistic, 'wc_net_sales')) {
276 $statistic->wc_net_sales = \intval($statistic->wc_net_sales);
277 }
278 if (\property_exists($statistic, 'wc_earnings_per_visitor')) {
279 $statistic->wc_earnings_per_visitor = \intval($statistic->wc_earnings_per_visitor);
280 }
281 foreach ($statistic as $key => $value) {
282 if (Str::startsWith($key, 'form_submissions')) {
283 $statistic->{$key} = \intval($value);
284 }
285 }
286 return $statistic;
287 }
288 /**
289 * @param array $partial_day_range
290 * @param array $attributes
291 *
292 * @return array
293 */
294 private function fill_in_partial_day_range(array $partial_day_range, array $attributes) : array
295 {
296 $original_start = (clone $this->date_range->start())->setTimezone(Timezone::site_timezone());
297 $start = $this->chart_interval->calculate_start_of_interval_for($original_start);
298 $original_end = (clone $this->date_range->end())->setTimezone(Timezone::site_timezone());
299 $end = $this->chart_interval->calculate_start_of_interval_for($original_end);
300 $end->add(new DateInterval('PT1S'));
301 $is_problematic_timezone = \in_array(Timezone::site_timezone()->getName(), ['Asia/Beirut', 'America/Santiago', 'America/Havana', 'America/Asuncion']);
302 // Imagine a scenario where the offset for the start date is -4 and the offset for the
303 // end date is -3. In that case, the DatePeriod is actually going to have its end date
304 // short an hour, causing a date we care about to be chopped off the end. This code makes
305 // sure that the end date is still the day after the last date we care about, so it
306 // can get chopped off without consequence.
307 if ($is_problematic_timezone) {
308 $offset_difference = $end->getOffset() - $start->getOffset();
309 $interval = DateInterval::createFromDateString($offset_difference . ' seconds');
310 $end->add($interval);
311 }
312 $date_range = new DatePeriod($start, $this->chart_interval->date_interval(), $end);
313 $filled_in_data = [];
314 foreach ($date_range as $date) {
315 // If the timezone switches at midnight, there will be a date where there is no midnight. It'll be something
316 // like 1am instead. All dates after the day that switches will also be at 1am even though they have a
317 // midnight. This alters those back to midnight, so they can be matched against correctly.
318 if ($is_problematic_timezone && $date->format('H:i:s') === "01:00:00") {
319 $date->setTime(0, 0, 0);
320 }
321 $stat = $this->get_statistic_for_date($partial_day_range, $date, $attributes);
322 $filled_in_data[] = [$date, $stat];
323 }
324 return $filled_in_data;
325 }
326 /**
327 * @param array $partial_day_range
328 * @param DateTime $datetime_to_match
329 * @param array $attributes
330 *
331 * @return float|int
332 */
333 private function get_statistic_for_date(array $partial_day_range, DateTime $datetime_to_match, array $attributes)
334 {
335 foreach ($partial_day_range as $day) {
336 $date = $day->date;
337 $value = $attributes['compute']($day, $attributes['id']);
338 try {
339 $datetime = new DateTime($date, Timezone::site_timezone());
340 } catch (Throwable $e) {
341 return 0;
342 }
343 // Intentionally using non-strict equality to see if two distinct DateTime objects represent the same time
344 if ($datetime == $datetime_to_match) {
345 if (\is_string($value)) {
346 if (\strpos($value, '.') !== \false) {
347 return \floatval($value);
348 } else {
349 return \intval($value);
350 }
351 }
352 return $value;
353 }
354 }
355 return 0;
356 }
357 private function get_ecommerce_icon() : ?string
358 {
359 if (\IAWPSCOPED\iawp()->is_woocommerce_support_enabled() && !\IAWPSCOPED\iawp()->is_surecart_support_enabled() && !\IAWPSCOPED\iawp()->is_edd_support_enabled() && !\IAWPSCOPED\iawp()->is_pmpro_support_enabled()) {
360 return 'woocommerce';
361 }
362 if (\IAWPSCOPED\iawp()->is_surecart_support_enabled() && !\IAWPSCOPED\iawp()->is_woocommerce_support_enabled() && !\IAWPSCOPED\iawp()->is_edd_support_enabled() && !\IAWPSCOPED\iawp()->is_pmpro_support_enabled()) {
363 return 'surecart';
364 }
365 if (\IAWPSCOPED\iawp()->is_edd_support_enabled() && !\IAWPSCOPED\iawp()->is_woocommerce_support_enabled() && !\IAWPSCOPED\iawp()->is_surecart_support_enabled() && !\IAWPSCOPED\iawp()->is_pmpro_support_enabled()) {
366 return 'edd';
367 }
368 if (\IAWPSCOPED\iawp()->is_pmpro_support_enabled() && !\IAWPSCOPED\iawp()->is_woocommerce_support_enabled() && !\IAWPSCOPED\iawp()->is_surecart_support_enabled() && !\IAWPSCOPED\iawp()->is_edd_support_enabled()) {
369 return 'pmpro';
370 }
371 return null;
372 }
373 }
374