From 1c730bc6920b683b7355a4142062933beeb417d0 Mon Sep 17 00:00:00 2001 From: Brian Sztamfater Date: Thu, 31 Aug 2023 13:45:54 -0300 Subject: [PATCH] feat: implement interactive graphs (#17029) Signed-off-by: Brian Sztamfater --- package.json | 2 +- .../interactive_graph/component_spec.cljs | 13 ++ .../graph/interactive_graph/style.cljs | 49 +++++ .../graph/interactive_graph/view.cljs | 141 ++++++++++++++ src/quo2/components/graph/utils.cljs | 95 ++++++++++ src/quo2/components/graph/utils_test.cljs | 179 ++++++++++++++++++ .../components/graph/wallet_graph/utils.cljs | 13 -- .../graph/wallet_graph/utils_test.cljs | 55 ------ .../components/graph/wallet_graph/view.cljs | 2 +- src/quo2/core.cljs | 4 +- src/quo2/foundations/colors.cljs | 1 + .../quo_preview/graph/interactive_graph.cljs | 152 +++++++++++++++ src/status_im2/contexts/quo_preview/main.cljs | 7 +- yarn.lock | 5 +- 14 files changed, 643 insertions(+), 75 deletions(-) create mode 100644 src/quo2/components/graph/interactive_graph/component_spec.cljs create mode 100644 src/quo2/components/graph/interactive_graph/style.cljs create mode 100644 src/quo2/components/graph/interactive_graph/view.cljs create mode 100644 src/quo2/components/graph/utils.cljs create mode 100644 src/quo2/components/graph/utils_test.cljs delete mode 100644 src/quo2/components/graph/wallet_graph/utils.cljs delete mode 100644 src/quo2/components/graph/wallet_graph/utils_test.cljs create mode 100644 src/status_im2/contexts/quo_preview/graph/interactive_graph.cljs diff --git a/package.json b/package.json index 71546ea037..761913b8f5 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "react-native-fetch-polyfill": "^1.1.2", "react-native-fs": "^2.14.1", "react-native-gesture-handler": "2.6.1", - "react-native-gifted-charts": "^1.3.2", + "react-native-gifted-charts": "git+https://github.com/status-im/react-native-gifted-charts.git#refs/tags/1.3.2-status.1", "react-native-haptic-feedback": "^1.9.0", "react-native-hole-view": "git+https://github.com/status-im/react-native-hole-view.git#refs/tags/v2.1.3-status", "react-native-image-crop-picker": "git+https://github.com/status-im/react-native-image-crop-picker.git#refs/tags/v0.36.2-status.0", diff --git a/src/quo2/components/graph/interactive_graph/component_spec.cljs b/src/quo2/components/graph/interactive_graph/component_spec.cljs new file mode 100644 index 0000000000..263e18e17e --- /dev/null +++ b/src/quo2/components/graph/interactive_graph/component_spec.cljs @@ -0,0 +1,13 @@ +(ns quo2.components.graph.interactive-graph.component-spec + (:require [test-helpers.component :as h] + [quo2.components.graph.interactive-graph.view :as interactive-graph])) + +(defn data + [num-elements] + (vec (take num-elements (repeat {:value 10})))) + +(h/describe "interactive-graph" + (h/test "render interactive graph" + (h/render [interactive-graph/view + {:data data}]) + (h/is-truthy (h/get-by-label-text :interactive-graph)))) diff --git a/src/quo2/components/graph/interactive_graph/style.cljs b/src/quo2/components/graph/interactive_graph/style.cljs new file mode 100644 index 0000000000..2f7721a2dd --- /dev/null +++ b/src/quo2/components/graph/interactive_graph/style.cljs @@ -0,0 +1,49 @@ +(ns quo2.components.graph.interactive-graph.style + (:require [quo2.foundations.typography :as typography])) + +(defn x-axis-label-text + [width y-axis-label-text-color] + (merge + typography/label + {:color y-axis-label-text-color + :height 16 + :text-align :center + :width width})) + +(defn y-axis-text + [y-axis-label-text-color y-axis-label-background-color] + (merge + typography/label + {:color y-axis-label-text-color + :padding-horizontal 3 + :margin-left 23 + :height 16 + :border-radius 6 + :overflow :hidden + :background-color y-axis-label-background-color})) + +(defn pointer-component + [customization-color] + {:width 8 + :height 8 + :border-radius 4 + :margin-left 1 + :background-color customization-color}) + +(defn reference-line-label + [border-color background-color text-color] + (merge + typography/label + {:align-self :flex-end + :right 10 + :margin-top -9 + :padding-horizontal 5 + :padding-top -10 + :height 19 + :line-height 14.62 + :border-radius 6 + :overflow :hidden + :border-color border-color + :border-width 2 + :background-color background-color + :color text-color})) diff --git a/src/quo2/components/graph/interactive_graph/view.cljs b/src/quo2/components/graph/interactive_graph/view.cljs new file mode 100644 index 0000000000..59c1915e88 --- /dev/null +++ b/src/quo2/components/graph/interactive_graph/view.cljs @@ -0,0 +1,141 @@ +(ns quo2.components.graph.interactive-graph.view + (:require [quo2.components.graph.utils :as utils] + [quo2.foundations.colors :as colors] + [quo2.theme :as quo.theme] + [react-native.charts :as charts] + [react-native.core :as rn] + [reagent.core :as reagent] + [quo2.components.graph.interactive-graph.style :as style])) + +(def chart-height 375) +(def max-data-points 500) +(def no-of-sections 4) +(def initial-spacing 56) +(def end-spacing 22) +(def y-axis-label-width -33) +(def inspecting? (reagent/atom false)) + +(defn- pointer + [customization-color] + (reagent/as-element + [rn/view + {:style + (style/pointer-component + customization-color)}])) + +(defn- pointer-config + [customization-color] + {:stroke-dash-array [2 2] + :pointer-component #(pointer customization-color) + :pointer-strip-color customization-color + :pointer-color customization-color + :pointer-strip-enable-gradient true}) + +(defn- get-pointer-props + [pointer-props] + (let [pointer-index (.-pointerIndex ^js pointer-props)] + (reset! inspecting? (not= pointer-index -1)))) + +(defn- get-line-color + [state theme] + (if @inspecting? + (colors/theme-colors colors/neutral-80-opa-40 + colors/white-opa-20 + theme) + (if (= state :positive) + (colors/theme-colors colors/success-50 + colors/success-60 + theme) + (colors/theme-colors colors/danger-50 + colors/danger-60 + theme)))) + +(defn- view-internal + [{:keys [data state customization-color theme reference-value reference-prefix decimal-separator] + :or {reference-prefix "$" + decimal-separator :dot}}] + (let [data (if (> (count data) max-data-points) + (utils/downsample-data data max-data-points) + data) + highest-value (utils/find-highest-value data) + lowest-value (utils/find-lowest-value data) + min-value (utils/calculate-rounded-min lowest-value) + max-value (- (utils/calculate-rounded-max highest-value) min-value) + step-value (/ max-value 4) + width (:width (rn/get-window)) + line-color (get-line-color state theme) + rules-color (colors/theme-colors colors/neutral-80-opa-10 + colors/white-opa-5 + theme) + y-axis-label-text-color (colors/theme-colors colors/neutral-80-opa-40 + colors/white-opa-40 + theme) + price-reference-label-text-color (colors/theme-colors colors/neutral-100 colors/white theme) + reference-label-border-color (colors/theme-colors colors/white colors/neutral-95 theme) + y-axis-label-background-color (colors/theme-colors colors/white-70-blur-opaque + colors/neutral-95 + theme) + customization-color (colors/theme-colors + (colors/custom-color customization-color 60) + (colors/custom-color customization-color 50) + theme) + y-axis-label-texts (utils/calculate-y-axis-labels min-value step-value 4) + x-axis-label-texts (utils/calculate-x-axis-labels data 5) + reference-label-background-color (colors/theme-colors colors/neutral-80-opa-5-opaque + colors/neutral-80 + theme) + reference-value (or reference-value (/ (+ highest-value lowest-value) 2)) + formatted-reference-value (utils/format-currency-number reference-value decimal-separator) + chart-width (+ width 13)] + [rn/view {:accessibility-label :interactive-graph} + [charts/line-chart + {:height chart-height + :width chart-width + :max-value max-value + :x-axis-length chart-width + :y-axis-offset min-value + :y-axis-label-texts y-axis-label-texts + :y-axis-label-texts-ignore-offset true + :adjust-to-width true + :data data + :hide-data-points true + :no-of-sections no-of-sections + :step-value step-value + :rules-color rules-color + :dash-width 2 + :dash-gap 2 + :hide-y-axis-text false + :thickness 1 + :color line-color + :y-axis-thickness 0 + :x-axis-thickness 0 + :initial-spacing initial-spacing + :end-spacing end-spacing + :disable-scroll true + :hide-origin true + :show-reference-line-1 true + :get-pointer-props get-pointer-props + :show-strip-on-focus true + :reference-line-1-config {:color rules-color} + :reference-line-1-position 0 + :show-reference-line-2 (and (not @inspecting?) + (<= reference-value highest-value) + (>= reference-value lowest-value)) + :reference-line-2-config {:color y-axis-label-text-color + :label-text-style (style/reference-line-label + reference-label-border-color + reference-label-background-color + price-reference-label-text-color) + :label-text (str reference-prefix + formatted-reference-value) + :dash-width 2} + :reference-line-2-position (- reference-value min-value) + :y-axis-text-style (style/y-axis-text y-axis-label-text-color + y-axis-label-background-color) + :y-axis-label-width y-axis-label-width + :pointer-config (pointer-config customization-color) + :x-axis-label-text-style (style/x-axis-label-text (/ width (count x-axis-label-texts)) + y-axis-label-text-color) + :x-axis-label-texts x-axis-label-texts}]])) + +(def view (quo.theme/with-theme view-internal)) diff --git a/src/quo2/components/graph/utils.cljs b/src/quo2/components/graph/utils.cljs new file mode 100644 index 0000000000..15e41bc423 --- /dev/null +++ b/src/quo2/components/graph/utils.cljs @@ -0,0 +1,95 @@ +(ns quo2.components.graph.utils + (:require [clojure.string :as string] + [goog.string :as gstring])) + +(defn find-highest-value + [coll] + (apply max (map :value coll))) + +(defn find-lowest-value + [coll] + (apply min (map :value coll))) + +(defn downsample-data + [data max-array-size] + (let [data-size (count data)] + (if (> data-size max-array-size) + (let [step-size (max (/ data-size max-array-size) 1)] + (vec (take-nth step-size data))) + data))) + +(defn format-compact-number + [number] + (let [abbreviations ["" "k" "M" "B" "T"] + log-base-1000 (js/Math.log10 1000) ; Calculate the logarithm base 1000 + magnitude (int (/ (js/Math.log10 number) log-base-1000)) + suffix (nth abbreviations magnitude) + scaled-number (/ number (js/Math.pow 1000.0 magnitude)) + formatted-scaled-number (if (zero? (rem scaled-number 1)) + (int scaled-number) + (-> (gstring/format "%.2f" scaled-number) + (string/replace #"\.?0+$" "")))] + (if (zero? magnitude) + (str number) + (if (and (>= scaled-number 1) (< scaled-number 1000)) + (str formatted-scaled-number suffix) + (str formatted-scaled-number "0" suffix))))) + +(defn calculate-x-axis-labels + [array num-elements] + (let [array-length (count array) + partitions (partition-all (js/Math.floor (/ array-length (min array-length num-elements))) + array)] + (->> partitions + (map first) + (map :date)))) + +(defn calculate-y-axis-labels + [min-value step-value no-of-steps] + (let [labels-array (for [i (range (inc no-of-steps))] + (let [value (+ min-value (* step-value i)) + compact-number (format-compact-number value)] + compact-number))] + (vec labels-array))) + +(defn calculate-rounded-max + [highest-value] + (let [min-percentage-above 1.05 ; 5% above + rounded-divisor 4 ; Divisor for even division by 4 + target-max (* min-percentage-above highest-value) + rounded-up (js/Math.ceil target-max) + remainder (mod rounded-up rounded-divisor) + y-axis-max (if (zero? remainder) + rounded-up + (+ rounded-up (- rounded-divisor remainder)))] + y-axis-max)) + +(defn calculate-rounded-min + [lowest] + (let [order-of-magnitude (js/Math.pow 10 (js/Math.floor (js/Math.log10 lowest))) + rounded-min (cond + (and (< lowest 1) (>= lowest 0)) 0 + (>= lowest 1) + (* (js/Math.floor (/ lowest + order-of-magnitude)) + order-of-magnitude) + (< lowest 0) + (* (calculate-rounded-max (* -1 lowest)) + -1))] + rounded-min)) + +(defn format-currency-number + [number decimal-separator] + (let [formatted (-> (if (= decimal-separator :comma) + (js/Intl.NumberFormat + "de-DE" + {:style "currency" :currency "EUR" :minimumFractionDigits 2}) + (js/Intl.NumberFormat + "en-US" + {:style "currency" :currency "USD" :minimumFractionDigits 2})) + (.format number)) + separator-char (if (= decimal-separator :comma) "," ".") + formatted-with-decimals (if (.includes formatted separator-char) + formatted + (str formatted separator-char "00"))] + formatted-with-decimals)) diff --git a/src/quo2/components/graph/utils_test.cljs b/src/quo2/components/graph/utils_test.cljs new file mode 100644 index 0000000000..4182eddb9d --- /dev/null +++ b/src/quo2/components/graph/utils_test.cljs @@ -0,0 +1,179 @@ +(ns quo2.components.graph.utils-test + (:require [cljs.test :refer-macros [deftest is testing]] + [quo2.components.graph.utils :as utils])) + +(deftest find-highest-value + (testing "Find highest value with a single map" + (let [data [{:value 5}]] + (is (= (utils/find-highest-value data) 5)))) + + (testing "Find highest value with multiple maps" + (let [data [{:value 5} {:value 10} {:value 3} {:value 7}]] + (is (= (utils/find-highest-value data) 10)))) + + (testing "Find highest value with negative values" + (let [data [{:value -2} {:value -10} {:value -3} {:value -7}]] + (is (= (utils/find-highest-value data) -2)))) + + (testing "Find highest value with decimal values" + (let [data [{:value 3.5} {:value 7.2} {:value 2.9}]] + (is (= (utils/find-highest-value data) 7.2)))) + + (testing "Find highest value with a large data set" + (let [data (vec (for [num (range 1000)] {:value num}))] + (is (= (utils/find-highest-value data) 999))))) + +(deftest find-lowest-value + (testing "Find lowest value with a single map" + (let [data [{:value 5}]] + (is (= (utils/find-lowest-value data) 5)))) + + (testing "Find lowest value with multiple maps" + (let [data [{:value 5} {:value 10} {:value 3} {:value 7}]] + (is (= (utils/find-lowest-value data) 3)))) + + (testing "Find lowest value with negative values" + (let [data [{:value -2} {:value -10} {:value -3} {:value -7}]] + (is (= (utils/find-lowest-value data) -10)))) + + (testing "Find lowest value with decimal values" + (let [data [{:value 3.5} {:value 7.2} {:value 2.9}]] + (is (= (utils/find-lowest-value data) 2.9)))) + + (testing "Find lowest value with a large data set" + (let [data (vec (for [num (range 1000)] {:value num}))] + (is (= (utils/find-lowest-value data) 0))))) + +(deftest downsample-data + (testing "Downsampling is applied correctly when needed" + (let [input-data [1 2 3 4 5 6 7 8 9 10] + max-array-size 5 + expected-output [1 3 5 7 9]] + (is (= (utils/downsample-data input-data max-array-size) expected-output)))) + + (testing "Downsampling is not applied when not needed" + (let [input-data [1 2 3 4 5 6 7 8 9 10] + max-array-size 10 + expected-output [1 2 3 4 5 6 7 8 9 10]] + (is (= (utils/downsample-data input-data max-array-size) expected-output)))) + + (testing "Downsampling works with empty input data" + (let [input-data [] + max-array-size 5 + expected-output []] + (is (= (utils/downsample-data input-data max-array-size) expected-output)))) + + (testing "Downsampling works with max-array-size of 1 (edge case)" + (let [input-data [1 2 3 4 5] + max-array-size 1 + expected-output [1]] + (is (= (utils/downsample-data input-data max-array-size) expected-output)))) + + (testing "Downsampling works with large input data and max-array-size (randomized test)" + (let [large-data (range 1000) + max-array-size 500 + expected-output (range 0 1000 2)] ; expected-output contains every 2nd element from 0 to 1000 + (is (= (utils/downsample-data large-data max-array-size) expected-output))))) + +(deftest format-compact-number + (testing "Format a whole number less than 1000" + (is (= (utils/format-compact-number 567) "567"))) + + (testing "Format a whole number exactly 1000" + (is (= (utils/format-compact-number 1000) "1k"))) + + (testing "Format a whole number greater than 1000" + (is (= (utils/format-compact-number 2500) "2.5k"))) + + (testing "Format a decimal number less than 1000" + (is (= (utils/format-compact-number 123.45) "123.45"))) + + (testing "Format a decimal number exactly 1000" + (is (= (utils/format-compact-number 1000) "1k"))) + + (testing "Format a decimal number greater than 1000" + (is (= (utils/format-compact-number 7890.123) "7.89k"))) + + (testing "Format a number in millions" + (is (= (utils/format-compact-number 123456789) "123.46M"))) + + (testing "Format a number in billions" + (is (= (utils/format-compact-number 1234567890) "1.23B"))) + + (testing "Format a number in trillions" + (is (= (utils/format-compact-number 1234567890000) "1.23T")))) + +(deftest calculate-x-axis-labels + (testing "Calculate x-axis labels with a small array and fewer elements" + (let [data [{:date "2023-01-01"} {:date "2023-01-02"} {:date "2023-01-03"} {:date "2023-01-04"}]] + (is (= (utils/calculate-x-axis-labels data 2) '("2023-01-01" "2023-01-03"))))) + + (testing "Calculate x-axis labels with a larger array and more elements" + (let [data (vec (for [i (range 10)] {:date (str "2023-01-0" (inc i))}))] + (is (= (utils/calculate-x-axis-labels data 5) + '("2023-01-01" "2023-01-03" "2023-01-05" "2023-01-07" "2023-01-09"))))) + + (testing "Calculate x-axis labels with a very small array" + (let [data [{:date "2023-01-01"}]] + (is (= (utils/calculate-x-axis-labels data 3) '("2023-01-01"))))) + + (testing "Calculate x-axis labels with a larger array and a single element" + (let [data (vec (for [i (range 10)] {:date (str "2023-01-0" (inc i))}))] + (is (= (utils/calculate-x-axis-labels data 1) '("2023-01-01")))))) + +(deftest calculate-y-axis-labels + (testing "Calculate y-axis labels with positive values" + (is (= (utils/calculate-y-axis-labels 0 10 5) ["0" "10" "20" "30" "40" "50"]))) + + (testing "Calculate y-axis labels with negative values" + (is (= (utils/calculate-y-axis-labels -20 5 4) ["-20" "-15" "-10" "-5" "0"]))) + + (testing "Calculate y-axis labels with decimal step value" + (is (= (utils/calculate-y-axis-labels 2.5 0.5 4) ["2.5" "3" "3.5" "4" "4.5"]))) + + (testing "Calculate y-axis labels with a single step" + (is (= (utils/calculate-y-axis-labels 5 1 1) ["5" "6"]))) + + (testing "Calculate y-axis labels with large step value and number of steps" + (is (= (utils/calculate-y-axis-labels 100 1000 3) ["100" "1.1k" "2.1k" "3.1k"])))) + +(deftest calculate-rounded-max + (testing "Calculate rounded max with a whole number" + (is (= (utils/calculate-rounded-max 100) 108))) + + (testing "Calculate rounded max with a decimal number" + (is (= (utils/calculate-rounded-max 50.5) 56))) + + (testing "Calculate rounded max with a number already divisible by divisor" + (is (= (utils/calculate-rounded-max 108) 116))) + + (testing "Calculate rounded max with a number close to the next divisor" + (is (= (utils/calculate-rounded-max 250) 264))) + + (testing "Calculate rounded max with a large number" + (is (= (utils/calculate-rounded-max 1000) 1052)))) + +(deftest calculate-rounded-min + (testing "Calculate rounded min with a whole number" + (is (= (utils/calculate-rounded-min 157) 100))) + + (testing "Calculate rounded min with a decimal number" + (is (= (utils/calculate-rounded-min 49.8) 40))) + + (testing "Calculate rounded min with a small number" + (is (= (utils/calculate-rounded-min 0.007) 0))) + + (testing "Calculate rounded min with a number already rounded" + (is (= (utils/calculate-rounded-min 500) 500))) + + (testing "Calculate rounded min with a negative number" + (is (= (utils/calculate-rounded-min -63) -68)))) + +(deftest format-currency-number-test + (testing "Format currency number with comma decimal separator" + (is (= (utils/format-currency-number 12345 :comma) "12.345,00")) + (is (= (utils/format-currency-number 12345.67 :comma) "12.345,67"))) + + (testing "Format currency number with dot decimal separator" + (is (= (utils/format-currency-number 12345 :dot) "12,345.00")) + (is (= (utils/format-currency-number 12345.67 :dot) "12,345.67")))) diff --git a/src/quo2/components/graph/wallet_graph/utils.cljs b/src/quo2/components/graph/wallet_graph/utils.cljs deleted file mode 100644 index e07b6387b1..0000000000 --- a/src/quo2/components/graph/wallet_graph/utils.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns quo2.components.graph.wallet-graph.utils) - -(defn find-highest-value - [coll] - (apply max (map :value coll))) - -(defn downsample-data - [data max-array-size] - (let [data-size (count data)] - (if (> data-size max-array-size) - (let [step-size (max (/ data-size max-array-size) 1)] - (vec (take-nth step-size data))) - data))) diff --git a/src/quo2/components/graph/wallet_graph/utils_test.cljs b/src/quo2/components/graph/wallet_graph/utils_test.cljs deleted file mode 100644 index f1cc88046d..0000000000 --- a/src/quo2/components/graph/wallet_graph/utils_test.cljs +++ /dev/null @@ -1,55 +0,0 @@ -(ns quo2.components.graph.wallet-graph.utils-test - (:require [cljs.test :refer-macros [deftest is testing]] - [quo2.components.graph.wallet-graph.utils :as utils])) - -(deftest find-highest-value - (testing "Find highest value with a single map" - (let [data [{:value 5}]] - (is (= (utils/find-highest-value data) 5)))) - - (testing "Find highest value with multiple maps" - (let [data [{:value 5} {:value 10} {:value 3} {:value 7}]] - (is (= (utils/find-highest-value data) 10)))) - - (testing "Find highest value with negative values" - (let [data [{:value -2} {:value -10} {:value -3} {:value -7}]] - (is (= (utils/find-highest-value data) -2)))) - - (testing "Find highest value with decimal values" - (let [data [{:value 3.5} {:value 7.2} {:value 2.9}]] - (is (= (utils/find-highest-value data) 7.2)))) - - (testing "Find highest value with a large data set" - (let [data (vec (for [num (range 1000)] {:value num}))] - (is (= (utils/find-highest-value data) 999))))) - -(deftest downsample-data - (testing "Downsampling is applied correctly when needed" - (let [input-data [1 2 3 4 5 6 7 8 9 10] - max-array-size 5 - expected-output [1 3 5 7 9]] - (is (= (utils/downsample-data input-data max-array-size) expected-output)))) - - (testing "Downsampling is not applied when not needed" - (let [input-data [1 2 3 4 5 6 7 8 9 10] - max-array-size 10 - expected-output [1 2 3 4 5 6 7 8 9 10]] - (is (= (utils/downsample-data input-data max-array-size) expected-output)))) - - (testing "Downsampling works with empty input data" - (let [input-data [] - max-array-size 5 - expected-output []] - (is (= (utils/downsample-data input-data max-array-size) expected-output)))) - - (testing "Downsampling works with max-array-size of 1 (edge case)" - (let [input-data [1 2 3 4 5] - max-array-size 1 - expected-output [1]] - (is (= (utils/downsample-data input-data max-array-size) expected-output)))) - - (testing "Downsampling works with large input data and max-array-size (randomized test)" - (let [large-data (range 1000) - max-array-size 500 - expected-output (range 0 1000 2)] ;; expected-output contains every 2nd element from 0 to 1000 - (is (= (utils/downsample-data large-data max-array-size) expected-output))))) diff --git a/src/quo2/components/graph/wallet_graph/view.cljs b/src/quo2/components/graph/wallet_graph/view.cljs index 90c022c7a2..9b62960f6a 100644 --- a/src/quo2/components/graph/wallet_graph/view.cljs +++ b/src/quo2/components/graph/wallet_graph/view.cljs @@ -6,7 +6,7 @@ [quo2.components.graph.wallet-graph.style :as style] [quo2.foundations.colors :as colors] [quo2.components.markdown.text :as text] - [quo2.components.graph.wallet-graph.utils :as utils])) + [quo2.components.graph.utils :as utils])) (defn- max-data-points [time-frame] diff --git a/src/quo2/core.cljs b/src/quo2/core.cljs index e501220391..e1fc021988 100644 --- a/src/quo2/core.cljs +++ b/src/quo2/core.cljs @@ -124,7 +124,8 @@ quo2.components.wallet.progress-bar.view quo2.components.wallet.summary-info.view quo2.components.wallet.token-input.view - quo2.components.wallet.wallet-overview.view)) + quo2.components.wallet.wallet-overview.view + [quo2.components.graph.interactive-graph.view :as interactive-graph])) (def separator quo2.components.common.separator.view/separator) @@ -205,6 +206,7 @@ (def empty-state quo2.components.empty-state.empty-state.view/empty-state) ;;;; Graph +(def interactive-graph quo2.components.graph.interactive-graph.view/view) (def wallet-graph quo2.components.graph.wallet-graph.view/view) ;;;; Header diff --git a/src/quo2/foundations/colors.cljs b/src/quo2/foundations/colors.cljs index 1b30a870ff..9e1e84a021 100644 --- a/src/quo2/foundations/colors.cljs +++ b/src/quo2/foundations/colors.cljs @@ -123,6 +123,7 @@ ;;;;Blur (def white-70-blur (alpha white 0.7)) +(def white-70-blur-opaque (alpha-opaque white 0.7)) (def neutral-80-opa-1-blur (alpha "#192438" 0.1)) (def neutral-5-opa-70-blur (alpha neutral-5 0.7)) (def neutral-10-opa-10-blur (alpha neutral-10 0.1)) diff --git a/src/status_im2/contexts/quo_preview/graph/interactive_graph.cljs b/src/status_im2/contexts/quo_preview/graph/interactive_graph.cljs new file mode 100644 index 0000000000..02e8e84199 --- /dev/null +++ b/src/status_im2/contexts/quo_preview/graph/interactive_graph.cljs @@ -0,0 +1,152 @@ +(ns status-im2.contexts.quo-preview.graph.interactive-graph + (:require [quo2.core :as quo] + [quo2.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im2.contexts.quo-preview.preview :as preview] + [quo2.components.graph.utils :as utils] + [goog.string :as gstring])) + +(def weekly-data + [{:value 123 + :date "Sun"} + {:value 160 + :date "Mon"} + {:value 435 + :date "Tue"} + {:value 2345 + :date "Wed"} + {:value 1444 + :date "Thu"} + {:value 931 + :date "Fri"} + {:value 1200 + :date "Sat"}]) + +(defn generate-crypto-token-prices + [num-elements volatility] + (loop [n num-elements + prices [] + prev-price (rand-int 100000) + volatility volatility + current-day (rand-int 31) ; Start with a random day + months ["Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"] + current-month (rand-nth months)] ; Start with a random month + (if (zero? n) + (vec (reverse prices)) + (let [fluctuation (* prev-price volatility) + random-delta (- (rand fluctuation) (/ fluctuation 2)) + new-price (max 1 (+ prev-price random-delta)) + new-day (if (= current-day 1) 31 (dec current-day)) ; Decrease the day + new-month (if (= current-day 1) + (let [prev-month-index (dec (.indexOf months current-month))] + (if (>= prev-month-index 0) + (nth months prev-month-index) + (nth months (dec (count months))))) + current-month) + new-prices (conj prices + {:value new-price + :date (str new-day " " new-month)})] + (recur (dec n) new-prices new-price volatility new-day months new-month))))) + + +(def descriptor + [{:label "State:" + :key :state + :type :select + :options [{:key :positive + :value "Positive"} + {:key :negative + :value "Negative"}]} + {:label "Time frame:" + :key :time-frame + :type :select + :options [{:key :empty + :value "Empty"} + {:key :1-week + :value "1 Week"} + {:key :1-month + :value "1 Month"} + {:key :3-months + :value "3 Months"} + {:key :1-year + :value "1 Year"} + {:key :all-time + :value "All time (500 years data)"}]} + {:label "Reference value:" + :key :reference-value + :type :number} + {:label "Reference prefix:" + :key :reference-prefix + :type :text} + {:label "Reference decimal separator:" + :key :decimal-separator + :type :select + :options [{:key :dot + :value "Dot (.)"} + {:key :comma + :value "Comma (,)"}]} + (preview/customization-color-option)]) + +(defn generate-data + [time-frame] + (let [data-points (case time-frame + :empty 0 + :1-week 7 + :1-month 30 + :3-months 90 + :1-year 365 + (* 365 500)) + volatility (case time-frame + :empty 0 + :1-week 2 + :1-month 1 + :3-months 0.5 + :1-year 0.05 + 0.005)] + (if (= time-frame :1-week) + weekly-data + (generate-crypto-token-prices data-points volatility)))) + +(defn f-view + [state] + (fn [] + (rn/use-effect (fn [] + (let [time-frame (:time-frame @state) + data (generate-data time-frame) + highest-value (utils/find-highest-value data) + lowest-value (utils/find-lowest-value data) + average-value (gstring/format "%.2f" (/ (+ highest-value lowest-value) 2))] + (swap! state assoc :data data :reference-value average-value))) + [(:time-frame @state)]) + [rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!} + [rn/view {:padding-bottom 150} + [preview/customizer state descriptor] + [quo/interactive-graph + {:data (:data @state) + :state (:state @state) + :reference-value (:reference-value @state) + :reference-prefix (:reference-prefix @state) + :customization-color (:customization-color @state) + :decimal-separator (:decimal-separator @state)}]]])) + +(defn view + [] + (let [data (generate-data :1-week) + highest-value (utils/find-highest-value data) + lowest-value (utils/find-lowest-value data) + average-value (gstring/format "%.2f" (/ (+ highest-value lowest-value) 2)) + state (reagent/atom {:state :positive + :time-frame :1-week + :customization-color :blue + :reference-value average-value + :reference-prefix "$" + :decimal-separator :dot + :data data})] + [rn/scroll-view + {:style + {:background-color (colors/theme-colors + colors/white + colors/neutral-95) + :flex 1}} + [:f> f-view state]])) diff --git a/src/status_im2/contexts/quo_preview/main.cljs b/src/status_im2/contexts/quo_preview/main.cljs index 25e34af44e..4a28aee4e9 100644 --- a/src/status_im2/contexts/quo_preview/main.cljs +++ b/src/status_im2/contexts/quo_preview/main.cljs @@ -27,6 +27,7 @@ [status-im2.contexts.quo-preview.calendar.calendar-year :as calendar-year] [status-im2.contexts.quo-preview.browser.browser-input :as browser-input] [status-im2.contexts.quo-preview.code.snippet :as code-snippet] + [status-im2.contexts.quo-preview.graph.interactive-graph :as interactive-graph] [status-im2.contexts.quo-preview.graph.wallet-graph :as wallet-graph] [status-im2.contexts.quo-preview.colors.color-picker :as color-picker] [status-im2.contexts.quo-preview.community.community-card-view :as community-card] @@ -212,7 +213,11 @@ :component empty-state/view}] :gradient [{:name :gradient-cover :component gradient-cover/view}] - :graph [{:name :wallet-graph + :graph [{:name :interactive-graph + :options {:topBar {:visible true}} + :component interactive-graph/view} + {:name :wallet-graph + :options {:topBar {:visible true}} :component wallet-graph/view}] :info [{:name :info-message :component info-message/view} diff --git a/yarn.lock b/yarn.lock index 34226e2963..ef7ddc00d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8854,10 +8854,9 @@ react-native-gesture-handler@2.6.1: lodash "^4.17.21" prop-types "^15.7.2" -react-native-gifted-charts@^1.3.2: +"react-native-gifted-charts@git+https://github.com/status-im/react-native-gifted-charts.git#refs/tags/1.3.2-status.1": version "1.3.2" - resolved "https://registry.yarnpkg.com/react-native-gifted-charts/-/react-native-gifted-charts-1.3.2.tgz#9e2d054b8571026eec5d6a38a7a424da065df726" - integrity sha512-MHWE0A772w57ZKz/r7cWjBFwvRzY3kWDv+PaMBACzNdL13paLl/uOHsKzPP1lZ3Hnj3iICEo7u4aqo0TQ3mGLQ== + resolved "git+https://github.com/status-im/react-native-gifted-charts.git#6c0bd2e75afe67d0423386247049257a0a0edda1" dependencies: react-native-linear-gradient "^2.7.3" react-native-svg "^13.9.0"