@libs-ui/utils 0.2.281 → 0.2.283

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -645,6 +645,70 @@ const omitBy = (objData, predicate) => {
645
645
  });
646
646
  return newObj;
647
647
  };
648
+ /**
649
+ * Lấy giá trị từ đối tượng theo đường dẫn chỉ định
650
+ *
651
+ * Hàm này giúp bạn truy cập vào các thuộc tính sâu bên trong một đối tượng một cách an toàn,
652
+ * tránh lỗi khi thuộc tính không tồn tại.
653
+ *
654
+ * @param obj Đối tượng nguồn cần lấy giá trị
655
+ * @param path Đường dẫn đến thuộc tính cần lấy. Có thể là:
656
+ * - Chuỗi: 'user.profile.name' hoặc 'items[0].title'
657
+ * - Mảng: ['user', 'profile', 'name'] hoặc ['items', '0', 'title']
658
+ * - Chuỗi rỗng '': trả về chính đối tượng gốc
659
+ * @param defaultValue Giá trị mặc định trả về khi không tìm thấy thuộc tính (mặc định: undefined)
660
+ * @param keepLastValueIfSignal Có giữ nguyên signal cuối cùng hay không (mặc định: false - sẽ gọi signal())
661
+ * @returns Giá trị tìm được hoặc giá trị mặc định
662
+ *
663
+ * @example
664
+ * // Ví dụ cơ bản
665
+ * const user = { name: 'John', age: 30 };
666
+ * get(user, 'name'); // 'John'
667
+ * get(user, 'email'); // undefined
668
+ * get(user, 'email', 'no-email'); // 'no-email'
669
+ *
670
+ * @example
671
+ * // Truyền path rỗng - trả về chính đối tượng gốc
672
+ * const data = { name: 'Alice', age: 25 };
673
+ * get(data, ''); // { name: 'Alice', age: 25 } (chính đối tượng data)
674
+ * get(data, '', 'default'); // { name: 'Alice', age: 25 } (bỏ qua defaultValue)
675
+ *
676
+ * @example
677
+ * // Truy cập thuộc tính sâu
678
+ * const data = {
679
+ * user: {
680
+ * profile: {
681
+ * name: 'Alice',
682
+ * settings: { theme: 'dark' }
683
+ * }
684
+ * }
685
+ * };
686
+ * get(data, 'user.profile.name'); // 'Alice'
687
+ * get(data, 'user.profile.settings.theme'); // 'dark'
688
+ * get(data, 'user.profile.avatar', 'default.jpg'); // 'default.jpg'
689
+ *
690
+ * @example
691
+ * // Truy cập mảng
692
+ * const items = [
693
+ * { name: 'Item 1', price: 100 },
694
+ * { name: 'Item 2', price: 200 }
695
+ * ];
696
+ * get(items, '[0].name'); // 'Item 1'
697
+ * get(items, '0.price'); // 100
698
+ * get(items, '[1].name'); // 'Item 2'
699
+ * get(items, '[2].name', 'Not found'); // 'Not found'
700
+ *
701
+ * @example
702
+ * // Sử dụng với mảng path
703
+ * const nested = { a: { b: { c: 'deep value' } } };
704
+ * get(nested, ['a', 'b', 'c']); // 'deep value'
705
+ * get(nested, ['a', 'b', 'd'], 'default'); // 'default'
706
+ *
707
+ * @example
708
+ * // Trường hợp đặc biệt
709
+ * get(null, 'any.path'); // undefined
710
+ * get(undefined, 'any.path', 'fallback'); // 'fallback'
711
+ */
648
712
  const get = (obj, path, defaultValue = undefined, keepLastValueIfSignal) => {
649
713
  if (isNil(obj)) {
650
714
  return defaultValue;
@@ -2447,36 +2511,190 @@ const convertUrlToFile = (url, fileName) => {
2447
2511
  });
2448
2512
  };
2449
2513
 
2514
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2515
+ // Constants for better maintainability
2516
+ const DEFAULT_MIN_TICK_COUNT = 5;
2517
+ const DEFAULT_MAX_TICK_COUNT = 10;
2518
+ const MIN_DISTANCE_TO_ZERO = 3;
2519
+ const NEGATIVE_THRESHOLD = -5;
2520
+ const FALLBACK_NEGATIVE_VALUE = -4;
2521
+ const MAX_POW_NUMBER = 14;
2522
+ const MAX_TEMPLATE_NUMBER = 10;
2523
+ // Cache for power calculations - shared across function calls for better performance
2524
+ const POWER_CACHE = new Map();
2525
+ /**
2526
+ * Calculates smart axis scale for charts
2527
+ *
2528
+ * @description This function can throw errors if:
2529
+ * - maxData < 0 and acceptNegative is false
2530
+ * - minNegative is undefined and acceptNegative is true
2531
+ * - maxData < minNegative (only when acceptNegative is true)
2532
+ * - minNegative > 0 (only when acceptNegative is true)
2533
+ *
2534
+ * @throws {Error} Validation errors for invalid input combinations
2535
+ * @required Always wrap this function in try-catch and handle exceptions
2536
+ * @param maxData - Maximum data value
2537
+ * @param options - Configuration options for axis scaling
2538
+ * @returns Axis scale configuration
2539
+ */
2450
2540
  const getSmartAxisScale = (maxData, options) => {
2451
- const stepCandidates = options?.stepCandidates || [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000];
2541
+ // Input validation - fail fast approach
2542
+ validateInputs(maxData, options);
2543
+ // Extract and set defaults
2544
+ const { minTickCount = DEFAULT_MIN_TICK_COUNT, maxTickCount = DEFAULT_MAX_TICK_COUNT, stepCandidates = [], acceptNegative = false, minNegative, acceptStepIsTypeFloat = false } = options || {};
2545
+ // Generate step candidates if not provided
2546
+ const finalStepCandidates = stepCandidates.length > 0
2547
+ ? [...stepCandidates]
2548
+ : generateStepCandidates(maxData, minTickCount, minNegative, acceptStepIsTypeFloat);
2549
+ // Remove console.log for production code
2550
+ // console.log(`stepCandidates: `, finalStepCandidates);
2551
+ // Calculate scale parameters
2552
+ const scaleParams = calculateScaleParams(maxData, acceptNegative, minNegative, finalStepCandidates);
2553
+ // Find optimal scale
2554
+ const optimalScale = findOptimalScale(scaleParams, finalStepCandidates, minTickCount, maxTickCount, options);
2555
+ if (optimalScale) {
2556
+ return optimalScale;
2557
+ }
2558
+ // Fallback calculation
2559
+ return calculateFallbackScale(scaleParams.distanceToZero, minTickCount);
2560
+ };
2561
+ /**
2562
+ * Validates input parameters
2563
+ */
2564
+ function validateInputs(maxData, options) {
2565
+ if (maxData < 0 && !options?.acceptNegative) {
2566
+ throw new Error("maxData is less than 0 and acceptNegative is false");
2567
+ }
2568
+ if (options?.acceptNegative) {
2569
+ if (isNil(options.minNegative)) {
2570
+ throw new Error("minNegative is required when acceptNegative is true");
2571
+ }
2572
+ if (maxData < options.minNegative) {
2573
+ throw new Error("maxData is less than minNegative");
2574
+ }
2575
+ if (options.minNegative >= 0) {
2576
+ throw new Error("minNegative must be negative");
2577
+ }
2578
+ }
2579
+ }
2580
+ /**
2581
+ * Calculates scale parameters for axis calculation
2582
+ */
2583
+ function calculateScaleParams(maxData, acceptNegative, minNegative, stepCandidates) {
2584
+ let distanceToZero = maxData;
2585
+ let reverse = 1;
2586
+ let adjustedMaxData = maxData;
2587
+ if (acceptNegative && !isNil(minNegative)) {
2588
+ if (maxData === 0) {
2589
+ adjustedMaxData = -1;
2590
+ }
2591
+ // Add negative step candidates
2592
+ if (stepCandidates) {
2593
+ stepCandidates.unshift(...stepCandidates.map(item => -item));
2594
+ }
2595
+ if (adjustedMaxData <= 0) {
2596
+ distanceToZero = minNegative;
2597
+ }
2598
+ if (distanceToZero > NEGATIVE_THRESHOLD) {
2599
+ distanceToZero = FALLBACK_NEGATIVE_VALUE;
2600
+ }
2601
+ if (adjustedMaxData > 0) {
2602
+ distanceToZero = adjustedMaxData + Math.abs(minNegative);
2603
+ reverse = -1;
2604
+ }
2605
+ }
2606
+ if (Math.abs(distanceToZero) < MIN_DISTANCE_TO_ZERO) {
2607
+ distanceToZero = MIN_DISTANCE_TO_ZERO;
2608
+ }
2609
+ return { distanceToZero, reverse, adjustedMaxData };
2610
+ }
2611
+ /**
2612
+ * Finds optimal scale from step candidates
2613
+ */
2614
+ function findOptimalScale(scaleParams, stepCandidates, minTickCount, maxTickCount, options) {
2615
+ const { distanceToZero, reverse, adjustedMaxData } = scaleParams;
2452
2616
  for (const step of stepCandidates) {
2453
- let tickCount = Math.ceil(maxData / step) + 1;
2454
- let maxY = step * tickCount;
2455
- if (tickCount >= (options?.minTickCount || 5) && tickCount <= (options?.maxTickCount || 10)) {
2456
- if (maxData < maxY) {
2457
- tickCount = maxData - (tickCount - 1) * step < -1 * (step / 4) ? tickCount - 1 : tickCount;
2458
- maxY = step * tickCount;
2459
- return {
2460
- step: step,
2461
- maxY: maxY,
2462
- ticks: tickCount
2463
- };
2617
+ let tickCount = Math.abs(Math.ceil(distanceToZero / step)) + (reverse === -1 ? 2 : 1);
2618
+ let maxValue = adjustedMaxData <= 0 ? 0 : step * tickCount * reverse;
2619
+ let minValue = 0;
2620
+ if (tickCount >= minTickCount && tickCount <= maxTickCount && adjustedMaxData < maxValue) {
2621
+ if (options?.acceptNegative) {
2622
+ minValue = calculateMinValue(step, tickCount, options.minNegative);
2464
2623
  }
2624
+ // Adjust tick count based on distance calculation
2625
+ const distanceCheck = distanceToZero - (tickCount - 1) * step;
2626
+ if (distanceCheck < -1 * (step / 4)) {
2627
+ tickCount -= 1;
2628
+ }
2629
+ maxValue = (step * tickCount * reverse) - (minValue * reverse);
2630
+ return {
2631
+ stepSize: Math.abs(step),
2632
+ max: maxValue,
2633
+ min: minValue,
2634
+ tickAmount: tickCount
2635
+ };
2636
+ }
2637
+ }
2638
+ return null;
2639
+ }
2640
+ /**
2641
+ * Calculates minimum value for negative acceptance
2642
+ */
2643
+ function calculateMinValue(step, tickCount, minNegative) {
2644
+ let minValue = 0;
2645
+ if (!isNil(minNegative)) {
2646
+ let tick = 1;
2647
+ while (tick <= tickCount && minValue >= minNegative) {
2648
+ minValue = tick * step;
2649
+ tick++;
2465
2650
  }
2466
2651
  }
2467
- const tickCount = options?.maxTickCount || 10;
2468
- let step = Math.ceil(maxData / tickCount);
2469
- let maxY = step * tickCount;
2470
- if (maxData === maxY) {
2652
+ return minValue;
2653
+ }
2654
+ /**
2655
+ * Calculates fallback scale when no optimal scale is found
2656
+ */
2657
+ function calculateFallbackScale(distanceToZero, minTickCount) {
2658
+ let step = Math.ceil(distanceToZero / minTickCount) || 1;
2659
+ let maxValue = step * minTickCount;
2660
+ if (distanceToZero === maxValue) {
2471
2661
  step = step + Math.ceil(step / 10);
2472
- maxY = step * tickCount;
2662
+ maxValue = step * minTickCount;
2473
2663
  }
2474
2664
  return {
2475
- step: step,
2476
- maxY: maxY,
2477
- ticks: tickCount
2665
+ stepSize: Math.abs(step),
2666
+ max: maxValue,
2667
+ min: 0,
2668
+ tickAmount: minTickCount
2478
2669
  };
2479
- };
2670
+ }
2671
+ /**
2672
+ * Generates step candidates with performance optimizations
2673
+ */
2674
+ function generateStepCandidates(maxData, minStep, minNegative, acceptStepIsTypeFloat) {
2675
+ const stepCandidates = [];
2676
+ const maxValueStep = Math.abs(Math.max(maxData, Math.abs(minNegative || 0))) / minStep;
2677
+ // Pre-populate power cache for better performance
2678
+ for (let powNumber = 0; powNumber < MAX_POW_NUMBER; powNumber++) {
2679
+ if (!POWER_CACHE.has(powNumber)) {
2680
+ POWER_CACHE.set(powNumber, Math.pow(10, powNumber));
2681
+ }
2682
+ const powByTemp = POWER_CACHE.get(powNumber);
2683
+ for (let templateNumber = 1; templateNumber < MAX_TEMPLATE_NUMBER; templateNumber++) {
2684
+ if (acceptStepIsTypeFloat) {
2685
+ const floatStep = powByTemp * (((templateNumber - 1) * 2 + 1) / 2);
2686
+ stepCandidates.push(floatStep);
2687
+ }
2688
+ const step = powByTemp * templateNumber;
2689
+ stepCandidates.push(step);
2690
+ // Early termination for performance
2691
+ if (step >= maxValueStep) {
2692
+ return stepCandidates;
2693
+ }
2694
+ }
2695
+ }
2696
+ return stepCandidates;
2697
+ }
2480
2698
 
2481
2699
  /**
2482
2700
  * Generated bundle index. Do not edit.