feat: implement interactive graphs (#17029)
Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
parent
62030497e5
commit
1c730bc692
|
@ -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",
|
||||
|
|
|
@ -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))))
|
|
@ -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}))
|
|
@ -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))
|
|
@ -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))
|
|
@ -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"))))
|
|
@ -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)))
|
|
@ -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)))))
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]]))
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue