Pretty much done
There are things which could be improved, but then there always are...
This commit is contained in:
parent
a782e17f7c
commit
315e320fcd
10
README.md
10
README.md
|
@ -6,6 +6,16 @@ Works well in Chrome and Firefox; not tested in Internet Exploder.
|
|||
|
||||

|
||||
|
||||
## Intended uses
|
||||
|
||||
This is a component for a console, typically one controlling a technical or scientific instrument. It is
|
||||
by design flexible and configurable. Minimum value, maximum value, warning value and number of gradations
|
||||
shown are all configurable, as are (obviously) styles.
|
||||
|
||||
A cursor will be shown if the value of *setpoint* is between *min-value* and *max-value*; *setpoint* is a dynamic value which is watched during the run. While the *model* value is within +- *tolerance* of the *setpoint* value, a class *target-class* is set on the meter indicating an on-target status (by default the frame goes green).
|
||||
|
||||
A red-zone may be shown if a *warn-value* is set which is between *min-value* and *max-value*. If such a *warn-value* is set, then if the current value (*model*) exceeds *warn-value*, a class *warning-class* is set on the meter indicating a warning status (by default the frame goes maroon).
|
||||
|
||||
## Development Mode
|
||||
|
||||
### Run application:
|
||||
|
|
|
@ -19,6 +19,16 @@
|
|||
stroke: #444444;
|
||||
}
|
||||
|
||||
.snm-gradation path {
|
||||
stroke: black;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.snm-gradation text {
|
||||
font-size: 50%;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.snm-hub {
|
||||
fill: #444444;
|
||||
}
|
||||
|
@ -50,13 +60,14 @@
|
|||
stroke-width: 15;
|
||||
}
|
||||
|
||||
.snm-target .snm-frame {
|
||||
stroke: green;
|
||||
}
|
||||
|
||||
.snm-value {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.snm-warning {
|
||||
fill: none;
|
||||
stroke-width: 10;
|
||||
stroke-linejoin: round;
|
||||
stroke: red;
|
||||
.snm-warning .snm-frame {
|
||||
stroke: maroon;
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
@ -1,5 +1,6 @@
|
|||
(ns swinging-needle-meter.swinging-needle-meter
|
||||
(:require [re-com.core :refer [h-box v-box box gap line label title slider checkbox p]]
|
||||
(:require [clojure.string :as string]
|
||||
[re-com.core :refer [h-box v-box box gap line label title slider checkbox p]]
|
||||
[re-com.box :refer [flex-child-style]]
|
||||
[re-com.util :refer [deref-or-value]]
|
||||
[re-com.validate :refer [number-or-string? css-style? html-attr? validate-args-macro]]
|
||||
|
@ -30,7 +31,7 @@
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; ------------------------------------------------------------------------------------
|
||||
;; Component: swinging-needle
|
||||
;; Component: swinging-needle-meter
|
||||
;; ------------------------------------------------------------------------------------
|
||||
|
||||
;;; It seems the defaults given here are just documentation; the defaults
|
||||
|
@ -48,10 +49,14 @@
|
|||
:validate-fn number? :description "the minimum value model can take"}
|
||||
{:name :max-value :required false :type "double" :default 100
|
||||
:validate-fn number? :description "the maximum value model can take"}
|
||||
{:name :warn-value :required false :type "double" :default 80
|
||||
:validate-fn number? :description "the maximum safe value model can take"}
|
||||
{:name :tolerance :required false :type "double" :default 3
|
||||
:validate-fn number? :description "the amount by which model can differ from setpoint and still be considered acceptable"}
|
||||
{:name :class :required false :type "string"
|
||||
:validate-fn string? :description "CSS class names, space separated, for the top-level SVG element"}
|
||||
{:name :alarm-class :required false :type "string"
|
||||
:validate-fn string? :description "CSS class names, space separated, for the cursor"}
|
||||
{:name :alarm-class :required false :type "string" :default "snm-warning"
|
||||
:validate-fn string? :description "CSS class names, space separated, applied to the frame in an alarm condition"}
|
||||
{:name :cursor-class :required false :type "string" :default "snm-cursor"
|
||||
:validate-fn string? :description "CSS class names, space separated, for the cursor"}
|
||||
{:name :frame-class :required false :type "string" :default "snm-frame"
|
||||
|
@ -64,10 +69,14 @@
|
|||
:validate-fn string? :description "CSS class names, space separated, for the scale"}
|
||||
{:name :redzone-class :required false :type "string" :default "snm-redzone"
|
||||
:validate-fn string? :description "CSS class names, space separated, for the redzone"}
|
||||
{:name :target-class :required false :type "string" :default "snm-target"
|
||||
:validate-fn string? :description "CSS class names, space separated, , applied to the frame in an on-target condition"}
|
||||
{:name :unit :required false :type "string"
|
||||
:validate-fn string? :description "Unit to show after the value"}
|
||||
{:name :id :required false :type "string" :default "meter"
|
||||
:validate-fn string? :description "Element id for this instance of the meter"}
|
||||
{:name :gradations :reduired false :type "integer" :default 5
|
||||
:validate-fn integer? :description "Number of gradations to show on the scale, not counting the point."}
|
||||
{:name :style :required false :type "CSS style map"
|
||||
:validate-fn css-style? :description "CSS styles to add or override"}
|
||||
{:name :attr :required false :type "HTML attr map"
|
||||
|
@ -81,6 +90,10 @@
|
|||
;; from the left end of the scale to right end, in degrees.
|
||||
(def full-scale-deflection 140)
|
||||
|
||||
;; ultimately this should be resizeable, and radius should be a function of
|
||||
;; size...
|
||||
(def scale-radius 75)
|
||||
|
||||
(defn deflection
|
||||
"Return the deflection of a needle given this `value` on the
|
||||
range `min-value`...`max-value`."
|
||||
|
@ -89,33 +102,97 @@
|
|||
deflection (/ value range)
|
||||
zero-offset (/ (- 0 min-value) range)
|
||||
limited (min (max (+ zero-offset deflection) 0) 1)]
|
||||
(js/console.log (str "zero-offset: " zero-offset))
|
||||
(* (- limited 0.5) full-scale-deflection)))
|
||||
|
||||
(defn polar-to-cartesian
|
||||
"Return, as a map with keys :x. :y, the cartesian coordinates at the point
|
||||
`radius` distance at `theta` (degrees) angle from a point at
|
||||
cartesian coordinates `cx`, `cy`."
|
||||
[cx cy radius theta]
|
||||
(let
|
||||
[in-radians (/ (* (- theta 90) (aget js/Math "PI")) 180.0)]
|
||||
{:x (+ cx (* radius (.cos js/Math in-radians)))
|
||||
:y (+ cy (* radius (.sin js/Math in-radians)))}))
|
||||
|
||||
(defn describe-arc
|
||||
"Return as a string an SVG path definition describing an arc centred
|
||||
at `cx`, cy` starting at `start-angle` and ending at `end-angle` (both
|
||||
angles in degrees)."
|
||||
[cx cy radius start-angle end-angle]
|
||||
(let
|
||||
[start (polar-to-cartesian cx cy radius start-angle)
|
||||
end (polar-to-cartesian cx cy radius end-angle)
|
||||
large-arc? (if (<= (- end-angle start-angle) 180) 0 1)
|
||||
sweep (if (> end-angle start-angle) 1 0)]
|
||||
(string/join " " ["M" (:x start) (:y start) "A" radius radius 0 large-arc? sweep (:x end) (:y end)])))
|
||||
|
||||
|
||||
(defn as-label
|
||||
"If this arg is a floating point number, format it to a reasonable width; else return it."
|
||||
[arg]
|
||||
(if
|
||||
(and (number? arg) (not (integer? arg)))
|
||||
(.toFixed arg 2)
|
||||
arg))
|
||||
|
||||
|
||||
(defn gradation
|
||||
"Return as a string an SVG path definition describing a radial stroke from a center
|
||||
at `cx`, cy` starting at `min-radius` and extending to `max-radius`."
|
||||
[cx cy min-radius max-radius angle label]
|
||||
(let
|
||||
[start (polar-to-cartesian cx cy min-radius angle)
|
||||
mid (polar-to-cartesian cx cy (+ min-radius
|
||||
(* (- max-radius min-radius) 0.333))
|
||||
angle)
|
||||
end (polar-to-cartesian cx cy max-radius angle)]
|
||||
[:g {:class "snm-gradation"}
|
||||
[:path {:d (string/join " " ["M" (:x mid) (:y mid) "L" (:x end) (:y end)])}]
|
||||
[:text {:text-anchor "middle"
|
||||
:x (:x start)
|
||||
:y (:y start)
|
||||
:transform (string/join " " ["rotate(" angle (:x start) (:y start) ")"])} (as-label label)]]))
|
||||
|
||||
|
||||
(defn swinging-needle-meter
|
||||
"Render an SVG swinging needle meter"
|
||||
[& {:keys [model setpoint width height min-value max-value class alarm-class cursor-class frame-class hub-class needle-class scale-class redzone-class unit id style attr]
|
||||
[& {:keys [model setpoint width height min-value max-value warn-value tolerance class gradations alarm-class cursor-class frame-class hub-class needle-class redzone-class scale-class target-class unit id style attr]
|
||||
:or {width "100%"
|
||||
height "100%"
|
||||
min-value 0
|
||||
max-value 100
|
||||
warn-value 80
|
||||
tolerance 3
|
||||
gradations 5
|
||||
alarm-class "snm-warning"
|
||||
cursor-class "snm-cursor"
|
||||
frame-class "snm-frame"
|
||||
hub-class "snm-hub"
|
||||
needle-class "snm-needle"
|
||||
scale-class "snm-scale"
|
||||
redzone-class "snm-redzone"
|
||||
target-class "snm-target"
|
||||
id "meter"}
|
||||
:as args}]
|
||||
{:pre [(validate-args-macro swinging-needle-args-desc args "swinging-needle")]}
|
||||
(let [model (deref-or-value model)
|
||||
setpoint (deref-or-value setpoint)
|
||||
current-value (str model (if unit " ") unit)]
|
||||
mid-point-deflection (/ full-scale-deflection 2)
|
||||
;; if warn-value is greater than max-value, we don't want a red-zone at all.
|
||||
red-zone-deflection (if
|
||||
(< warn-value max-value)
|
||||
(* full-scale-deflection (/ warn-value max-value))
|
||||
full-scale-deflection)]
|
||||
[box
|
||||
:align :start
|
||||
:child [:div
|
||||
(merge
|
||||
{:class (str "swinging-needle " class)
|
||||
{:class (str "swinging-needle " class " " (str
|
||||
(if (< min-value model warn-value) ""
|
||||
(str " " alarm-class))
|
||||
(if (and (> setpoint min-value)(< (abs (- model setpoint)) tolerance))
|
||||
(str " " target-class) "")))
|
||||
:style (merge (flex-child-style "none")
|
||||
{:width width :height height}
|
||||
style)}
|
||||
|
@ -131,37 +208,19 @@
|
|||
:id id
|
||||
:class (str "snm-meter " class)}
|
||||
[:text
|
||||
{:xml:space "preserve"
|
||||
:x "-75.5"
|
||||
:y "50"
|
||||
:id (str id "-min-value")
|
||||
:class "snm-limit"
|
||||
:transform "matrix(0.2398013,-0.97082199,0.97082199,0.2398013,0,0)"}[:tspan min-value]]
|
||||
[:text
|
||||
{:xml:space "preserve"
|
||||
:x "102"
|
||||
:y "-102"
|
||||
:id (str id "-max-value")
|
||||
:class "snm-limit"
|
||||
:transform "matrix(0.26614433,0.96393319,-0.96393319,0.26614433,0,0)"}[:tspan max-value]]
|
||||
[:text
|
||||
{:xml:space "preserve"
|
||||
;; 4.5 here is a real fudge. It's roughly half the width in SVG units of a single character;
|
||||
;; it's intended to keep the visible text roughly in the middle of the meter.
|
||||
:x (str (- 80 (* (count current-value) 4.5)))
|
||||
:y "60"
|
||||
{:text-anchor "middle"
|
||||
:x 80
|
||||
:y 70
|
||||
:width "100"
|
||||
:id (str id "-current-value")
|
||||
:class "snm-value"}[:tspan current-value]]
|
||||
:class "snm-value"}[:tspan (str (as-label model) (if unit " ") unit)]]
|
||||
[:path {:class scale-class
|
||||
:id (str id "-scale")
|
||||
:d "m 11.85914,76.864488 c 0,0 14.34545,-53.795412 68.140856,-53.795412 53.795424,0 68.140864,53.795412 68.140864,53.795412"}]
|
||||
:d (describe-arc 80 100 scale-radius (- 0 mid-point-deflection) mid-point-deflection)}]
|
||||
[:path {:class redzone-class
|
||||
:id (str id "-redzone")
|
||||
:d "m 137.74738,54.878869 c 0,0 3.02675,3.620416 6.3911,11.14347 3.36435,7.523055 4.20612,11.198095 4.20612,11.198095"}]
|
||||
[:rect {:class (str frame-class (if (< min-value model max-value) "" (str " " alarm-class)))
|
||||
:id (str id "-frame")
|
||||
:x "5" :y "5" :height "100" :width "150"}]
|
||||
:d (describe-arc 80 100 scale-radius (- red-zone-deflection mid-point-deflection) mid-point-deflection)}]
|
||||
|
||||
[:path {:class cursor-class
|
||||
:id (str id "-cursor")
|
||||
:d "M 80,20 80,100"
|
||||
|
@ -171,11 +230,20 @@
|
|||
:id (str id "-needle")
|
||||
:d "M 80,20 80,100"
|
||||
:transform (str "rotate( " (deflection model min-value max-value) ", 80, 100)") }]
|
||||
(apply vector (cons :g (map #(gradation 80 100 60 82
|
||||
(- (* %
|
||||
(/ full-scale-deflection gradations))
|
||||
mid-point-deflection)
|
||||
(+ min-value
|
||||
(*
|
||||
(/
|
||||
(- max-value min-value)
|
||||
gradations) %)))
|
||||
(range 0 (+ gradations 1)))))
|
||||
[:rect {:class frame-class
|
||||
:id (str id "-frame")
|
||||
:x "5" :y "5" :height "100" :width "150"}]
|
||||
[:circle {:class hub-class
|
||||
:id (str id "-hub")
|
||||
:r "10" :cx "80" :cy "100"}]]
|
||||
;;; Useful for debugging:
|
||||
(str "value: " model "; min: " min-value
|
||||
"; max: " max-value
|
||||
"; deflection: " (int (deflection model min-value max-value)))
|
||||
]]))
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
[reagent.core :as reagent]))
|
||||
|
||||
;; ------------------------------------------------------------------------------------
|
||||
;; Demo: swinging-needle
|
||||
;; Demo: swinging-needle-meter
|
||||
;; ------------------------------------------------------------------------------------
|
||||
|
||||
(defn swinging-needle-demo
|
||||
[]
|
||||
(let [value (reagent/atom 75)
|
||||
setpoint (reagent/atom 75)
|
||||
striped? (reagent/atom false)]
|
||||
setpoint (reagent/atom 75)]
|
||||
(fn
|
||||
[]
|
||||
[v-box
|
||||
|
@ -27,13 +26,27 @@
|
|||
:width "450px"
|
||||
:children [[title2 "Notes"]
|
||||
[status-text "Wildly experimental"]
|
||||
[p "An SVG swinging needle meter."]
|
||||
[p "An SVG swinging needle meter intended to be useful in dashboards."]
|
||||
[p "Note that the cursor will vanish if the setpoint is null or is less than or equal to min-value; this is intentional."]
|
||||
[p "Note that if the value of model is lower then min-value or greater than max-value,
|
||||
it will be limited as it would be on a mechanical meter."]
|
||||
[p "You can hide the redzone by setting its style to the style 'snm-scale'"]
|
||||
[p
|
||||
"TODO: You can't adjust the position of the start of the red-zone; "]
|
||||
[p "You can hide the redzone by setting its style to the style 'snm-scale', or by setting 'warn-value' equal to 'max-value'."]
|
||||
|
||||
[title2 "Behaviour"]
|
||||
|
||||
[p "min-value and max-value must be numbers; max-value must be greater than min-value.
|
||||
The default behaviour is of a swinging needle meter with the needle deflection proportional
|
||||
to the value of the model (also a number) expressed as a proportion of the difference between
|
||||
min-value and max-value."]
|
||||
|
||||
[p "A red-zone can be introduced by setting a value for warn-value between min-value and max-value. Additionally, if
|
||||
the value of model exceeds warn-value the class alarm-class will be set on the meter indicating a warning state."]
|
||||
|
||||
[p "A cursor can be shown by setting the value of set-point between min-value and max-value. A tolerance value can be specified
|
||||
by setting a value for tolerance. If the difference between the model value and the setpoint value is less than the tolerance
|
||||
value, the class target-class will be set on the meter to indicate an on-target status. The setpoint value, like the model value,
|
||||
may change dynamically at run-time."]
|
||||
|
||||
[args-table swinging-needle-args-desc]]]
|
||||
[v-box
|
||||
:gap "10px"
|
||||
|
@ -43,10 +56,13 @@
|
|||
:children [[swinging-needle-meter
|
||||
:model value
|
||||
:setpoint setpoint
|
||||
;; :unit "Mw"
|
||||
:unit "Mw"
|
||||
;; :min-value 20
|
||||
;; :warn-value 35
|
||||
;; :max-value 40
|
||||
;; :alarm-class "snm-warning"
|
||||
;; :max-value (aget js/Math "PI")
|
||||
:tolerance 2
|
||||
:alarm-class "snm-warning"
|
||||
:width "350px"]
|
||||
[title :level :level3 :label "Parameters"]
|
||||
[h-box
|
||||
|
|
Loading…
Reference in a new issue