PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.63
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.63
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / widgets / KPI_Tiles_Microcharts / KPI_Tiles_Microcharts.php

KPI_Tiles_Microcharts.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.63, at includes/widgets/KPI_Tiles_Microcharts/KPI_Tiles_Microcharts.php

1,751 lines 56.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * KPI Tiles with Microcharts Widget (Free).
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Group_Control_Background;
12 use Elementor\Group_Control_Border;
13 use Elementor\Group_Control_Box_Shadow;
14 use Elementor\Group_Control_Typography;
15 use Elementor\Plugin;
16 use Elementor\Repeater;
17 use Elementor\Widget_Base;
18
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * Renders KPI tiles with trend deltas and sparklines.
25 */
26 class KPI_Tiles_Microcharts extends Widget_Base
27 {
28 /**
29 * Get widget slug.
30 *
31 * @return string
32 */
33 public function get_name(): string
34 {
35 return 'king-addons-kpi-tiles-microcharts';
36 }
37
38 /**
39 * Get widget title.
40 *
41 * @return string
42 */
43 public function get_title(): string
44 {
45 return esc_html__('KPI Tiles with Microcharts', 'king-addons');
46 }
47
48 /**
49 * Get widget icon.
50 *
51 * @return string
52 */
53 public function get_icon(): string
54 {
55 return 'king-addons-icon king-addons-kpi-tiles-microcharts';
56 }
57
58 /**
59 * Enqueue script dependencies.
60 *
61 * @return array<int, string>
62 */
63 public function get_script_depends(): array
64 {
65 return [
66 KING_ADDONS_ASSETS_UNIQUE_KEY . '-kpi-tiles-microcharts-script',
67 ];
68 }
69
70 /**
71 * Enqueue style dependencies.
72 *
73 * @return array<int, string>
74 */
75 public function get_style_depends(): array
76 {
77 return [
78 KING_ADDONS_ASSETS_UNIQUE_KEY . '-kpi-tiles-microcharts-style',
79 ];
80 }
81
82 /**
83 * Get widget categories.
84 *
85 * @return array<int, string>
86 */
87 public function get_categories(): array
88 {
89 return ['king-addons'];
90 }
91
92 /**
93 * Get widget keywords.
94 *
95 * @return array<int, string>
96 */
97 public function get_keywords(): array
98 {
99 return ['kpi', 'tiles', 'microcharts', 'sparkline', 'metrics', 'dashboard', 'stats'];
100 }
101
102 public function get_custom_help_url()
103 {
104 return 'mailto:[email protected]?subject=Bug Report - King Addons&body=Please describe the issue';
105 }
106
107 /**
108 * Register widget controls.
109 *
110 * @return void
111 */
112 public function register_controls(): void
113 {
114 $this->register_content_controls();
115 $this->register_layout_controls();
116 $this->register_sparkline_controls();
117 $this->register_style_layout_controls();
118 $this->register_style_tile_controls();
119 $this->register_style_title_controls();
120 $this->register_style_value_controls();
121 $this->register_style_delta_controls();
122 $this->register_style_sparkline_controls();
123 $this->register_style_footer_controls();
124 $this->register_pro_notice_controls();
125 }
126
127 /**
128 * Render widget output.
129 *
130 * @return void
131 */
132 public function render(): void
133 {
134 $settings = $this->get_settings_for_display();
135 $this->render_output($settings);
136 }
137
138 /**
139 * Register content controls.
140 *
141 * @return void
142 */
143 protected function register_content_controls(): void
144 {
145 $this->start_controls_section(
146 'kng_kpi_tiles_section',
147 [
148 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('KPI Tiles', 'king-addons'),
149 'tab' => Controls_Manager::TAB_CONTENT,
150 ]
151 );
152
153 $repeater = new Repeater();
154
155 $repeater->add_control(
156 'kng_tile_title',
157 [
158 'label' => esc_html__('Title', 'king-addons'),
159 'type' => Controls_Manager::TEXT,
160 'default' => esc_html__('Revenue', 'king-addons'),
161 'label_block' => true,
162 ]
163 );
164
165 $repeater->add_control(
166 'kng_tile_value',
167 [
168 'label' => esc_html__('Value', 'king-addons'),
169 'type' => Controls_Manager::NUMBER,
170 'step' => 0.01,
171 'default' => 128000,
172 ]
173 );
174
175 $repeater->add_control(
176 'kng_tile_prefix',
177 [
178 'label' => esc_html__('Value Prefix', 'king-addons'),
179 'type' => Controls_Manager::TEXT,
180 'default' => '$',
181 ]
182 );
183
184 $repeater->add_control(
185 'kng_tile_suffix',
186 [
187 'label' => esc_html__('Value Suffix', 'king-addons'),
188 'type' => Controls_Manager::TEXT,
189 ]
190 );
191
192 $repeater->add_control(
193 'kng_tile_decimals',
194 [
195 'label' => esc_html__('Decimals', 'king-addons'),
196 'type' => Controls_Manager::NUMBER,
197 'min' => 0,
198 'max' => 4,
199 'step' => 1,
200 'default' => 0,
201 ]
202 );
203
204 $repeater->add_control(
205 'kng_tile_separator',
206 [
207 'label' => esc_html__('Thousands Separator', 'king-addons'),
208 'type' => Controls_Manager::SELECT,
209 'options' => [
210 'auto' => esc_html__('Auto', 'king-addons'),
211 'comma' => esc_html__('Comma', 'king-addons'),
212 'dot' => esc_html__('Dot', 'king-addons'),
213 'space' => esc_html__('Space', 'king-addons'),
214 ],
215 'default' => 'auto',
216 ]
217 );
218
219 $repeater->add_control(
220 'kng_tile_show_delta',
221 [
222 'label' => esc_html__('Show Delta', 'king-addons'),
223 'type' => Controls_Manager::SWITCHER,
224 'return_value' => 'yes',
225 'default' => 'yes',
226 'separator' => 'before',
227 ]
228 );
229
230 $repeater->add_control(
231 'kng_tile_trend',
232 [
233 'label' => esc_html__('Trend Direction', 'king-addons'),
234 'type' => Controls_Manager::SELECT,
235 'options' => [
236 'up' => esc_html__('Up', 'king-addons'),
237 'down' => esc_html__('Down', 'king-addons'),
238 'flat' => esc_html__('Flat', 'king-addons'),
239 ],
240 'default' => 'up',
241 'condition' => [
242 'kng_tile_show_delta' => 'yes',
243 ],
244 ]
245 );
246
247 $repeater->add_control(
248 'kng_tile_delta_type',
249 [
250 'label' => esc_html__('Delta Type', 'king-addons'),
251 'type' => Controls_Manager::SELECT,
252 'options' => [
253 'percent' => esc_html__('Percent', 'king-addons'),
254 'absolute' => esc_html__('Absolute', 'king-addons'),
255 ],
256 'default' => 'percent',
257 'condition' => [
258 'kng_tile_show_delta' => 'yes',
259 ],
260 ]
261 );
262
263 $repeater->add_control(
264 'kng_tile_delta_value',
265 [
266 'label' => esc_html__('Delta Value', 'king-addons'),
267 'type' => Controls_Manager::NUMBER,
268 'step' => 0.01,
269 'default' => 12,
270 'condition' => [
271 'kng_tile_show_delta' => 'yes',
272 ],
273 ]
274 );
275
276 $repeater->add_control(
277 'kng_tile_delta_decimals',
278 [
279 'label' => esc_html__('Delta Decimals', 'king-addons'),
280 'type' => Controls_Manager::NUMBER,
281 'min' => 0,
282 'max' => 4,
283 'step' => 1,
284 'default' => 0,
285 'condition' => [
286 'kng_tile_show_delta' => 'yes',
287 ],
288 ]
289 );
290
291 $repeater->add_control(
292 'kng_tile_delta_separator',
293 [
294 'label' => esc_html__('Delta Thousands Separator', 'king-addons'),
295 'type' => Controls_Manager::SELECT,
296 'options' => [
297 'auto' => esc_html__('Auto', 'king-addons'),
298 'comma' => esc_html__('Comma', 'king-addons'),
299 'dot' => esc_html__('Dot', 'king-addons'),
300 'space' => esc_html__('Space', 'king-addons'),
301 ],
302 'default' => 'auto',
303 'condition' => [
304 'kng_tile_show_delta' => 'yes',
305 ],
306 ]
307 );
308
309 $repeater->add_control(
310 'kng_tile_delta_prefix',
311 [
312 'label' => esc_html__('Delta Prefix', 'king-addons'),
313 'type' => Controls_Manager::TEXT,
314 'condition' => [
315 'kng_tile_show_delta' => 'yes',
316 ],
317 ]
318 );
319
320 $repeater->add_control(
321 'kng_tile_delta_suffix',
322 [
323 'label' => esc_html__('Delta Suffix', 'king-addons'),
324 'type' => Controls_Manager::TEXT,
325 'condition' => [
326 'kng_tile_show_delta' => 'yes',
327 ],
328 ]
329 );
330
331 $repeater->add_control(
332 'kng_tile_period',
333 [
334 'label' => esc_html__('Period Label', 'king-addons'),
335 'type' => Controls_Manager::TEXT,
336 'default' => esc_html__('vs last month', 'king-addons'),
337 'label_block' => true,
338 'condition' => [
339 'kng_tile_show_delta' => 'yes',
340 ],
341 ]
342 );
343
344 $repeater->add_control(
345 'kng_tile_show_sparkline',
346 [
347 'label' => esc_html__('Show Sparkline', 'king-addons'),
348 'type' => Controls_Manager::SWITCHER,
349 'return_value' => 'yes',
350 'default' => 'yes',
351 'separator' => 'before',
352 ]
353 );
354
355 $repeater->add_control(
356 'kng_tile_sparkline',
357 [
358 'label' => esc_html__('Sparkline Values', 'king-addons'),
359 'type' => Controls_Manager::TEXTAREA,
360 'rows' => 3,
361 'default' => '12, 14, 13, 18, 21, 19, 26',
362 'description' => esc_html__('Comma separated numbers. Min 2, max 50.', 'king-addons'),
363 'condition' => [
364 'kng_tile_show_sparkline' => 'yes',
365 ],
366 ]
367 );
368
369 $repeater->add_control(
370 'kng_tile_secondary',
371 [
372 'label' => esc_html__('Secondary Value', 'king-addons'),
373 'type' => Controls_Manager::TEXT,
374 'default' => esc_html__('ARR 1.2M', 'king-addons'),
375 ]
376 );
377
378 $repeater->add_control(
379 'kng_tile_badge',
380 [
381 'label' => esc_html__('Badge', 'king-addons'),
382 'type' => Controls_Manager::TEXT,
383 'default' => esc_html__('New', 'king-addons'),
384 ]
385 );
386
387 $repeater->add_control(
388 'kng_tile_link',
389 [
390 'label' => esc_html__('Tile Link', 'king-addons'),
391 'type' => Controls_Manager::URL,
392 'placeholder' => esc_html__('https://your-link.com', 'king-addons'),
393 ]
394 );
395
396 $this->add_control(
397 'kng_kpi_tiles',
398 [
399 'label' => esc_html__('Tiles', 'king-addons'),
400 'type' => Controls_Manager::REPEATER,
401 'fields' => $repeater->get_controls(),
402 'default' => [
403 [
404 'kng_tile_title' => esc_html__('Revenue', 'king-addons'),
405 'kng_tile_value' => 128000,
406 'kng_tile_prefix' => '$',
407 'kng_tile_suffix' => '',
408 'kng_tile_decimals' => 0,
409 'kng_tile_separator' => 'comma',
410 'kng_tile_show_delta' => 'yes',
411 'kng_tile_trend' => 'up',
412 'kng_tile_delta_type' => 'percent',
413 'kng_tile_delta_value' => 12,
414 'kng_tile_delta_decimals' => 0,
415 'kng_tile_delta_separator' => 'auto',
416 'kng_tile_delta_prefix' => '',
417 'kng_tile_delta_suffix' => '',
418 'kng_tile_period' => esc_html__('vs last month', 'king-addons'),
419 'kng_tile_show_sparkline' => 'yes',
420 'kng_tile_sparkline' => '12, 14, 13, 18, 21, 19, 26',
421 'kng_tile_secondary' => esc_html__('ARR 1.2M', 'king-addons'),
422 'kng_tile_badge' => esc_html__('New', 'king-addons'),
423 ],
424 [
425 'kng_tile_title' => esc_html__('Conversion', 'king-addons'),
426 'kng_tile_value' => 3.2,
427 'kng_tile_prefix' => '',
428 'kng_tile_suffix' => '%',
429 'kng_tile_decimals' => 1,
430 'kng_tile_separator' => 'auto',
431 'kng_tile_show_delta' => 'yes',
432 'kng_tile_trend' => 'up',
433 'kng_tile_delta_type' => 'percent',
434 'kng_tile_delta_value' => 0.4,
435 'kng_tile_delta_decimals' => 1,
436 'kng_tile_delta_separator' => 'auto',
437 'kng_tile_delta_prefix' => '',
438 'kng_tile_delta_suffix' => '',
439 'kng_tile_period' => esc_html__('vs last week', 'king-addons'),
440 'kng_tile_show_sparkline' => 'yes',
441 'kng_tile_sparkline' => '2.6, 2.9, 3.1, 2.8, 3.4, 3.2',
442 'kng_tile_secondary' => '',
443 'kng_tile_badge' => '',
444 ],
445 [
446 'kng_tile_title' => esc_html__('MRR', 'king-addons'),
447 'kng_tile_value' => 42000,
448 'kng_tile_prefix' => '$',
449 'kng_tile_suffix' => '',
450 'kng_tile_decimals' => 0,
451 'kng_tile_separator' => 'comma',
452 'kng_tile_show_delta' => 'yes',
453 'kng_tile_trend' => 'up',
454 'kng_tile_delta_type' => 'percent',
455 'kng_tile_delta_value' => 6,
456 'kng_tile_delta_decimals' => 0,
457 'kng_tile_delta_separator' => 'auto',
458 'kng_tile_delta_prefix' => '',
459 'kng_tile_delta_suffix' => '',
460 'kng_tile_period' => esc_html__('MoM', 'king-addons'),
461 'kng_tile_show_sparkline' => 'yes',
462 'kng_tile_sparkline' => '32, 34, 36, 38, 40, 42',
463 'kng_tile_secondary' => esc_html__('Goal 50k', 'king-addons'),
464 'kng_tile_badge' => '',
465 ],
466 [
467 'kng_tile_title' => esc_html__('NPS', 'king-addons'),
468 'kng_tile_value' => 58,
469 'kng_tile_prefix' => '',
470 'kng_tile_suffix' => '',
471 'kng_tile_decimals' => 0,
472 'kng_tile_separator' => 'auto',
473 'kng_tile_show_delta' => 'yes',
474 'kng_tile_trend' => 'flat',
475 'kng_tile_delta_type' => 'absolute',
476 'kng_tile_delta_value' => 0,
477 'kng_tile_delta_decimals' => 0,
478 'kng_tile_delta_separator' => 'auto',
479 'kng_tile_delta_prefix' => '',
480 'kng_tile_delta_suffix' => '',
481 'kng_tile_period' => esc_html__('QoQ', 'king-addons'),
482 'kng_tile_show_sparkline' => 'yes',
483 'kng_tile_sparkline' => '52, 54, 55, 56, 57, 58',
484 'kng_tile_secondary' => '',
485 'kng_tile_badge' => '',
486 ],
487 ],
488 'title_field' => '{{ kng_tile_title }}',
489 ]
490 );
491
492 $this->end_controls_section();
493 }
494
495 /**
496 * Register layout controls.
497 *
498 * @return void
499 */
500 protected function register_layout_controls(): void
501 {
502 $this->start_controls_section(
503 'kng_layout_section',
504 [
505 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Layout', 'king-addons'),
506 'tab' => Controls_Manager::TAB_CONTENT,
507 ]
508 );
509
510 $this->add_responsive_control(
511 'kng_columns',
512 [
513 'label' => esc_html__('Columns', 'king-addons'),
514 'type' => Controls_Manager::NUMBER,
515 'min' => 1,
516 'max' => 6,
517 'step' => 1,
518 'default' => 4,
519 'tablet_default' => 2,
520 'mobile_default' => 1,
521 'selectors' => [
522 '{{WRAPPER}} .king-addons-kpi-tiles__grid' => 'grid-template-columns: repeat({{VALUE}}, minmax(0, 1fr));',
523 ],
524 ]
525 );
526
527 $this->add_responsive_control(
528 'kng_grid_gap',
529 [
530 'label' => esc_html__('Gap', 'king-addons'),
531 'type' => Controls_Manager::SLIDER,
532 'size_units' => ['px'],
533 'range' => [
534 'px' => [
535 'min' => 0,
536 'max' => 80,
537 ],
538 ],
539 'default' => [
540 'size' => 20,
541 'unit' => 'px',
542 ],
543 'selectors' => [
544 '{{WRAPPER}} .king-addons-kpi-tiles__grid' => 'gap: {{SIZE}}{{UNIT}};',
545 ],
546 ]
547 );
548
549 $this->add_control(
550 'kng_equal_height',
551 [
552 'label' => esc_html__('Equal Height', 'king-addons'),
553 'type' => Controls_Manager::SWITCHER,
554 'return_value' => 'yes',
555 'default' => 'yes',
556 ]
557 );
558
559 $this->add_responsive_control(
560 'kng_tile_min_height',
561 [
562 'label' => esc_html__('Tile Min Height', 'king-addons'),
563 'type' => Controls_Manager::SLIDER,
564 'size_units' => ['px', '%'],
565 'range' => [
566 'px' => [
567 'min' => 0,
568 'max' => 600,
569 ],
570 '%' => [
571 'min' => 0,
572 'max' => 100,
573 ],
574 ],
575 'selectors' => [
576 '{{WRAPPER}} .king-addons-kpi-tiles__card' => 'min-height: {{SIZE}}{{UNIT}};',
577 ],
578 ]
579 );
580
581 $this->add_control(
582 'kng_content_alignment',
583 [
584 'label' => esc_html__('Content Alignment', 'king-addons'),
585 'type' => Controls_Manager::CHOOSE,
586 'options' => [
587 'left' => [
588 'title' => esc_html__('Left', 'king-addons'),
589 'icon' => 'eicon-text-align-left',
590 ],
591 'center' => [
592 'title' => esc_html__('Center', 'king-addons'),
593 'icon' => 'eicon-text-align-center',
594 ],
595 'right' => [
596 'title' => esc_html__('Right', 'king-addons'),
597 'icon' => 'eicon-text-align-right',
598 ],
599 ],
600 'toggle' => false,
601 'default' => 'left',
602 ]
603 );
604
605 $this->end_controls_section();
606 }
607
608 /**
609 * Register sparkline controls.
610 *
611 * @return void
612 */
613 protected function register_sparkline_controls(): void
614 {
615 $this->start_controls_section(
616 'kng_sparkline_section',
617 [
618 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Sparkline', 'king-addons'),
619 'tab' => Controls_Manager::TAB_CONTENT,
620 ]
621 );
622
623 $this->add_control(
624 'kng_sparkline_smooth',
625 [
626 'label' => esc_html__('Smoothing', 'king-addons'),
627 'type' => Controls_Manager::SWITCHER,
628 'return_value' => 'yes',
629 'default' => '',
630 ]
631 );
632
633 $this->add_control(
634 'kng_sparkline_last_dot',
635 [
636 'label' => esc_html__('Show Last Dot', 'king-addons'),
637 'type' => Controls_Manager::SWITCHER,
638 'return_value' => 'yes',
639 'default' => 'yes',
640 ]
641 );
642
643 $this->add_control(
644 'kng_sparkline_zero_baseline',
645 [
646 'label' => esc_html__('Zero Baseline', 'king-addons'),
647 'type' => Controls_Manager::SWITCHER,
648 'return_value' => 'yes',
649 'default' => '',
650 ]
651 );
652
653 $this->end_controls_section();
654 }
655
656 /**
657 * Register grid style controls.
658 *
659 * @return void
660 */
661 protected function register_style_layout_controls(): void
662 {
663 $this->start_controls_section(
664 'kng_style_layout_section',
665 [
666 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Wall / Grid', 'king-addons'),
667 'tab' => Controls_Manager::TAB_STYLE,
668 ]
669 );
670
671 $this->add_group_control(
672 Group_Control_Background::get_type(),
673 [
674 'name' => 'kng_grid_background',
675 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles',
676 ]
677 );
678
679 $this->add_responsive_control(
680 'kng_grid_padding',
681 [
682 'label' => esc_html__('Padding', 'king-addons'),
683 'type' => Controls_Manager::DIMENSIONS,
684 'size_units' => ['px', '%', 'em'],
685 'selectors' => [
686 '{{WRAPPER}} .king-addons-kpi-tiles' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
687 ],
688 ]
689 );
690
691 $this->end_controls_section();
692 }
693
694 /**
695 * Register tile style controls.
696 *
697 * @return void
698 */
699 protected function register_style_tile_controls(): void
700 {
701 $this->start_controls_section(
702 'kng_style_tile_section',
703 [
704 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Tile', 'king-addons'),
705 'tab' => Controls_Manager::TAB_STYLE,
706 ]
707 );
708
709 $this->add_group_control(
710 Group_Control_Background::get_type(),
711 [
712 'name' => 'kng_tile_background',
713 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__card',
714 ]
715 );
716
717 $this->add_group_control(
718 Group_Control_Border::get_type(),
719 [
720 'name' => 'kng_tile_border',
721 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__card',
722 ]
723 );
724
725 $this->add_control(
726 'kng_tile_radius',
727 [
728 'label' => esc_html__('Border Radius', 'king-addons'),
729 'type' => Controls_Manager::SLIDER,
730 'size_units' => ['px', '%'],
731 'range' => [
732 'px' => ['min' => 0, 'max' => 60],
733 '%' => ['min' => 0, 'max' => 100],
734 ],
735 'selectors' => [
736 '{{WRAPPER}} .king-addons-kpi-tiles__card' => 'border-radius: {{SIZE}}{{UNIT}};',
737 ],
738 ]
739 );
740
741 $this->add_group_control(
742 Group_Control_Box_Shadow::get_type(),
743 [
744 'name' => 'kng_tile_shadow',
745 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__card',
746 ]
747 );
748
749 $this->add_group_control(
750 Group_Control_Box_Shadow::get_type(),
751 [
752 'name' => 'kng_tile_shadow_hover',
753 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__card:hover',
754 ]
755 );
756
757 $this->add_control(
758 'kng_tile_hover_lift',
759 [
760 'label' => esc_html__('Hover Lift', 'king-addons'),
761 'type' => Controls_Manager::SLIDER,
762 'size_units' => ['px'],
763 'range' => [
764 'px' => [
765 'min' => 0,
766 'max' => 30,
767 ],
768 ],
769 'default' => [
770 'size' => 4,
771 'unit' => 'px',
772 ],
773 'selectors' => [
774 '{{WRAPPER}} .king-addons-kpi-tiles__card:hover' => 'transform: translateY(-{{SIZE}}{{UNIT}});',
775 ],
776 ]
777 );
778
779 $this->add_control(
780 'kng_tile_content_gap',
781 [
782 'label' => esc_html__('Content Gap', 'king-addons'),
783 'type' => Controls_Manager::SLIDER,
784 'size_units' => ['px'],
785 'range' => [
786 'px' => [
787 'min' => 0,
788 'max' => 30,
789 ],
790 ],
791 'default' => [
792 'size' => 10,
793 'unit' => 'px',
794 ],
795 'selectors' => [
796 '{{WRAPPER}} .king-addons-kpi-tiles__card' => 'gap: {{SIZE}}{{UNIT}};',
797 ],
798 ]
799 );
800
801 $this->add_responsive_control(
802 'kng_tile_padding',
803 [
804 'label' => esc_html__('Inner Padding', 'king-addons'),
805 'type' => Controls_Manager::DIMENSIONS,
806 'size_units' => ['px', '%', 'em'],
807 'selectors' => [
808 '{{WRAPPER}} .king-addons-kpi-tiles__card' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
809 ],
810 ]
811 );
812
813 $this->end_controls_section();
814 }
815
816 /**
817 * Register title style controls.
818 *
819 * @return void
820 */
821 protected function register_style_title_controls(): void
822 {
823 $this->start_controls_section(
824 'kng_style_title_section',
825 [
826 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Title', 'king-addons'),
827 'tab' => Controls_Manager::TAB_STYLE,
828 ]
829 );
830
831 $this->add_group_control(
832 Group_Control_Typography::get_type(),
833 [
834 'name' => 'kng_title_typography',
835 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__title',
836 ]
837 );
838
839 $this->add_control(
840 'kng_title_color',
841 [
842 'label' => esc_html__('Color', 'king-addons'),
843 'type' => Controls_Manager::COLOR,
844 'selectors' => [
845 '{{WRAPPER}} .king-addons-kpi-tiles__title' => 'color: {{VALUE}};',
846 ],
847 ]
848 );
849
850 $this->end_controls_section();
851 }
852
853 /**
854 * Register value style controls.
855 *
856 * @return void
857 */
858 protected function register_style_value_controls(): void
859 {
860 $this->start_controls_section(
861 'kng_style_value_section',
862 [
863 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Value', 'king-addons'),
864 'tab' => Controls_Manager::TAB_STYLE,
865 ]
866 );
867
868 $this->add_group_control(
869 Group_Control_Typography::get_type(),
870 [
871 'name' => 'kng_value_typography',
872 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__value',
873 ]
874 );
875
876 $this->add_control(
877 'kng_value_color',
878 [
879 'label' => esc_html__('Color', 'king-addons'),
880 'type' => Controls_Manager::COLOR,
881 'selectors' => [
882 '{{WRAPPER}} .king-addons-kpi-tiles__value' => 'color: {{VALUE}};',
883 ],
884 ]
885 );
886
887 $this->add_group_control(
888 Group_Control_Typography::get_type(),
889 [
890 'name' => 'kng_value_affix_typography',
891 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__value-prefix, {{WRAPPER}} .king-addons-kpi-tiles__value-suffix',
892 ]
893 );
894
895 $this->add_control(
896 'kng_value_affix_color',
897 [
898 'label' => esc_html__('Prefix/Suffix Color', 'king-addons'),
899 'type' => Controls_Manager::COLOR,
900 'selectors' => [
901 '{{WRAPPER}} .king-addons-kpi-tiles__value-prefix, {{WRAPPER}} .king-addons-kpi-tiles__value-suffix' => 'color: {{VALUE}};',
902 ],
903 ]
904 );
905
906 $this->end_controls_section();
907 }
908
909 /**
910 * Register delta style controls.
911 *
912 * @return void
913 */
914 protected function register_style_delta_controls(): void
915 {
916 $this->start_controls_section(
917 'kng_style_delta_section',
918 [
919 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Delta', 'king-addons'),
920 'tab' => Controls_Manager::TAB_STYLE,
921 ]
922 );
923
924 $this->add_group_control(
925 Group_Control_Typography::get_type(),
926 [
927 'name' => 'kng_delta_typography',
928 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__delta',
929 ]
930 );
931
932 $this->add_control(
933 'kng_delta_gap',
934 [
935 'label' => esc_html__('Delta Gap', 'king-addons'),
936 'type' => Controls_Manager::SLIDER,
937 'size_units' => ['px'],
938 'range' => [
939 'px' => [
940 'min' => 0,
941 'max' => 24,
942 ],
943 ],
944 'default' => [
945 'size' => 6,
946 'unit' => 'px',
947 ],
948 'selectors' => [
949 '{{WRAPPER}} .king-addons-kpi-tiles__delta' => 'gap: {{SIZE}}{{UNIT}};',
950 ],
951 ]
952 );
953
954 $this->add_control(
955 'kng_delta_icon_size',
956 [
957 'label' => esc_html__('Icon Size', 'king-addons'),
958 'type' => Controls_Manager::SLIDER,
959 'size_units' => ['px'],
960 'range' => [
961 'px' => [
962 'min' => 10,
963 'max' => 32,
964 ],
965 ],
966 'default' => [
967 'size' => 16,
968 'unit' => 'px',
969 ],
970 'selectors' => [
971 '{{WRAPPER}} .king-addons-kpi-tiles__delta-icon' => 'width: {{SIZE}}{{UNIT}}; height: {{SIZE}}{{UNIT}};',
972 ],
973 ]
974 );
975
976 $this->add_control(
977 'kng_delta_up_color',
978 [
979 'label' => esc_html__('Positive Color', 'king-addons'),
980 'type' => Controls_Manager::COLOR,
981 'selectors' => [
982 '{{WRAPPER}}' => '--kng-kpi-up-color: {{VALUE}};',
983 ],
984 ]
985 );
986
987 $this->add_control(
988 'kng_delta_down_color',
989 [
990 'label' => esc_html__('Negative Color', 'king-addons'),
991 'type' => Controls_Manager::COLOR,
992 'selectors' => [
993 '{{WRAPPER}}' => '--kng-kpi-down-color: {{VALUE}};',
994 ],
995 ]
996 );
997
998 $this->add_control(
999 'kng_delta_flat_color',
1000 [
1001 'label' => esc_html__('Neutral Color', 'king-addons'),
1002 'type' => Controls_Manager::COLOR,
1003 'selectors' => [
1004 '{{WRAPPER}}' => '--kng-kpi-flat-color: {{VALUE}};',
1005 ],
1006 ]
1007 );
1008
1009 $this->add_group_control(
1010 Group_Control_Typography::get_type(),
1011 [
1012 'name' => 'kng_period_typography',
1013 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__delta-period',
1014 ]
1015 );
1016
1017 $this->add_control(
1018 'kng_period_color',
1019 [
1020 'label' => esc_html__('Period Color', 'king-addons'),
1021 'type' => Controls_Manager::COLOR,
1022 'selectors' => [
1023 '{{WRAPPER}} .king-addons-kpi-tiles__delta-period' => 'color: {{VALUE}};',
1024 ],
1025 ]
1026 );
1027
1028 $this->end_controls_section();
1029 }
1030
1031 /**
1032 * Register sparkline style controls.
1033 *
1034 * @return void
1035 */
1036 protected function register_style_sparkline_controls(): void
1037 {
1038 $this->start_controls_section(
1039 'kng_style_sparkline_section',
1040 [
1041 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Sparkline', 'king-addons'),
1042 'tab' => Controls_Manager::TAB_STYLE,
1043 ]
1044 );
1045
1046 $this->add_control(
1047 'kng_sparkline_color',
1048 [
1049 'label' => esc_html__('Stroke Color', 'king-addons'),
1050 'type' => Controls_Manager::COLOR,
1051 'selectors' => [
1052 '{{WRAPPER}}' => '--kng-kpi-sparkline-color: {{VALUE}};',
1053 ],
1054 'description' => esc_html__('Leave empty to use trend colors.', 'king-addons'),
1055 ]
1056 );
1057
1058 $this->add_responsive_control(
1059 'kng_sparkline_height',
1060 [
1061 'label' => esc_html__('Height', 'king-addons'),
1062 'type' => Controls_Manager::SLIDER,
1063 'size_units' => ['px'],
1064 'range' => [
1065 'px' => [
1066 'min' => 20,
1067 'max' => 120,
1068 ],
1069 ],
1070 'default' => [
1071 'size' => 42,
1072 'unit' => 'px',
1073 ],
1074 'selectors' => [
1075 '{{WRAPPER}} .king-addons-kpi-tiles__sparkline-svg' => 'height: {{SIZE}}{{UNIT}};',
1076 ],
1077 ]
1078 );
1079
1080 $this->add_control(
1081 'kng_sparkline_stroke_width',
1082 [
1083 'label' => esc_html__('Stroke Width', 'king-addons'),
1084 'type' => Controls_Manager::SLIDER,
1085 'size_units' => ['px'],
1086 'range' => [
1087 'px' => [
1088 'min' => 1,
1089 'max' => 6,
1090 ],
1091 ],
1092 'default' => [
1093 'size' => 2,
1094 'unit' => 'px',
1095 ],
1096 'selectors' => [
1097 '{{WRAPPER}} .king-addons-kpi-tiles__sparkline-line' => 'stroke-width: {{SIZE}}{{UNIT}};',
1098 ],
1099 ]
1100 );
1101
1102 $this->add_control(
1103 'kng_sparkline_dot_size',
1104 [
1105 'label' => esc_html__('Dot Size', 'king-addons'),
1106 'type' => Controls_Manager::SLIDER,
1107 'size_units' => ['px'],
1108 'range' => [
1109 'px' => [
1110 'min' => 0,
1111 'max' => 10,
1112 ],
1113 ],
1114 'default' => [
1115 'size' => 3,
1116 'unit' => 'px',
1117 ],
1118 'selectors' => [
1119 '{{WRAPPER}} .king-addons-kpi-tiles__sparkline-dot' => 'r: {{SIZE}}{{UNIT}};',
1120 ],
1121 ]
1122 );
1123
1124 $this->end_controls_section();
1125 }
1126
1127 /**
1128 * Register footer meta style controls.
1129 *
1130 * @return void
1131 */
1132 protected function register_style_footer_controls(): void
1133 {
1134 $this->start_controls_section(
1135 'kng_style_footer_section',
1136 [
1137 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Footer Meta', 'king-addons'),
1138 'tab' => Controls_Manager::TAB_STYLE,
1139 ]
1140 );
1141
1142 $this->add_group_control(
1143 Group_Control_Typography::get_type(),
1144 [
1145 'name' => 'kng_secondary_typography',
1146 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__secondary',
1147 ]
1148 );
1149
1150 $this->add_control(
1151 'kng_secondary_color',
1152 [
1153 'label' => esc_html__('Secondary Color', 'king-addons'),
1154 'type' => Controls_Manager::COLOR,
1155 'selectors' => [
1156 '{{WRAPPER}} .king-addons-kpi-tiles__secondary' => 'color: {{VALUE}};',
1157 ],
1158 ]
1159 );
1160
1161 $this->add_group_control(
1162 Group_Control_Typography::get_type(),
1163 [
1164 'name' => 'kng_badge_typography',
1165 'selector' => '{{WRAPPER}} .king-addons-kpi-tiles__badge',
1166 ]
1167 );
1168
1169 $this->add_control(
1170 'kng_badge_color',
1171 [
1172 'label' => esc_html__('Badge Color', 'king-addons'),
1173 'type' => Controls_Manager::COLOR,
1174 'selectors' => [
1175 '{{WRAPPER}} .king-addons-kpi-tiles__badge' => 'color: {{VALUE}};',
1176 ],
1177 ]
1178 );
1179
1180 $this->add_control(
1181 'kng_badge_background',
1182 [
1183 'label' => esc_html__('Badge Background', 'king-addons'),
1184 'type' => Controls_Manager::COLOR,
1185 'selectors' => [
1186 '{{WRAPPER}} .king-addons-kpi-tiles__badge' => 'background-color: {{VALUE}};',
1187 ],
1188 ]
1189 );
1190
1191 $this->add_control(
1192 'kng_badge_radius',
1193 [
1194 'label' => esc_html__('Badge Radius', 'king-addons'),
1195 'type' => Controls_Manager::SLIDER,
1196 'size_units' => ['px', '%'],
1197 'range' => [
1198 'px' => ['min' => 0, 'max' => 60],
1199 '%' => ['min' => 0, 'max' => 100],
1200 ],
1201 'selectors' => [
1202 '{{WRAPPER}} .king-addons-kpi-tiles__badge' => 'border-radius: {{SIZE}}{{UNIT}};',
1203 ],
1204 ]
1205 );
1206
1207 $this->end_controls_section();
1208 }
1209
1210 /**
1211 * Render pro upgrade notice.
1212 *
1213 * @return void
1214 */
1215 public function register_pro_notice_controls(): void
1216 {
1217 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
1218 Core::renderProFeaturesSection($this, '', Controls_Manager::RAW_HTML, 'kpi-tiles-microcharts', [
1219 'CSV and JSON data sources with refresh controls',
1220 'Advanced locale and currency formatting',
1221 'Animated value changes and sparkline morphs',
1222 'Enhanced microcharts with area fills and gradients',
1223 ]);
1224 }
1225 }
1226
1227 /**
1228 * Render widget output.
1229 *
1230 * @param array<string, mixed> $settings Widget settings.
1231 *
1232 * @return void
1233 */
1234 protected function render_output(array $settings): void
1235 {
1236 $tiles = $settings['kng_kpi_tiles'] ?? [];
1237 if (empty($tiles)) {
1238 if ($this->is_editor()) {
1239 echo '<div class="king-addons-kpi-tiles__empty">' . esc_html__('Add KPI tiles to get started.', 'king-addons') . '</div>';
1240 }
1241 return;
1242 }
1243
1244 $wrapper_classes = $this->get_wrapper_classes($settings);
1245 ?>
1246 <div class="<?php echo esc_attr(implode(' ', $wrapper_classes)); ?>">
1247 <div class="king-addons-kpi-tiles__grid">
1248 <?php foreach ($tiles as $tile) : ?>
1249 <?php $this->render_tile($settings, $tile); ?>
1250 <?php endforeach; ?>
1251 </div>
1252 </div>
1253 <?php
1254 }
1255
1256 /**
1257 * Render a single tile.
1258 *
1259 * @param array<string, mixed> $settings Widget settings.
1260 * @param array<string, mixed> $tile Tile data.
1261 * @return void
1262 */
1263 protected function render_tile(array $settings, array $tile): void
1264 {
1265 $title = $tile['kng_tile_title'] ?? '';
1266 $prefix = $tile['kng_tile_prefix'] ?? '';
1267 $suffix = $tile['kng_tile_suffix'] ?? '';
1268 $decimals = isset($tile['kng_tile_decimals']) ? (int) $tile['kng_tile_decimals'] : 0;
1269 $separator = $this->sanitize_separator((string) ($tile['kng_tile_separator'] ?? 'auto'));
1270 $show_delta = ($tile['kng_tile_show_delta'] ?? 'yes') === 'yes';
1271 $trend = $this->sanitize_trend((string) ($tile['kng_tile_trend'] ?? 'up'));
1272 $delta_type = $this->sanitize_delta_type((string) ($tile['kng_tile_delta_type'] ?? 'percent'));
1273 $delta_decimals = isset($tile['kng_tile_delta_decimals']) ? (int) $tile['kng_tile_delta_decimals'] : $decimals;
1274 $delta_separator = isset($tile['kng_tile_delta_separator'])
1275 ? $this->sanitize_separator((string) $tile['kng_tile_delta_separator'])
1276 : $separator;
1277 $delta_prefix = $tile['kng_tile_delta_prefix'] ?? '';
1278 $delta_suffix = $tile['kng_tile_delta_suffix'] ?? '';
1279 $period_label = $tile['kng_tile_period'] ?? '';
1280 $secondary = $tile['kng_tile_secondary'] ?? '';
1281 $badge = $tile['kng_tile_badge'] ?? '';
1282 $show_sparkline = ($tile['kng_tile_show_sparkline'] ?? 'yes') === 'yes';
1283
1284 $value = $this->format_number($tile['kng_tile_value'] ?? '', $decimals, $separator);
1285 $delta_value = $this->format_number($tile['kng_tile_delta_value'] ?? '', $delta_decimals, $delta_separator);
1286 if ('' !== $delta_value && 'percent' === $delta_type && '' === $delta_suffix) {
1287 $delta_value .= '%';
1288 }
1289
1290 $sparkline_raw = '';
1291 $sparkline_values = [];
1292 $sparkline_svg = '';
1293 $sparkline_error = false;
1294
1295 if ($show_sparkline) {
1296 $sparkline_raw = (string) ($tile['kng_tile_sparkline'] ?? '');
1297 $sparkline_values = $this->parse_sparkline_values($sparkline_raw);
1298 $sparkline_svg = $this->build_sparkline_svg($sparkline_values, $settings);
1299 $sparkline_error = $this->is_editor() && '' !== trim($sparkline_raw) && count($sparkline_values) < 2;
1300 }
1301
1302 $card_attributes = $this->get_card_attributes($tile, $trend, $title);
1303 ?>
1304 <div <?php echo $this->build_html_attributes($card_attributes); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
1305 <?php if ('' !== $title) : ?>
1306 <div class="king-addons-kpi-tiles__title"><?php echo esc_html($title); ?></div>
1307 <?php endif; ?>
1308
1309 <?php if ('' !== $value) : ?>
1310 <div class="king-addons-kpi-tiles__value">
1311 <?php if ('' !== $prefix) : ?>
1312 <span class="king-addons-kpi-tiles__value-prefix"><?php echo esc_html($prefix); ?></span>
1313 <?php endif; ?>
1314 <span class="king-addons-kpi-tiles__value-number"><?php echo esc_html($value); ?></span>
1315 <?php if ('' !== $suffix) : ?>
1316 <span class="king-addons-kpi-tiles__value-suffix"><?php echo esc_html($suffix); ?></span>
1317 <?php endif; ?>
1318 </div>
1319 <?php endif; ?>
1320
1321 <?php if ($show_delta && ('' !== $delta_value || '' !== $period_label)) : ?>
1322 <div class="king-addons-kpi-tiles__delta king-addons-kpi-tiles__delta--<?php echo esc_attr($trend); ?>">
1323 <span class="king-addons-kpi-tiles__delta-icon" aria-hidden="true">
1324 <?php echo $this->get_trend_icon($trend); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1325 </span>
1326 <?php if ('' !== $delta_value) : ?>
1327 <?php if ('' !== $delta_prefix) : ?>
1328 <span class="king-addons-kpi-tiles__delta-prefix"><?php echo esc_html($delta_prefix); ?></span>
1329 <?php endif; ?>
1330 <span class="king-addons-kpi-tiles__delta-value"><?php echo esc_html($delta_value); ?></span>
1331 <?php if ('' !== $delta_suffix) : ?>
1332 <span class="king-addons-kpi-tiles__delta-suffix"><?php echo esc_html($delta_suffix); ?></span>
1333 <?php endif; ?>
1334 <?php endif; ?>
1335 <?php if ('' !== $period_label) : ?>
1336 <span class="king-addons-kpi-tiles__delta-period"><?php echo esc_html($period_label); ?></span>
1337 <?php endif; ?>
1338 </div>
1339 <?php endif; ?>
1340
1341 <?php if ($show_sparkline && $sparkline_error) : ?>
1342 <div class="king-addons-kpi-tiles__sparkline king-addons-kpi-tiles__sparkline--invalid">
1343 <?php echo esc_html__('Sparkline needs at least 2 numeric values.', 'king-addons'); ?>
1344 </div>
1345 <?php elseif ($show_sparkline && '' !== $sparkline_svg) : ?>
1346 <div class="king-addons-kpi-tiles__sparkline">
1347 <?php echo $sparkline_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1348 </div>
1349 <?php endif; ?>
1350
1351 <?php if ('' !== $secondary || '' !== $badge) : ?>
1352 <div class="king-addons-kpi-tiles__footer">
1353 <?php if ('' !== $secondary) : ?>
1354 <span class="king-addons-kpi-tiles__secondary"><?php echo esc_html($secondary); ?></span>
1355 <?php endif; ?>
1356 <?php if ('' !== $badge) : ?>
1357 <span class="king-addons-kpi-tiles__badge"><?php echo esc_html($badge); ?></span>
1358 <?php endif; ?>
1359 </div>
1360 <?php endif; ?>
1361 </div>
1362 <?php
1363 }
1364
1365 /**
1366 * Build wrapper classes.
1367 *
1368 * @param array<string, mixed> $settings Widget settings.
1369 *
1370 * @return array<int, string>
1371 */
1372 protected function get_wrapper_classes(array $settings): array
1373 {
1374 $classes = ['king-addons-kpi-tiles'];
1375
1376 $alignment = $settings['kng_content_alignment'] ?? 'left';
1377 $classes[] = 'king-addons-kpi-tiles--align-' . sanitize_html_class((string) $alignment);
1378
1379 if (!empty($settings['kng_equal_height'])) {
1380 $classes[] = 'king-addons-kpi-tiles--equal-height';
1381 }
1382
1383 $classes = array_merge($classes, $this->get_additional_wrapper_classes($settings));
1384
1385 return array_filter($classes);
1386 }
1387
1388 /**
1389 * Placeholder for pro wrapper classes.
1390 *
1391 * @param array<string, mixed> $settings Widget settings.
1392 *
1393 * @return array<int, string>
1394 */
1395 protected function get_additional_wrapper_classes(array $settings): array
1396 {
1397 unset($settings);
1398 return [];
1399 }
1400
1401 /**
1402 * Build card attributes.
1403 *
1404 * @param array<string, mixed> $tile Tile data.
1405 * @param string $trend Trend direction.
1406 * @param string $title Tile title.
1407 * @return array<string, string>
1408 */
1409 protected function get_card_attributes(array $tile, string $trend, string $title): array
1410 {
1411 $classes = [
1412 'king-addons-kpi-tiles__card',
1413 'king-addons-kpi-tiles__card--' . $trend,
1414 ];
1415
1416 $attributes = [
1417 'class' => implode(' ', $classes),
1418 ];
1419
1420 $link = $tile['kng_tile_link'] ?? [];
1421 if (!empty($link['url'])) {
1422 $attributes['data-card-link'] = esc_url_raw($link['url']);
1423 $rel_parts = [];
1424 if (!empty($link['is_external'])) {
1425 $attributes['data-card-link-target'] = '_blank';
1426 $rel_parts[] = 'noopener';
1427 $rel_parts[] = 'noreferrer';
1428 }
1429 if (!empty($link['nofollow'])) {
1430 $rel_parts[] = 'nofollow';
1431 }
1432 if (!empty($rel_parts)) {
1433 $attributes['data-card-link-rel'] = implode(' ', array_values(array_unique($rel_parts)));
1434 }
1435 $attributes['tabindex'] = '0';
1436 $attributes['role'] = 'link';
1437 if ('' !== $title) {
1438 $attributes['aria-label'] = $title;
1439 }
1440 $classes[] = 'king-addons-kpi-tiles__card--link';
1441 $attributes['class'] = implode(' ', $classes);
1442 }
1443
1444 return $attributes;
1445 }
1446
1447 /**
1448 * Build HTML attributes string.
1449 *
1450 * @param array<string, string> $attributes Attributes.
1451 *
1452 * @return string
1453 */
1454 protected function build_html_attributes(array $attributes): string
1455 {
1456 $output = [];
1457
1458 foreach ($attributes as $key => $value) {
1459 if ('' === $value) {
1460 continue;
1461 }
1462 $output[] = esc_attr($key) . '="' . esc_attr((string) $value) . '"';
1463 }
1464
1465 return implode(' ', $output);
1466 }
1467
1468 /**
1469 * Build sparkline SVG markup.
1470 *
1471 * @param array<int, float> $values Sparkline values.
1472 * @param array<string, mixed> $settings Widget settings.
1473 *
1474 * @return string
1475 */
1476 protected function build_sparkline_svg(array $values, array $settings): string
1477 {
1478 if (count($values) < 2) {
1479 return '';
1480 }
1481
1482 $width = 120.0;
1483 $height = 36.0;
1484 $padding = 2.0;
1485
1486 $min = min($values);
1487 $max = max($values);
1488
1489 if (!empty($settings['kng_sparkline_zero_baseline'])) {
1490 $min = min($min, 0);
1491 $max = max($max, 0);
1492 }
1493
1494 if ($min === $max) {
1495 $min -= 1;
1496 $max += 1;
1497 }
1498
1499 $range = $max - $min;
1500 $count = count($values);
1501 $points = [];
1502
1503 foreach ($values as $index => $value) {
1504 $x = $count > 1
1505 ? ($index / ($count - 1)) * ($width - ($padding * 2)) + $padding
1506 : $width / 2;
1507 $y = $height - $padding - ((($value - $min) / $range) * ($height - ($padding * 2)));
1508
1509 $points[] = [
1510 'x' => $this->format_point($x),
1511 'y' => $this->format_point($y),
1512 ];
1513 }
1514
1515 $smooth = !empty($settings['kng_sparkline_smooth']);
1516 $line_markup = $smooth ? $this->build_smooth_path_markup($points) : $this->build_polyline_markup($points);
1517 $dot_markup = '';
1518
1519 if (!empty($settings['kng_sparkline_last_dot'])) {
1520 $last = $points[$count - 1];
1521 $dot_markup = '<circle class="king-addons-kpi-tiles__sparkline-dot" cx="' . esc_attr($last['x']) . '" cy="' . esc_attr($last['y']) . '" r="3" />';
1522 }
1523
1524 return '<svg class="king-addons-kpi-tiles__sparkline-svg" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true">' . $line_markup . $dot_markup . '</svg>';
1525 }
1526
1527 /**
1528 * Build smooth path markup.
1529 *
1530 * @param array<int, array<string, string>> $points Sparkline points.
1531 *
1532 * @return string
1533 */
1534 protected function build_smooth_path_markup(array $points): string
1535 {
1536 $count = count($points);
1537 if ($count < 2) {
1538 return '';
1539 }
1540
1541 $d = 'M ' . $points[0]['x'] . ' ' . $points[0]['y'];
1542
1543 for ($i = 1; $i < $count; $i++) {
1544 $prev = $points[$i - 1];
1545 $current = $points[$i];
1546 $mid_x = $this->format_point(((float) $prev['x'] + (float) $current['x']) / 2);
1547 $mid_y = $this->format_point(((float) $prev['y'] + (float) $current['y']) / 2);
1548 $d .= ' Q ' . $prev['x'] . ' ' . $prev['y'] . ' ' . $mid_x . ' ' . $mid_y;
1549 }
1550
1551 $last = $points[$count - 1];
1552 $d .= ' T ' . $last['x'] . ' ' . $last['y'];
1553
1554 return '<path class="king-addons-kpi-tiles__sparkline-line" d="' . esc_attr($d) . '" />';
1555 }
1556
1557 /**
1558 * Build polyline markup.
1559 *
1560 * @param array<int, array<string, string>> $points Sparkline points.
1561 *
1562 * @return string
1563 */
1564 protected function build_polyline_markup(array $points): string
1565 {
1566 $pairs = [];
1567 foreach ($points as $point) {
1568 $pairs[] = $point['x'] . ',' . $point['y'];
1569 }
1570
1571 return '<polyline class="king-addons-kpi-tiles__sparkline-line" points="' . esc_attr(implode(' ', $pairs)) . '" />';
1572 }
1573
1574 /**
1575 * Format point value.
1576 *
1577 * @param float $value Raw point value.
1578 *
1579 * @return string
1580 */
1581 protected function format_point(float $value): string
1582 {
1583 return number_format($value, 2, '.', '');
1584 }
1585
1586 /**
1587 * Parse sparkline values from string.
1588 *
1589 * @param string $raw_values Raw values string.
1590 *
1591 * @return array<int, float>
1592 */
1593 protected function parse_sparkline_values(string $raw_values): array
1594 {
1595 $raw_values = trim($raw_values);
1596 if ('' === $raw_values) {
1597 return [];
1598 }
1599
1600 $parts = preg_split('/\s*,\s*/', $raw_values, -1, PREG_SPLIT_NO_EMPTY);
1601 if (empty($parts)) {
1602 return [];
1603 }
1604
1605 $values = [];
1606 foreach ($parts as $part) {
1607 $clean = trim((string) $part);
1608 if ('' === $clean || !is_numeric($clean)) {
1609 continue;
1610 }
1611 $values[] = (float) $clean;
1612 }
1613
1614 if (count($values) > 50) {
1615 $values = array_slice($values, 0, 50);
1616 }
1617
1618 return $values;
1619 }
1620
1621 /**
1622 * Format numeric value.
1623 *
1624 * @param mixed $value Raw value.
1625 * @param int $decimals Decimals count.
1626 * @param string $separator Thousands separator option.
1627 *
1628 * @return string
1629 */
1630 protected function format_number($value, int $decimals, string $separator): string
1631 {
1632 if (!is_numeric($value)) {
1633 return '';
1634 }
1635
1636 $number = (float) $value;
1637 $decimals = max(0, min(4, $decimals));
1638 $separator = $this->sanitize_separator($separator);
1639
1640 if ('auto' === $separator) {
1641 return number_format_i18n($number, $decimals);
1642 }
1643
1644 $thousand_separator = $this->get_thousand_separator($separator);
1645 $decimal_separator = $this->get_decimal_separator($separator);
1646
1647 return number_format($number, $decimals, $decimal_separator, $thousand_separator);
1648 }
1649
1650 /**
1651 * Get thousands separator character.
1652 *
1653 * @param string $separator Separator key.
1654 *
1655 * @return string
1656 */
1657 protected function get_thousand_separator(string $separator): string
1658 {
1659 switch ($separator) {
1660 case 'comma':
1661 return ',';
1662 case 'dot':
1663 return '.';
1664 case 'space':
1665 return ' ';
1666 default:
1667 return '';
1668 }
1669 }
1670
1671 /**
1672 * Get decimal separator character.
1673 *
1674 * @param string $separator Separator key.
1675 *
1676 * @return string
1677 */
1678 protected function get_decimal_separator(string $separator): string
1679 {
1680 return 'dot' === $separator ? ',' : '.';
1681 }
1682
1683 /**
1684 * Sanitize trend direction.
1685 *
1686 * @param string $trend Raw trend value.
1687 *
1688 * @return string
1689 */
1690 protected function sanitize_trend(string $trend): string
1691 {
1692 $allowed = ['up', 'down', 'flat'];
1693 return in_array($trend, $allowed, true) ? $trend : 'up';
1694 }
1695
1696 /**
1697 * Sanitize delta type.
1698 *
1699 * @param string $delta_type Raw delta type.
1700 *
1701 * @return string
1702 */
1703 protected function sanitize_delta_type(string $delta_type): string
1704 {
1705 return 'absolute' === $delta_type ? 'absolute' : 'percent';
1706 }
1707
1708 /**
1709 * Sanitize separator option.
1710 *
1711 * @param string $separator Raw separator.
1712 *
1713 * @return string
1714 */
1715 protected function sanitize_separator(string $separator): string
1716 {
1717 $allowed = ['auto', 'comma', 'dot', 'space'];
1718 return in_array($separator, $allowed, true) ? $separator : 'auto';
1719 }
1720
1721 /**
1722 * Get trend icon SVG.
1723 *
1724 * @param string $trend Trend direction.
1725 *
1726 * @return string
1727 */
1728 protected function get_trend_icon(string $trend): string
1729 {
1730 switch ($trend) {
1731 case 'down':
1732 return '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M10 3v12M10 15l-5-5M10 15l5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>';
1733 case 'flat':
1734 return '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M3 10h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>';
1735 case 'up':
1736 default:
1737 return '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M10 17V5M10 5l-5 5M10 5l5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>';
1738 }
1739 }
1740
1741 /**
1742 * Check if in Elementor editor.
1743 *
1744 * @return bool
1745 */
1746 protected function is_editor(): bool
1747 {
1748 return class_exists(Plugin::class) && Plugin::$instance->editor->is_edit_mode();
1749 }
1750 }
1751