├── .gitignore ├── LICENSE ├── README.md ├── docs ├── data │ ├── birdstrikes.json │ └── cars.json └── index.html ├── elm-package.json ├── examples ├── data │ ├── birdstrikes.json │ └── cars.json ├── elm-package.json └── src │ ├── Data │ ├── Birdstrikes.elm │ └── Cars.elm │ └── Main.elm └── src └── Facet ├── Axis.elm ├── Channel.elm ├── Encoding.elm ├── Field.elm ├── Helpers.elm ├── Internal ├── Axis.elm ├── Channel.elm ├── Encoding.elm ├── Encoding │ ├── Arc.elm │ ├── Area.elm │ ├── Fill.elm │ ├── Line.elm │ ├── Polygon.elm │ ├── Position.elm │ ├── Rect.elm │ ├── Rule.elm │ ├── Stroke.elm │ ├── Symbol.elm │ ├── Text.elm │ └── Trail.elm ├── Field.elm └── Legend.elm ├── List └── Extra.elm ├── Maybe └── Extra.elm ├── Plot.elm └── Scale.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | ./examples/elm-stuff 3 | ./examples/elm.js 4 | *.js 5 | *.html 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Michael Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Facet Plot 2 | 3 | A plotting library inspired by the _Grammar of Graphics_ and various 4 | implementations including [ggplot](http://ggplot2.tidyverse.org/index.html) 5 | and [vega](https://vega.github.io/vega/). 6 | 7 | The main idea behind the library is that a wide variety of different plots 8 | can be created by composing a small set of primitive visual marks and that 9 | data can be encoded as some visual attribute of those marks. Some [examples 10 | created with the library can be found here.](https://enetsee.github.io/facet-plot-alpha/) 11 | 12 | Once a plot is declared, it can be 'compiled' with some appropriate data to 13 | generate a [`Scenegraph`](https://github.com/enetsee/facet-scenegraph-alpha). 14 | 15 | The `Scenegraph` can then be rendered with any back-end. At the moment the only 16 | available rendering is for [SVG](https:/github.com/enetsee/facet-render-svg-alpha) 17 | but over time I may look to create back-ends for Canvas and WebGL. 18 | 19 | `Facet` also supports [theming](https://github.com/enetsee/facet-theme-alpha) 20 | i.e. creating a set of default styles to be applied to non-data attributes of a plot. 21 | 22 | The key abstractions that support this are outlined below. 23 | 24 | ## NOTE 25 | 26 | As indicated by the name, this library is very much in development. I have 27 | open sourced it now since I want to use it in a work project and would like 28 | help and feedback on the API. 29 | 30 | ## Plot 31 | 32 | A `Plot` allows you to combine several layers of `Encodings` along with 33 | the corresponding `Legends` and `Axis`. 34 | 35 | In addition, you can specify how the plot should be facetted to create 36 | [small multiples](https://en.wikipedia.org/wiki/Small_multiple). 37 | 38 | ### Axis 39 | 40 | An `Axis` is a special type of `Legend` for `PositionalChannel`s which 41 | shows the user-defined mapping between data and an on-screen position. 42 | 43 | 44 | ### Legend 45 | 46 | A visualization of the user-defined mapping between data and some visual 47 | aspect of a mark. 48 | 49 | ### Facet 50 | 51 | Faceting a plot creates series of similar plots (or 'small multiples') 52 | sharing the same scale and axes, allowing them to be easily compared. 53 | 54 | A plot can be facetted by one `Field` to create either a row or column of 55 | small multiples. 56 | 57 | A plot can also be facetted by two `Field`s to create a grid of small multiples. 58 | 59 | 60 | ## Encoding 61 | 62 | An `Encoding` is a means of encoding data as visual mark by combining 63 | several `Channel`s to represent various attributes of that visual mark. 64 | 65 | A description of each encoding along with the required and optional `Channels` 66 | is given below. 67 | 68 | ### Arc 69 | 70 | A circular arc. 71 | 72 | #### Required channels 73 | 74 | - x position (`PositionalChannel`) 75 | - y position (`PositionalChannel`) 76 | - start angle in Radians (`FloatChannel`) 77 | - end angle in Radians (`FloatChannel`) 78 | - outer radius in user-space pixels (`FloatChannel`) 79 | 80 | #### Optional channels 81 | 82 | - inner radius in user-space pixels (`FloatChannel`) 83 | - corner radius in user-space pixels (`FloatChannel`) 84 | - fill color (`ColorChannel`) 85 | - fill opacity, between 0 and 1 (`FloatChannel`) 86 | - stroke color (`ColorChannel`) 87 | - stroke opacity, between 0 and 1 (`FloatChannel`) 88 | - stroke width in user-space pixels (`FloatChannel`) 89 | - stroke dash (`StrokeDashChannel`) 90 | - tooltip (`TextChannel`) 91 | 92 | ### Area 93 | 94 | Filled area with either vertical or horizontal orientation. 95 | 96 | #### Required channels 97 | 98 | - x positions (`PositionalChannel`) 99 | - y positions (`PositionalChannel`) 100 | 101 | You must also provide an interpolation method and the preferred behaviour 102 | when missing values are encountered 103 | 104 | #### Optional channels 105 | 106 | - fill color (`ColorChannel`) 107 | - fill opacity, between 0 and 1 (`FloatChannel`) 108 | - stroke color (`ColorChannel`) 109 | - stroke opacity, between 0 and 1 (`FloatChannel`) 110 | - stroke width in user-space pixels (`FloatChannel`) 111 | - stroke dash (`StrokeDashChannel`) 112 | - tooltip (`TextChannel`) 113 | 114 | ### Line 115 | 116 | Stroked lines. 117 | 118 | #### Required channels 119 | 120 | - x positions (`PositionalChannel`) 121 | - y positions (`PositionalChannel`) 122 | 123 | You must also provide an interpolation method and the preferred behaviour 124 | when missing values are encountered 125 | 126 | #### Optional channels 127 | 128 | - stroke color (`ColorChannel`) 129 | - stroke opacity, between 0 and 1 (`FloatChannel`) 130 | - stroke width in user-space pixels (`FloatChannel`) 131 | - stroke dash (`StrokeDashChannel`) 132 | - tooltip (`TextChannel`) 133 | 134 | ### Polygon 135 | 136 | Arbitrary filled polygons. 137 | 138 | #### Required channels 139 | 140 | - x positions (`PositionalChannel`) 141 | - y positions (`PositionalChannel`) 142 | 143 | You must also provide an interpolation method and the preferred behaviour 144 | when missing values are encountered 145 | 146 | #### Optional channels 147 | 148 | - fill color (`ColorChannel`) 149 | - fill opacity, between 0 and 1 (`FloatChannel`) 150 | - stroke color (`ColorChannel`) 151 | - stroke opacity, between 0 and 1 (`FloatChannel`) 152 | - stroke width in user-space pixels (`FloatChannel`) 153 | - stroke dash (`StrokeDashChannel`) 154 | - tooltip (`TextChannel`) 155 | 156 | ### Rect 157 | 158 | Filled rectangles. 159 | 160 | #### Required channels 161 | 162 | Either 163 | - primary and secondary x and y positions 164 | or 165 | - primary x and y positions, width and height 166 | 167 | #### Optional channels 168 | 169 | - corner radius in user-space pixels (`FloatChannel`) 170 | - fill color (`ColorChannel`) 171 | - fill opacity, between 0 and 1 (`FloatChannel`) 172 | - stroke color (`ColorChannel`) 173 | - stroke opacity, between 0 and 1 (`FloatChannel`) 174 | - stroke width in user-space pixels (`FloatChannel`) 175 | - stroke dash (`StrokeDashChannel`) 176 | - tooltip (`TextChannel`) 177 | 178 | ### Rule 179 | 180 | Stroked line segments. 181 | 182 | #### Required channels 183 | 184 | - primary and secondary x positions (`PositionalChannel`) 185 | - primary and secondary y positions (`PositionalChannel`) 186 | 187 | #### Optional channels 188 | 189 | - stroke color (`ColorChannel`) 190 | - stroke opacity, between 0 and 1 (`FloatChannel`) 191 | - stroke width in user-space pixels (`FloatChannel`) 192 | - stroke dash (`StrokeDashChannel`) 193 | - tooltip (`TextChannel`) 194 | 195 | ### Symbol 196 | 197 | Plotting symbols, including circles, squares and other shapes. 198 | 199 | #### Required channels 200 | 201 | - shape (`ShapeChannel`) 202 | - x position (`PositionalChannel`) 203 | - y position (`PositionalChannel`) 204 | 205 | #### Optional channels 206 | 207 | - size in user-space pixels squared (`FloatChannel`) 208 | - angle in Radians (`FloatChannel`) 209 | - fill color (`ColorChannel`) 210 | - fill opacity, between 0 and 1 (`FloatChannel`) 211 | - stroke color (`ColorChannel`) 212 | - stroke opacity, between 0 and 1 (`FloatChannel`) 213 | - stroke width in user-space pixels (`FloatChannel`) 214 | - stroke dash (`StrokeDashChannel`) 215 | - tooltip (`TextChannel`) 216 | 217 | ### Text 218 | 219 | Text labels with configurable fonts, alignment and angle. 220 | 221 | #### Required Channels 222 | 223 | - text (`TextChannel`) 224 | - x position (`PositionalChannel`) 225 | - y position (`PositionalChannel`) 226 | 227 | 228 | #### Optional channels 229 | 230 | - size in user-space pixels squared (`FloatChannel`) 231 | - angle in Radians (`FloatChannel`) 232 | - fill color (`ColorChannel`) 233 | - fill opacity, between 0 and 1 (`FloatChannel`) 234 | - stroke color (`ColorChannel`) 235 | - stroke opacity, between 0 and 1 (`FloatChannel`) 236 | - stroke width in user-space pixels (`FloatChannel`) 237 | - stroke dash (`StrokeDashChannel`) 238 | - tooltip (`TextChannel`) 239 | 240 | ### Trail 241 | 242 | Filled lines with varying width. 243 | 244 | #### Required Channels 245 | 246 | - widths (`FloatChannel`) 247 | - x positions (`PositionalChannel`) 248 | - y positions (`PositionalChannel`) 249 | 250 | 251 | #### Optional channels 252 | 253 | - fill color (`ColorChannel`) 254 | - fill opacity, between 0 and 1 (`FloatChannel`) 255 | - tooltip (`TextChannel`) 256 | 257 | ## Channel 258 | 259 | A `Channel` is a means representing data as some attribute of a visual mark by 260 | specify a `Field` to extract data and a `Scale` to transform it to the type 261 | required for that `Channel` 262 | 263 | Available channels are summarized below. Each channel corresponds with the type 264 | required by some visual attribute of mark. 265 | 266 | ### Positional Channel 267 | 268 | A `PositionalChannel` is used to associate a data value with a position on either 269 | the x- or y-axis. 270 | 271 | ### Angle Channel 272 | 273 | An `AngleChannel` is used to encode data as the rotation of a visual mark. 274 | 275 | ### Color Channel 276 | 277 | A `ColorChannel` is used to encode data as either the fill color or stroke color 278 | of a visual mark. 279 | 280 | ### Float Channel 281 | 282 | A `FloatChannel` is used to encode data as some non-positional numeric attribute 283 | of a visual mark e.g. stroke width, size, font size. 284 | 285 | ### Int Channel 286 | 287 | A `IntChannel` is used to encode data as some non-positional numeric attribute 288 | of a visual mark e.g. stroke width, size, font size. 289 | 290 | ### Shape Channel 291 | 292 | A `ShapeChannel` is used to encode data as the shape used in a `Symbol` visual 293 | mark. 294 | 295 | ### Text Channel 296 | 297 | A `TextChannel` is used to encode data as the text of a `Text` mark or as the 298 | tooltip of any visual mark. 299 | 300 | ### Stroke-dash Channel 301 | 302 | A `StrokeDashChannel` is used to encode data as the stroke dash array 303 | and (optional) stroke dash offset of a visual mark. 304 | 305 | ## Scale 306 | 307 | A scale provides a means of mapping between values of type _domain_ to 308 | values of type _range_. 309 | 310 | Scales allow you to specify how data gets transformed after being extracted 311 | by a `Field`. 312 | 313 | ## Field 314 | 315 | A `Field` is a means of extracting a value from some data type. 316 | 317 | There are different `Field`s allowing you to extract different 'shapes' 318 | of data: 319 | - A scalar `Field` extracts single item from a single piece of data; 320 | - A vector `Field` extracts a list of items from a list of data; 321 | - An aggregate `Field` summarizes a list of data as a single item. 322 | 323 | In addition, each type of field supports situations where the item you 324 | are extracting may be missing. 325 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.2", 3 | "summary": "A plotting library inspired by the _Grammar of Graphics_.", 4 | "repository": "https://github.com/enetsee/facet-plot-alpha.git", 5 | "license": "MIT", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Facet.Axis", 11 | "Facet.Channel", 12 | "Facet.Encoding", 13 | "Facet.Field", 14 | "Facet.Plot", 15 | "Facet.Scale" 16 | ], 17 | "dependencies": { 18 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 19 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 20 | "enetsee/elm-scale": "1.0.8 <= v < 2.0.0", 21 | "enetsee/facet-scenegraph-alpha": "1.0.0 <= v < 2.0.0", 22 | "enetsee/facet-theme-alpha": "1.0.0 <= v < 2.0.0", 23 | "folkertdev/svg-path-lowlevel": "1.0.1 <= v < 2.0.0" 24 | }, 25 | "elm-version": "0.18.0 <= v < 0.19.0" 26 | } 27 | -------------------------------------------------------------------------------- /examples/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", 13 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 14 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 15 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 16 | "elm-lang/svg": "2.0.0 <= v < 3.0.0", 17 | "enetsee/facet-scenegraph-alpha": "1.0.0 <= v < 2.0.0", 18 | "enetsee/facet-theme-alpha": "1.0.0 <= v < 2.0.0", 19 | "enetsee/facet-render-svg-alpha": "1.0.0 <= v < 2.0.0", 20 | "enetsee/elm-scale": "1.0.8 <= v < 2.0.0", 21 | "folkertdev/one-true-path-experiment": "3.0.2 <= v < 4.0.0", 22 | "folkertdev/svg-path-lowlevel": "1.0.1 <= v < 2.0.0" 23 | }, 24 | "elm-version": "0.18.0 <= v < 0.19.0" 25 | } 26 | -------------------------------------------------------------------------------- /examples/src/Data/Birdstrikes.elm: -------------------------------------------------------------------------------- 1 | module Data.Birdstrikes exposing (Birdstrike, Damage, compareDamage, PhaseOfFlight, comparePhaseOfFlight, Size, compareSize, TimeOfDay, compareTimeOfDay, birdstrikes) 2 | 3 | import Date exposing (Date) 4 | import Date exposing (Date) 5 | import Http 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Decode.Pipeline as Decode 8 | import Task exposing (Task) 9 | 10 | 11 | type alias Birdstrike = 12 | { airportName : String 13 | , aircraftMakeModel : String 14 | , effectAmountOfDamage : Damage 15 | , flightDate : Date 16 | , aircraftAirlineOperator : String 17 | , originState : String 18 | , whenPhaseOfFlight : PhaseOfFlight 19 | , wildlifeSize : Size 20 | , wildlifeSpecies : String 21 | , whenTimeOfDay : TimeOfDay 22 | , costOther : Float 23 | , costRepair : Float 24 | , costTotal : Float 25 | , speedIASinKnots : Maybe Float 26 | } 27 | 28 | 29 | birdstrikes : () -> Task Http.Error (List Birdstrike) 30 | birdstrikes () = 31 | Http.get 32 | "https://enetsee.github.io/facet-plot-alpha/data/birdstrikes.json" 33 | (Decode.list decode) 34 | |> Http.toTask 35 | 36 | 37 | decode : Decoder Birdstrike 38 | decode = 39 | Decode.decode Birdstrike 40 | |> Decode.required "Airport__Name" Decode.string 41 | |> Decode.required "Aircraft__Make_Model" Decode.string 42 | |> Decode.required "Effect__Amount_of_damage" decodeDamage 43 | |> Decode.required "Flight_Date" decodeDate 44 | |> Decode.required "Aircraft__Airline_Operator" Decode.string 45 | |> Decode.required "Origin_State" Decode.string 46 | |> Decode.required "When__Phase_of_flight" decodePhaseOfFlight 47 | |> Decode.required "Wildlife__Size" decodeSize 48 | |> Decode.required "Wildlife__Species" Decode.string 49 | |> Decode.required "When__Time_of_day" decodeTimeOfDay 50 | |> Decode.required "Cost__Other" Decode.float 51 | |> Decode.required "Cost__Repair" Decode.float 52 | |> Decode.required "Cost__Total_$" Decode.float 53 | |> Decode.required "Speed_IAS_in_knots" (Decode.nullable Decode.float) 54 | 55 | 56 | decodeDate : Decoder Date 57 | decodeDate = 58 | Decode.andThen 59 | (\dateStr -> 60 | case Date.fromString dateStr of 61 | Ok d -> 62 | Decode.succeed d 63 | 64 | Err err -> 65 | Decode.fail err 66 | ) 67 | Decode.string 68 | 69 | 70 | decodeResult : Result String a -> Decoder a 71 | decodeResult result = 72 | case result of 73 | Ok x -> 74 | Decode.succeed x 75 | 76 | Err x -> 77 | Decode.fail x 78 | 79 | 80 | 81 | -- Time of Day -- 82 | 83 | 84 | type TimeOfDay 85 | = Dawn 86 | | Day 87 | | Dusk 88 | | Night 89 | 90 | 91 | decodeTimeOfDay : Decoder TimeOfDay 92 | decodeTimeOfDay = 93 | Decode.string 94 | |> Decode.andThen (timeOfDayFromString >> decodeResult) 95 | 96 | 97 | timeOfDayFromString : String -> Result String TimeOfDay 98 | timeOfDayFromString str = 99 | case str of 100 | "Dawn" -> 101 | Ok Dawn 102 | 103 | "Day" -> 104 | Ok Day 105 | 106 | "Dusk" -> 107 | Ok Dusk 108 | 109 | "Night" -> 110 | Ok Night 111 | 112 | _ -> 113 | Err str 114 | 115 | 116 | timeOfDayOrd : TimeOfDay -> number 117 | timeOfDayOrd timeOfDay = 118 | case timeOfDay of 119 | Dawn -> 120 | 0 121 | 122 | Day -> 123 | 1 124 | 125 | Dusk -> 126 | 2 127 | 128 | Night -> 129 | 3 130 | 131 | 132 | compareTimeOfDay : TimeOfDay -> TimeOfDay -> Order 133 | compareTimeOfDay d1 d2 = 134 | compare (timeOfDayOrd d1) (timeOfDayOrd d2) 135 | 136 | 137 | 138 | -- Damage -- 139 | 140 | 141 | type Damage 142 | = None 143 | | B 144 | | C 145 | | Minor 146 | | DamageMedium 147 | | Substantial 148 | 149 | 150 | decodeDamage : Decoder Damage 151 | decodeDamage = 152 | Decode.string 153 | |> Decode.andThen (damageFromString >> decodeResult) 154 | 155 | 156 | damageFromString : String -> Result String Damage 157 | damageFromString str = 158 | case str of 159 | "None" -> 160 | Ok None 161 | 162 | "B" -> 163 | Ok B 164 | 165 | "C" -> 166 | Ok C 167 | 168 | "Minor" -> 169 | Ok Minor 170 | 171 | "Medium" -> 172 | Ok DamageMedium 173 | 174 | "Substantial" -> 175 | Ok Substantial 176 | 177 | _ -> 178 | Err str 179 | 180 | 181 | compareDamage : Damage -> Damage -> Order 182 | compareDamage d1 d2 = 183 | compare (damageOrd d1) (damageOrd d2) 184 | 185 | 186 | damageOrd : Damage -> number 187 | damageOrd damage = 188 | case damage of 189 | None -> 190 | 0 191 | 192 | B -> 193 | 1 194 | 195 | C -> 196 | 2 197 | 198 | Minor -> 199 | 3 200 | 201 | DamageMedium -> 202 | 4 203 | 204 | Substantial -> 205 | 5 206 | 207 | 208 | 209 | -- Phase of fligh -- 210 | 211 | 212 | type PhaseOfFlight 213 | = Parked 214 | | Taxi 215 | | TakeoffRun 216 | | Climb 217 | | Approach 218 | | Descent 219 | | LandingRoll 220 | 221 | 222 | decodePhaseOfFlight : Decoder PhaseOfFlight 223 | decodePhaseOfFlight = 224 | Decode.string 225 | |> Decode.andThen (phaseOfFlightFromString >> decodeResult) 226 | 227 | 228 | phaseOfFlightFromString : String -> Result String PhaseOfFlight 229 | phaseOfFlightFromString str = 230 | case str of 231 | "Parked" -> 232 | Ok Parked 233 | 234 | "Taxi" -> 235 | Ok Taxi 236 | 237 | "Take-off run" -> 238 | Ok TakeoffRun 239 | 240 | "Climb" -> 241 | Ok Climb 242 | 243 | "Approach" -> 244 | Ok Approach 245 | 246 | "Descent" -> 247 | Ok Descent 248 | 249 | "Landing Roll" -> 250 | Ok LandingRoll 251 | 252 | _ -> 253 | Err str 254 | 255 | 256 | comparePhaseOfFlight : PhaseOfFlight -> PhaseOfFlight -> Order 257 | comparePhaseOfFlight d1 d2 = 258 | compare (phaseOfFlightOrd d1) (phaseOfFlightOrd d2) 259 | 260 | 261 | phaseOfFlightOrd : PhaseOfFlight -> number 262 | phaseOfFlightOrd phaseOfFlight = 263 | case phaseOfFlight of 264 | Parked -> 265 | 0 266 | 267 | Taxi -> 268 | 1 269 | 270 | TakeoffRun -> 271 | 2 272 | 273 | Climb -> 274 | 3 275 | 276 | Approach -> 277 | 4 278 | 279 | Descent -> 280 | 5 281 | 282 | LandingRoll -> 283 | 6 284 | 285 | 286 | 287 | -- Size -- 288 | 289 | 290 | type Size 291 | = Small 292 | | Medium 293 | | Large 294 | 295 | 296 | decodeSize : Decoder Size 297 | decodeSize = 298 | Decode.string 299 | |> Decode.andThen (sizeFromStr >> decodeResult) 300 | 301 | 302 | sizeFromStr : String -> Result String Size 303 | sizeFromStr str = 304 | case str of 305 | "Small" -> 306 | Ok Small 307 | 308 | "Medium" -> 309 | Ok Medium 310 | 311 | "Large" -> 312 | Ok Large 313 | 314 | _ -> 315 | Err str 316 | 317 | 318 | compareSize : Size -> Size -> Order 319 | compareSize d1 d2 = 320 | compare (sizeOrd d1) (sizeOrd d2) 321 | 322 | 323 | sizeOrd : Size -> number 324 | sizeOrd size = 325 | case size of 326 | Small -> 327 | 0 328 | 329 | Medium -> 330 | 1 331 | 332 | Large -> 333 | 2 334 | -------------------------------------------------------------------------------- /examples/src/Data/Cars.elm: -------------------------------------------------------------------------------- 1 | module Data.Cars exposing (Car, cars) 2 | 3 | import Date exposing (Date) 4 | import Http 5 | import Json.Decode as Decode exposing (Decoder) 6 | import Json.Decode.Pipeline as Decode 7 | import Task exposing (Task) 8 | 9 | 10 | cars : () -> Task Http.Error (List Car) 11 | cars () = 12 | Http.get 13 | "https://enetsee.github.io/facet-plot-alpha/data/cars.json" 14 | (Decode.list decode) 15 | |> Http.toTask 16 | 17 | 18 | {-| Example data from https://vega.github.io/vega-editor/app/data/cars.json 19 | -} 20 | type alias Car = 21 | { name : String 22 | , milesPerGallon : Maybe Float 23 | , cylinders : Int 24 | , displacement : Float 25 | , horsepower : Maybe Int 26 | , weightInLbs : Int 27 | , acceleration : Float 28 | , year : Maybe Date 29 | , origin : String 30 | } 31 | 32 | 33 | decode : Decoder Car 34 | decode = 35 | Decode.decode Car 36 | |> Decode.required "Name" Decode.string 37 | |> Decode.required "Miles_per_Gallon" (Decode.nullable Decode.float) 38 | |> Decode.required "Cylinders" Decode.int 39 | |> Decode.required "Displacement" Decode.float 40 | |> Decode.required "Horsepower" (Decode.nullable Decode.int) 41 | |> Decode.required "Weight_in_lbs" Decode.int 42 | |> Decode.required "Acceleration" Decode.float 43 | |> Decode.required "Year" (Decode.nullable decodeDate) 44 | |> Decode.required "Origin" Decode.string 45 | 46 | 47 | decodeDate : Decoder Date 48 | decodeDate = 49 | Decode.andThen 50 | (\dateStr -> 51 | case Date.fromString dateStr of 52 | Ok d -> 53 | Decode.succeed d 54 | 55 | Err err -> 56 | Decode.fail err 57 | ) 58 | Decode.string 59 | -------------------------------------------------------------------------------- /src/Facet/Axis.elm: -------------------------------------------------------------------------------- 1 | module Facet.Axis 2 | exposing 3 | ( Axis 4 | , continuousX 5 | , continuousY 6 | , discreteX 7 | , discreteY 8 | , bandX 9 | , bandY 10 | , customBandX 11 | , customBandY 12 | , linearX 13 | , linearY 14 | , log10X 15 | , log10Y 16 | , sqrtX 17 | , sqrtY 18 | , continuousScale 19 | , ordinalScale 20 | , continuousDomain 21 | , ordinalDomain 22 | , ticks 23 | , labelFormat 24 | , labelAngle 25 | , orientTop 26 | , orientBottom 27 | , orientLeft 28 | , orientRight 29 | , Vertical 30 | , Horizontal 31 | ) 32 | 33 | {-| 34 | @docs Axis, Horizontal , Vertical 35 | 36 | @docs continuousX, continuousY 37 | 38 | @docs linearX, linearY, log10X, log10Y, sqrtX, sqrtY 39 | 40 | @docs discreteX, discreteY 41 | 42 | @docs bandX, customBandX , bandY, customBandY 43 | 44 | @docs continuousScale, ordinalScale 45 | 46 | @docs continuousDomain, ordinalDomain 47 | 48 | @docs ticks, labelFormat,orientTop, orientBottom, orientLeft,orientRight, labelAngle 49 | 50 | -} 51 | 52 | import Facet.Internal.Axis as Axis 53 | import Facet.Scale exposing (Scale) 54 | 55 | 56 | {-| An `Axis` is a special type of `Legend` for positional channels 57 | -} 58 | type alias Axis orientation domain = 59 | Axis.Axis orientation domain 60 | 61 | 62 | {-| -} 63 | type alias Horizontal = 64 | Axis.Horizontal 65 | 66 | 67 | {-| -} 68 | type alias Vertical = 69 | Axis.Vertical 70 | 71 | 72 | {-| Create an x-axis with a continuous domain, defined by an upper and lower 73 | limit. 74 | -} 75 | continuousX : 76 | { r 77 | | title : Maybe String 78 | , scale : ( xdomain, xdomain ) -> ( Float, Float ) -> Scale xdomain Float 79 | } 80 | -> Axis Vertical xdomain 81 | continuousX { title, scale } = 82 | Axis.continuousX title scale 83 | 84 | 85 | {-| Create a y-axis with a continuos domain, defined by an upper and lower 86 | limit. 87 | -} 88 | continuousY : 89 | { r 90 | | title : Maybe String 91 | , scale : ( ydomain, ydomain ) -> ( Float, Float ) -> Scale ydomain Float 92 | } 93 | -> Axis Horizontal ydomain 94 | continuousY { title, scale } = 95 | Axis.continuousY title scale 96 | 97 | 98 | {-| Create an x-axis with a discrete domain, defined by the list of elements 99 | in the domain. 100 | -} 101 | discreteX : 102 | { r 103 | | title : Maybe String 104 | , scale : List xdomain -> ( Float, Float ) -> Scale xdomain Float 105 | } 106 | -> Axis Vertical xdomain 107 | discreteX { title, scale } = 108 | Axis.discreteX title scale 109 | 110 | 111 | {-| Create an x-axis with a discrete domain, explicitly providing a function 112 | to construct an ordinal scale 113 | -} 114 | discreteY : 115 | { r 116 | | title : Maybe String 117 | , scale : List ydomain -> ( Float, Float ) -> Scale ydomain Float 118 | } 119 | -> Axis Horizontal ydomain 120 | discreteY { title, scale } = 121 | Axis.discreteY title scale 122 | 123 | 124 | {-| Create an x-axis with a band scale from a comparable domain 125 | -} 126 | bandX : Maybe String -> Axis.Axis Vertical comparableDomain 127 | bandX title = 128 | Axis.bandX title 129 | 130 | 131 | {-| Create a y-axis with a band scale from a comparable domain 132 | -} 133 | bandY : Maybe String -> Axis.Axis Horizontal comparableDomain 134 | bandY title = 135 | Axis.bandY title 136 | 137 | 138 | {-| Create an x-axis with a band scale from an arbitrary domain 139 | -} 140 | customBandX : Maybe String -> Axis.Axis Vertical anyDomain 141 | customBandX title = 142 | Axis.customBandX title 143 | 144 | 145 | {-| -} 146 | customBandY : Maybe String -> Axis.Axis Horizontal anyDomain 147 | customBandY title = 148 | Axis.customBandY title 149 | 150 | 151 | {-| -} 152 | linearX : Maybe String -> Axis Vertical Float 153 | linearX title = 154 | Axis.linearX title 155 | 156 | 157 | {-| -} 158 | linearY : Maybe String -> Axis Horizontal Float 159 | linearY title = 160 | Axis.linearY title 161 | 162 | 163 | {-| -} 164 | log10X : Maybe String -> Axis Vertical Float 165 | log10X title = 166 | Axis.log10X title 167 | 168 | 169 | {-| -} 170 | log10Y : Maybe String -> Axis Horizontal Float 171 | log10Y title = 172 | Axis.log10Y title 173 | 174 | 175 | {-| -} 176 | sqrtX : Maybe String -> Axis Vertical Float 177 | sqrtX title = 178 | Axis.sqrtX title 179 | 180 | 181 | {-| -} 182 | sqrtY : Maybe String -> Axis Horizontal Float 183 | sqrtY title = 184 | Axis.sqrtY title 185 | 186 | 187 | 188 | -- modifiers ------------------------------------------------------------------- 189 | 190 | 191 | {-| -} 192 | continuousScale : 193 | (( domain, domain ) -> ( Float, Float ) -> Scale domain Float) 194 | -> Axis orientation domain 195 | -> Axis orientation domain 196 | continuousScale scale axis = 197 | Axis.continuousScale scale axis 198 | 199 | 200 | {-| -} 201 | ordinalScale : 202 | (List domain -> ( Float, Float ) -> Scale domain Float) 203 | -> Axis orientation domain 204 | -> Axis orientation domain 205 | ordinalScale scale axis = 206 | Axis.ordinalScale scale axis 207 | 208 | 209 | {-| -} 210 | continuousDomain : ( domain, domain ) -> Axis orientation domain -> Axis orientation domain 211 | continuousDomain domain axis = 212 | Axis.continuousDomain domain axis 213 | 214 | 215 | {-| -} 216 | ordinalDomain : List domain -> Axis orientation domain -> Axis orientation domain 217 | ordinalDomain domain axis = 218 | Axis.ordinalDomain domain axis 219 | 220 | 221 | {-| -} 222 | labelFormat : (domain -> String) -> Axis orientation domain -> Axis orientation domain 223 | labelFormat format axis = 224 | Axis.format format axis 225 | 226 | 227 | {-| -} 228 | labelAngle : Float -> Axis.Axis orientation domain -> Axis.Axis orientation domain 229 | labelAngle angle axis = 230 | Axis.labelAngle angle axis 231 | 232 | 233 | {-| -} 234 | ticks : Int -> Axis orientation domain -> Axis orientation domain 235 | ticks numTicks axis = 236 | Axis.ticks numTicks axis 237 | 238 | 239 | {-| -} 240 | orientLeft : Axis Horizontal domain -> Axis Horizontal domain 241 | orientLeft axis = 242 | Axis.orientLeft axis 243 | 244 | 245 | {-| -} 246 | orientRight : Axis Horizontal domain -> Axis Horizontal domain 247 | orientRight axis = 248 | Axis.orientRight axis 249 | 250 | 251 | {-| -} 252 | orientTop : Axis Vertical domain -> Axis Vertical domain 253 | orientTop axis = 254 | Axis.orientTop axis 255 | 256 | 257 | {-| -} 258 | orientBottom : Axis Vertical domain -> Axis Vertical domain 259 | orientBottom axis = 260 | Axis.orientBottom axis 261 | -------------------------------------------------------------------------------- /src/Facet/Channel.elm: -------------------------------------------------------------------------------- 1 | module Facet.Channel 2 | exposing 3 | ( PositionalChannel 4 | , ChannelWithLegend 5 | , AngleChannel 6 | , ColorChannel 7 | , FloatChannel 8 | , IntChannel 9 | , ShapeChannel 10 | , TextChannel 11 | , StrokeDashChannel 12 | , positionalCompareWith 13 | , positional 14 | , channelCompareWith 15 | , channel 16 | , angleCompareWith 17 | , angle 18 | , colorCompareWith 19 | , color 20 | , floatCompareWith 21 | , float 22 | , intCompareWith 23 | , int 24 | , shapeCompareWith 25 | , shape 26 | , textCompareWith 27 | , text 28 | , strokeDashCompareWith 29 | , strokeDash 30 | ) 31 | 32 | {-| 33 | 34 | # Channel 35 | 36 | A `Channel` is a means representing data as some attribute of a visual mark. 37 | 38 | ## Positional Channel 39 | @docs PositionalChannel, positional, positionalCompareWith 40 | 41 | @docs ChannelWithLegend, channel, channelCompareWith 42 | 43 | ## Angle Channel 44 | @docs AngleChannel, angle, angleCompareWith 45 | 46 | ## Color Channel 47 | @docs ColorChannel, color, colorCompareWith 48 | 49 | ## Float Channel 50 | @docs FloatChannel, float, floatCompareWith 51 | 52 | ## Int Channel 53 | @docs IntChannel, int, intCompareWith 54 | 55 | ## Shape Channel 56 | @docs ShapeChannel, shape, shapeCompareWith 57 | 58 | ## Text Channel 59 | @docs TextChannel , text, textCompareWith 60 | 61 | ## Stroke-dash Channel 62 | @docs StrokeDashChannel, strokeDash, strokeDashCompareWith 63 | 64 | -} 65 | 66 | import Color exposing (Color) 67 | import Facet.Internal.Channel as Channel 68 | import Facet.Field exposing (Field) 69 | import Facet.Scale exposing (Scale) 70 | import Facet.Scenegraph.Shape exposing (Shape) 71 | import Facet.Scenegraph.Stroke exposing (StrokeDash) 72 | 73 | 74 | {-| A `PositionalChannel` is used to associate a data value with a position on either 75 | the x- or y-axis. 76 | -} 77 | type alias PositionalChannel data domain = 78 | Channel.PositionalChannel data domain 79 | 80 | 81 | {-| -} 82 | positional : 83 | Field data comparableDomain 84 | -> PositionalChannel data comparableDomain 85 | positional field = 86 | Channel.positional field 87 | 88 | 89 | {-| -} 90 | positionalCompareWith : 91 | { a 92 | | compareWith : domain -> domain -> Order 93 | , field : Field data domain 94 | } 95 | -> PositionalChannel data domain 96 | positionalCompareWith { compareWith, field } = 97 | Channel.positionalCompareWith compareWith field 98 | 99 | 100 | {-| -} 101 | type alias ChannelWithLegend data range = 102 | Channel.ChannelWithLegend data range 103 | 104 | 105 | {-| -} 106 | channelCompareWith : 107 | { a 108 | | compareWith : domain -> domain -> Order 109 | , formatDomain : domain -> String 110 | , scale : Scale domain range 111 | , field : Field data domain 112 | } 113 | -> ChannelWithLegend data range 114 | channelCompareWith { compareWith, formatDomain, scale, field } = 115 | Channel.channelCompareWith compareWith formatDomain scale field 116 | 117 | 118 | {-| -} 119 | channel : 120 | { a 121 | | formatDomain : comparableDomain -> String 122 | , scale : Scale comparableDomain range 123 | , field : Field data comparableDomain 124 | } 125 | -> ChannelWithLegend data range 126 | channel { formatDomain, scale, field } = 127 | Channel.channel formatDomain scale field 128 | 129 | 130 | {-| An `AngleChannel` is used to encode data as the rotation of a visual mark. 131 | -} 132 | type alias AngleChannel data = 133 | Channel.AngleChannel data 134 | 135 | 136 | {-| -} 137 | angle : 138 | { a 139 | | formatDomain : comparableDomain -> String 140 | , scale : Scale comparableDomain Float 141 | , field : Field data comparableDomain 142 | } 143 | -> AngleChannel data 144 | angle { formatDomain, scale, field } = 145 | Channel.angle formatDomain scale field 146 | 147 | 148 | {-| -} 149 | angleCompareWith : 150 | { a 151 | | compareWith : domain -> domain -> Order 152 | , formatDomain : domain -> String 153 | , scale : Scale domain Float 154 | , field : Field data domain 155 | } 156 | -> AngleChannel data 157 | angleCompareWith { compareWith, formatDomain, scale, field } = 158 | Channel.angleCompareWith compareWith formatDomain scale field 159 | 160 | 161 | {-| A `ColorChannel` is used to encode data as either the fill color or stroke color 162 | of a visual mark. 163 | -} 164 | type alias ColorChannel data = 165 | Channel.ColorChannel data 166 | 167 | 168 | {-| -} 169 | color : 170 | { a 171 | | formatDomain : comparableDomain -> String 172 | , scale : Scale comparableDomain Color 173 | , field : Field data comparableDomain 174 | } 175 | -> ColorChannel data 176 | color { formatDomain, scale, field } = 177 | Channel.color formatDomain scale field 178 | 179 | 180 | {-| -} 181 | colorCompareWith : 182 | { a 183 | | compareWith : domain -> domain -> Order 184 | , formatDomain : domain -> String 185 | , scale : Scale domain Color 186 | , field : Field data domain 187 | } 188 | -> ColorChannel data 189 | colorCompareWith { compareWith, formatDomain, scale, field } = 190 | Channel.colorCompareWith compareWith formatDomain scale field 191 | 192 | 193 | {-| A `FloatChannel` is used to encode data as some non-positional numeric attribute 194 | of a visual mark e.g. stroke width, size, font size. 195 | -} 196 | type alias FloatChannel data = 197 | Channel.FloatChannel data 198 | 199 | 200 | {-| -} 201 | float : 202 | { a 203 | | formatDomain : comparableDomain -> String 204 | , scale : Scale comparableDomain Float 205 | , field : Field data comparableDomain 206 | } 207 | -> FloatChannel data 208 | float { formatDomain, scale, field } = 209 | Channel.float formatDomain scale field 210 | 211 | 212 | {-| -} 213 | floatCompareWith : 214 | { a 215 | | compareWith : domain -> domain -> Order 216 | , formatDomain : domain -> String 217 | , scale : Scale domain Float 218 | , field : Field data domain 219 | } 220 | -> FloatChannel data 221 | floatCompareWith { compareWith, formatDomain, scale, field } = 222 | Channel.floatCompareWith compareWith formatDomain scale field 223 | 224 | 225 | {-| A `IntChannel` is used to encode data as some non-positional numeric attribute 226 | of a visual mark e.g. stroke width, size, font size. 227 | -} 228 | type alias IntChannel data = 229 | Channel.IntChannel data 230 | 231 | 232 | {-| -} 233 | int : 234 | { a 235 | | formatDomain : comparableDomain -> String 236 | , scale : Scale comparableDomain Int 237 | , field : Field data comparableDomain 238 | } 239 | -> IntChannel data 240 | int { formatDomain, scale, field } = 241 | Channel.int formatDomain scale field 242 | 243 | 244 | {-| -} 245 | intCompareWith : 246 | { a 247 | | compareWith : domain -> domain -> Order 248 | , formatDomain : domain -> String 249 | , scale : Scale domain Int 250 | , field : Field data domain 251 | } 252 | -> IntChannel data 253 | intCompareWith { compareWith, formatDomain, scale, field } = 254 | Channel.intCompareWith compareWith formatDomain scale field 255 | 256 | 257 | {-| A `ShapeChannel` is used to encode data as the shape used in a `Symbol` visual 258 | mark. 259 | -} 260 | type alias ShapeChannel data = 261 | Channel.ShapeChannel data 262 | 263 | 264 | {-| -} 265 | shape : 266 | { a 267 | | formatDomain : comparableDomain -> String 268 | , scale : Scale comparableDomain Shape 269 | , field : Field data comparableDomain 270 | } 271 | -> ShapeChannel data 272 | shape { formatDomain, scale, field } = 273 | Channel.shape formatDomain scale field 274 | 275 | 276 | {-| -} 277 | shapeCompareWith : 278 | { a 279 | | compareWith : domain -> domain -> Order 280 | , formatDomain : domain -> String 281 | , scale : Scale domain Shape 282 | , field : Field data domain 283 | } 284 | -> ShapeChannel data 285 | shapeCompareWith { compareWith, formatDomain, scale, field } = 286 | Channel.shapeCompareWith compareWith formatDomain scale field 287 | 288 | 289 | {-| A `StrokeDashChannel` is used to encode data as the stroke dash array 290 | and (optional) stroke dash offset of a visual mark. 291 | -} 292 | type alias StrokeDashChannel data = 293 | Channel.StrokeDashChannel data 294 | 295 | 296 | {-| -} 297 | strokeDash : 298 | { a 299 | | formatDomain : comparableDomain -> String 300 | , scale : Scale comparableDomain StrokeDash 301 | , field : Field data comparableDomain 302 | } 303 | -> StrokeDashChannel data 304 | strokeDash { formatDomain, scale, field } = 305 | Channel.strokeDash formatDomain scale field 306 | 307 | 308 | {-| -} 309 | strokeDashCompareWith : 310 | { a 311 | | compareWith : domain -> domain -> Order 312 | , formatDomain : domain -> String 313 | , scale : Scale domain StrokeDash 314 | , field : Field data domain 315 | } 316 | -> StrokeDashChannel data 317 | strokeDashCompareWith { compareWith, formatDomain, scale, field } = 318 | Channel.strokeDashCompareWith compareWith formatDomain scale field 319 | 320 | 321 | {-| A `TextChannel` is used to encode data as the text of a `Text` mark or as the 322 | tooltip of any visual mark. 323 | -} 324 | type alias TextChannel data = 325 | Channel.TextChannel data 326 | 327 | 328 | {-| -} 329 | text : 330 | { a 331 | | formatDomain : comparableDomain -> String 332 | , scale : Scale comparableDomain String 333 | , field : Field data comparableDomain 334 | } 335 | -> TextChannel data 336 | text { formatDomain, scale, field } = 337 | Channel.text formatDomain scale field 338 | 339 | 340 | {-| -} 341 | textCompareWith : 342 | { a 343 | | compareWith : domain -> domain -> Order 344 | , formatDomain : domain -> String 345 | , scale : Scale domain String 346 | , field : Field data domain 347 | } 348 | -> TextChannel data 349 | textCompareWith { compareWith, formatDomain, scale, field } = 350 | Channel.textCompareWith compareWith formatDomain scale field 351 | -------------------------------------------------------------------------------- /src/Facet/Field.elm: -------------------------------------------------------------------------------- 1 | module Facet.Field 2 | exposing 3 | ( Field 4 | , constant 5 | , scalar 6 | , maybeScalar 7 | , aggregate 8 | , maybeAggregate 9 | , vector 10 | , maybeVector 11 | ) 12 | 13 | {-| 14 | @docs Field 15 | 16 | @docs constant 17 | 18 | @docs scalar, maybeScalar 19 | 20 | @docs aggregate, maybeAggregate 21 | 22 | @docs vector, maybeVector 23 | -} 24 | 25 | import Facet.Internal.Field as Field 26 | 27 | 28 | {-| A `Field` is a means of extracting a value from some data type. 29 | 30 | There are different `Field`s allowing you to extract different 'shapes' 31 | of data: 32 | - A scalar `Field` extracts single item from a single piece of data; 33 | - A vector `Field` extracts a list of items from a list of data; 34 | - An aggregate `Field` summarizes a list of data as a single item. 35 | 36 | In addition, each type of field supports situations where the item you 37 | are extracting may be missing. 38 | -} 39 | type alias Field data domain = 40 | Field.Field data domain 41 | 42 | 43 | {-| -} 44 | constant : domain -> Field data domain 45 | constant value = 46 | Field.constant value 47 | 48 | 49 | {-| -} 50 | scalar : { name : Maybe String, extract : data -> domain } -> Field data domain 51 | scalar { name, extract } = 52 | Field.scalar name extract 53 | 54 | 55 | {-| -} 56 | maybeScalar : { a | name : Maybe String, extract : data -> Maybe domain } -> Field data domain 57 | maybeScalar { name, extract } = 58 | Field.maybeScalar name extract 59 | 60 | 61 | {-| -} 62 | maybeVector : { a | name : Maybe String, extract : List data -> List (Maybe domain) } -> Field data domain 63 | maybeVector { name, extract } = 64 | Field.maybeVector name extract 65 | 66 | 67 | {-| -} 68 | vector : { a | name : Maybe String, extract : List data -> List domain } -> Field data domain 69 | vector { name, extract } = 70 | Field.vector name extract 71 | 72 | 73 | {-| -} 74 | maybeAggregate : { a | name : Maybe String, extract : List data -> Maybe domain } -> Field data domain 75 | maybeAggregate { name, extract } = 76 | Field.maybeAggregate name extract 77 | 78 | 79 | {-| -} 80 | aggregate : { a | name : Maybe String, extract : List data -> domain } -> Field data domain 81 | aggregate { name, extract } = 82 | Field.aggregate name extract 83 | -------------------------------------------------------------------------------- /src/Facet/Helpers.elm: -------------------------------------------------------------------------------- 1 | module Facet.Helpers exposing (minWith, maxWith, compareMaybe) 2 | 3 | 4 | minWith : (a -> a -> Order) -> a -> a -> a 5 | minWith compareWith a b = 6 | case compareWith a b of 7 | LT -> 8 | a 9 | 10 | _ -> 11 | b 12 | 13 | 14 | maxWith : (a -> a -> Order) -> a -> a -> a 15 | maxWith compareWith a b = 16 | case compareWith a b of 17 | GT -> 18 | a 19 | 20 | _ -> 21 | b 22 | 23 | 24 | compareMaybe : (a -> Maybe b) -> (b -> b -> Order) -> a -> a -> Order 25 | compareMaybe extract compareWith d1 d2 = 26 | case ( extract d1, extract d2 ) of 27 | ( Just v1, Just v2 ) -> 28 | compareWith v1 v2 29 | 30 | ( Just _, _ ) -> 31 | GT 32 | 33 | ( _, Just _ ) -> 34 | LT 35 | 36 | _ -> 37 | EQ 38 | -------------------------------------------------------------------------------- /src/Facet/Internal/Channel.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Channel 2 | exposing 3 | ( PositionalChannel 4 | , ChannelWithLegend 5 | , AngleChannel 6 | , ColorChannel 7 | , FloatChannel 8 | , IntChannel 9 | , ShapeChannel 10 | , TextChannel 11 | , StrokeDashChannel 12 | , legend 13 | , extract 14 | , extractVector 15 | , summarize 16 | , isAggregate 17 | , isVector 18 | , compareAt 19 | , compareMaybeAt 20 | , equalAt 21 | , equalMaybeAt 22 | , positionalCompareWith 23 | , positional 24 | , channelCompareWith 25 | , channel 26 | , angleCompareWith 27 | , angle 28 | , colorCompareWith 29 | , color 30 | , floatCompareWith 31 | , float 32 | , intCompareWith 33 | , int 34 | , shapeCompareWith 35 | , shape 36 | , textCompareWith 37 | , text 38 | , strokeDashCompareWith 39 | , strokeDash 40 | ) 41 | 42 | import Color exposing (Color) 43 | import Facet.Internal.Field as Field exposing (Field) 44 | import Facet.Internal.Legend as Legend exposing (Legend) 45 | import Facet.Maybe.Extra as Maybe 46 | import Facet.Scale as Scale exposing (Scale) 47 | import Facet.Scenegraph.Shape exposing (Shape) 48 | import Facet.Scenegraph.Stroke exposing (StrokeDash) 49 | 50 | 51 | {-| 52 | A channel to encode data as position 53 | -} 54 | type alias PositionalChannel data domain = 55 | { title : Maybe String 56 | , isAggregate : Bool 57 | , isVector : Bool 58 | , fieldEqual : data -> data -> Bool 59 | , fieldCompare : data -> data -> Order 60 | , field : Field data domain 61 | , displayAxis : Bool 62 | , compareDomain : domain -> domain -> Order 63 | , limits : Maybe ( domain, domain ) 64 | } 65 | 66 | 67 | {-| 68 | -} 69 | type alias ChannelWithLegend data range = 70 | { title : Maybe String 71 | , isAggregate : Bool 72 | , isVector : Bool 73 | , fieldEqual : data -> data -> Bool 74 | , fieldCompare : data -> data -> Order 75 | , legend : Int -> Legend range 76 | , displayLegend : Bool 77 | , extract : data -> Maybe range 78 | , extractVector : List data -> List (Maybe range) 79 | , summarize : List data -> Maybe range 80 | } 81 | 82 | 83 | extract : ChannelWithLegend data range -> (data -> Maybe range) 84 | extract { extract } = 85 | extract 86 | 87 | 88 | extractVector : ChannelWithLegend data range -> (List data -> List (Maybe range)) 89 | extractVector { extractVector } = 90 | extractVector 91 | 92 | 93 | summarize : ChannelWithLegend data range -> (List data -> Maybe range) 94 | summarize { summarize } = 95 | summarize 96 | 97 | 98 | isVector : { b | isVector : a } -> a 99 | isVector { isVector } = 100 | isVector 101 | 102 | 103 | isAggregate : { b | isAggregate : a } -> a 104 | isAggregate { isAggregate } = 105 | isAggregate 106 | 107 | 108 | compareMaybeAt : Maybe { a | fieldCompare : data -> data -> Order } -> data -> data -> Order 109 | compareMaybeAt channel d1 d2 = 110 | Maybe.maybe 111 | EQ 112 | (\ch -> ch.fieldCompare d1 d2) 113 | channel 114 | 115 | 116 | compareAt : { a | fieldCompare : data -> data -> Order } -> data -> data -> Order 117 | compareAt channel d1 d2 = 118 | channel.fieldCompare d1 d2 119 | 120 | 121 | equalAt : { a | fieldEqual : data -> data -> Bool } -> data -> data -> Bool 122 | equalAt channel d1 d2 = 123 | channel.fieldEqual d1 d2 124 | 125 | 126 | equalMaybeAt : Maybe { a | fieldEqual : data -> data -> Bool } -> data -> data -> Bool 127 | equalMaybeAt maybeChannel d1 d2 = 128 | Maybe.maybe True (\ch -> ch.fieldEqual d1 d2) maybeChannel 129 | 130 | 131 | legend : Int -> ChannelWithLegend data a -> Maybe (Legend a) 132 | legend ticks channel = 133 | let 134 | legend = 135 | channel.legend ticks 136 | in 137 | if Legend.isEmpty legend then 138 | Nothing 139 | else 140 | Just legend 141 | 142 | 143 | 144 | -- Positional channels --------------------------------------------------------- 145 | 146 | 147 | {-| -} 148 | positional : 149 | Field data comparableDomain 150 | -> PositionalChannel data comparableDomain 151 | positional field = 152 | positionalCompareWith compare field 153 | 154 | 155 | {-| -} 156 | positionalCompareWith : 157 | (domain -> domain -> Order) 158 | -> Field data domain 159 | -> PositionalChannel data domain 160 | positionalCompareWith customCompare field = 161 | { title = Field.fieldName field 162 | , isAggregate = Field.isAggregate field 163 | , isVector = Field.isVector field 164 | , fieldEqual = Field.equalAt field 165 | , fieldCompare = Field.compareAt customCompare field 166 | , field = field 167 | , displayAxis = True 168 | , compareDomain = customCompare 169 | , limits = Nothing 170 | } 171 | 172 | 173 | 174 | -- Channels with legends ------------------------------------------------------- 175 | 176 | 177 | channelCompareWith : 178 | (domain -> domain -> Order) 179 | -> (domain -> String) 180 | -> Scale domain range 181 | -> Field data domain 182 | -> ChannelWithLegend data range 183 | channelCompareWith compareWith formatDomain scale field = 184 | { title = Field.fieldName field 185 | , isAggregate = Field.isAggregate field 186 | , isVector = Field.isVector field 187 | , fieldEqual = Field.equalAt field 188 | , fieldCompare = Field.compareAt compareWith field 189 | , legend = Legend.legend formatDomain field scale 190 | , displayLegend = True 191 | , extract = Field.extract field >> Maybe.andThen (Scale.scale scale) 192 | , extractVector = Field.extractVector field >> List.map (Maybe.andThen (Scale.scale scale)) 193 | , summarize = Field.summarize field >> Maybe.andThen (Scale.scale scale) 194 | } 195 | 196 | 197 | channel : 198 | (comparableDomain -> String) 199 | -> Scale comparableDomain range 200 | -> Field data comparableDomain 201 | -> ChannelWithLegend data range 202 | channel formatDomain scale field = 203 | channelCompareWith compare formatDomain scale field 204 | 205 | 206 | 207 | -- Angle ------------------------------------------------------------------------ 208 | 209 | 210 | type alias AngleChannel data = 211 | ChannelWithLegend data Float 212 | 213 | 214 | angle : 215 | (comparableDomain -> String) 216 | -> Scale comparableDomain Float 217 | -> Field data comparableDomain 218 | -> AngleChannel data 219 | angle formatDomain scale field = 220 | channel formatDomain scale field 221 | 222 | 223 | angleCompareWith : 224 | (domain -> domain -> Order) 225 | -> (domain -> String) 226 | -> Scale domain Float 227 | -> Field data domain 228 | -> AngleChannel data 229 | angleCompareWith compareWith formatDomain scale field = 230 | channelCompareWith compareWith formatDomain scale field 231 | 232 | 233 | 234 | -- Color ----------------------------------------------------------------------- 235 | 236 | 237 | type alias ColorChannel data = 238 | ChannelWithLegend data Color 239 | 240 | 241 | color : 242 | (comparableDomain -> String) 243 | -> Scale comparableDomain Color 244 | -> Field data comparableDomain 245 | -> ColorChannel data 246 | color formatDomain scale field = 247 | channel formatDomain scale field 248 | 249 | 250 | colorCompareWith : 251 | (domain -> domain -> Order) 252 | -> (domain -> String) 253 | -> Scale domain Color 254 | -> Field data domain 255 | -> ColorChannel data 256 | colorCompareWith compareWith formatDomain scale field = 257 | channelCompareWith compareWith formatDomain scale field 258 | 259 | 260 | 261 | -- Float ------------------------------------------------------------------------ 262 | 263 | 264 | type alias FloatChannel data = 265 | ChannelWithLegend data Float 266 | 267 | 268 | float : 269 | (comparableDomain -> String) 270 | -> Scale comparableDomain Float 271 | -> Field data comparableDomain 272 | -> FloatChannel data 273 | float formatDomain scale field = 274 | channel formatDomain scale field 275 | 276 | 277 | floatCompareWith : 278 | (domain -> domain -> Order) 279 | -> (domain -> String) 280 | -> Scale domain Float 281 | -> Field data domain 282 | -> FloatChannel data 283 | floatCompareWith compareWith formatDomain scale field = 284 | channelCompareWith compareWith formatDomain scale field 285 | 286 | 287 | 288 | -- Float ------------------------------------------------------------------------ 289 | 290 | 291 | type alias IntChannel data = 292 | ChannelWithLegend data Int 293 | 294 | 295 | int : 296 | (comparableDomain -> String) 297 | -> Scale comparableDomain Int 298 | -> Field data comparableDomain 299 | -> IntChannel data 300 | int formatDomain scale field = 301 | channel formatDomain scale field 302 | 303 | 304 | intCompareWith : 305 | (domain -> domain -> Order) 306 | -> (domain -> String) 307 | -> Scale domain Int 308 | -> Field data domain 309 | -> IntChannel data 310 | intCompareWith compareWith formatDomain scale field = 311 | channelCompareWith compareWith formatDomain scale field 312 | 313 | 314 | 315 | -- Shape ------------------------------------------------------------------------ 316 | 317 | 318 | type alias ShapeChannel data = 319 | ChannelWithLegend data Shape 320 | 321 | 322 | shape : 323 | (comparableDomain -> String) 324 | -> Scale comparableDomain Shape 325 | -> Field data comparableDomain 326 | -> ShapeChannel data 327 | shape formatDomain scale field = 328 | channel formatDomain scale field 329 | 330 | 331 | shapeCompareWith : 332 | (domain -> domain -> Order) 333 | -> (domain -> String) 334 | -> Scale domain Shape 335 | -> Field data domain 336 | -> ShapeChannel data 337 | shapeCompareWith compareWith formatDomain scale field = 338 | channelCompareWith compareWith formatDomain scale field 339 | 340 | 341 | 342 | -- StrokeDash ------------------------------------------------------------------ 343 | 344 | 345 | type alias StrokeDashChannel data = 346 | ChannelWithLegend data StrokeDash 347 | 348 | 349 | strokeDash : 350 | (comparableDomain -> String) 351 | -> Scale comparableDomain StrokeDash 352 | -> Field data comparableDomain 353 | -> StrokeDashChannel data 354 | strokeDash formatDomain scale field = 355 | channel formatDomain scale field 356 | 357 | 358 | strokeDashCompareWith : 359 | (domain -> domain -> Order) 360 | -> (domain -> String) 361 | -> Scale domain StrokeDash 362 | -> Field data domain 363 | -> StrokeDashChannel data 364 | strokeDashCompareWith compareWith formatDomain scale field = 365 | channelCompareWith compareWith formatDomain scale field 366 | 367 | 368 | 369 | -- Text ------------------------------------------------------------------------ 370 | 371 | 372 | type alias TextChannel data = 373 | ChannelWithLegend data String 374 | 375 | 376 | text : 377 | (comparableDomain -> String) 378 | -> Scale comparableDomain String 379 | -> Field data comparableDomain 380 | -> TextChannel data 381 | text formatDomain scale field = 382 | channel formatDomain scale field 383 | 384 | 385 | textCompareWith : 386 | (domain -> domain -> Order) 387 | -> (domain -> String) 388 | -> Scale domain String 389 | -> Field data domain 390 | -> TextChannel data 391 | textCompareWith compareWith formatDomain scale field = 392 | channelCompareWith compareWith formatDomain scale field 393 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Area.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Area 2 | exposing 3 | ( Area 4 | , hArea 5 | , vArea 6 | , stream 7 | , scenegraph 8 | , legends 9 | ) 10 | 11 | import Facet.Internal.Channel as Channel exposing (ShapeChannel, FloatChannel, PositionalChannel, TextChannel) 12 | import Facet.Internal.Encoding.Fill as Fill exposing (Fill) 13 | import Facet.Internal.Encoding.Position as Position exposing (Position(..)) 14 | import Facet.Internal.Encoding.Stroke as Stroke exposing (Stroke) 15 | import Facet.Internal.Field as Field 16 | import Facet.Internal.Legend exposing (Legend, LegendSpec) 17 | import Facet.List.Extra as List 18 | import Facet.Maybe.Extra as Maybe 19 | import Facet.Scale as Scale exposing (Scale) 20 | import Facet.Scenegraph as Scenegraph exposing (Scenegraph) 21 | import Facet.Scenegraph.Cursor as Cursor exposing (Cursor) 22 | import Facet.Scenegraph.Fill as Scenegraph 23 | import Facet.Scenegraph.Stroke as Scenegraph 24 | import Facet.Scenegraph.Interpolate exposing (Interpolate(..)) 25 | import Facet.Scenegraph.Mark as Mark exposing (Behaviour(..), Orientation(..)) 26 | import Facet.Scenegraph.Position as ScenegraphPosition 27 | import Facet.Theme as Theme 28 | 29 | 30 | {-| Filled areas with horizontal or vertical orientation 31 | 32 | -} 33 | type alias Area data xdomain ydomain = 34 | { x : PositionalChannel data xdomain 35 | , y : Position data ydomain 36 | , interpolate : Interpolate 37 | , behavior : Mark.Behaviour 38 | , orientation : Orientation 39 | , fill : Maybe (Fill data) 40 | , stroke : Maybe (Stroke data) 41 | , cursor : Maybe Cursor 42 | , href : Maybe String 43 | , tooltip : Maybe (TextChannel data) 44 | } 45 | 46 | 47 | 48 | -- Helpers --------------------------------------------------------------------- 49 | 50 | 51 | {-| Construct an `Area` encoding with `x` and `y2` positions with `y` fixed at 0. 52 | This can be used to build [Area charts](https://en.wikipedia.org/wiki/Area_chart) 53 | -} 54 | hArea : 55 | PositionalChannel data xdomain 56 | -> PositionalChannel data number 57 | -> Interpolate 58 | -> Behaviour 59 | -> Area data xdomain number 60 | hArea x y2 interpolate behaviour = 61 | let 62 | y = 63 | PrimarySecondary constantY y2 64 | 65 | constantY = 66 | Field.constant 0 67 | |> Channel.positional 68 | in 69 | Area 70 | x 71 | y 72 | interpolate 73 | behaviour 74 | Horizontal 75 | Nothing 76 | Nothing 77 | Nothing 78 | Nothing 79 | Nothing 80 | 81 | 82 | vArea : 83 | PositionalChannel data xdomain 84 | -> PositionalChannel data number 85 | -> Interpolate 86 | -> Behaviour 87 | -> Area data xdomain number 88 | vArea y x2 interpolate behaviour = 89 | let 90 | x = 91 | PrimarySecondary constantX x2 92 | 93 | constantX = 94 | Field.constant 0 95 | |> Channel.positional 96 | in 97 | Area y 98 | x 99 | interpolate 100 | behaviour 101 | Vertical 102 | Nothing 103 | Nothing 104 | Nothing 105 | Nothing 106 | Nothing 107 | 108 | 109 | {-| Construct an `Area` encoding with `x`, `y` and `y2`. 110 | This can be used to build [Streamgraphs](https://en.wikipedia.org/wiki/Streamgraph). 111 | -} 112 | stream : 113 | PositionalChannel data xdomain 114 | -> PositionalChannel data ydomain 115 | -> PositionalChannel data ydomain 116 | -> Interpolate 117 | -> Behaviour 118 | -> Area data xdomain ydomain 119 | stream x y y2 interpolate behaviour = 120 | let 121 | ypos = 122 | PrimarySecondary y y2 123 | in 124 | Area 125 | x 126 | ypos 127 | interpolate 128 | behaviour 129 | Horizontal 130 | Nothing 131 | Nothing 132 | Nothing 133 | Nothing 134 | Nothing 135 | 136 | 137 | 138 | -- LEGENDS --------------------------------------------------------------------- 139 | 140 | 141 | legends : Int -> Area data xdomain ydomain -> LegendSpec 142 | legends ticks encoding = 143 | { fillColor = encoding.fill |> Maybe.andThen .fill |> Maybe.andThen (Channel.legend ticks) 144 | , fillOpacity = encoding.fill |> Maybe.andThen .fillOpacity |> Maybe.andThen (Channel.legend ticks) 145 | , strokeColor = encoding.stroke |> Maybe.andThen .stroke |> Maybe.andThen (Channel.legend ticks) 146 | , strokeOpacity = encoding.stroke |> Maybe.andThen .strokeOpacity |> Maybe.andThen (Channel.legend ticks) 147 | , strokeWidth = encoding.stroke |> Maybe.andThen .strokeWidth |> Maybe.andThen (Channel.legend ticks) 148 | , strokeDash = encoding.stroke |> Maybe.andThen .strokeDash |> Maybe.andThen (Channel.legend ticks) 149 | , angle = Nothing 150 | , shape = Nothing 151 | , cornerRadius = Nothing 152 | , size = Nothing 153 | , width = Nothing 154 | } 155 | 156 | 157 | 158 | -- SCENEGRAPH ------------------------------------------------------------------ 159 | 160 | 161 | {-| An intermediate representation of an encoding which allows for all 162 | fields to possibly be `Nothing` 163 | -} 164 | type alias AreaInternal = 165 | { x : Maybe (List (Maybe Float)) 166 | , y : Maybe (List (Maybe ScenegraphPosition.Position)) 167 | , fill : Maybe Scenegraph.Fill 168 | , stroke : Maybe Scenegraph.Stroke 169 | , cursor : Maybe Cursor 170 | , href : Maybe String 171 | , tooltip : Maybe String 172 | } 173 | 174 | 175 | {-| Combine `AreaInternal`s with a preference for `mark2` (in practice, 176 | for each field in the two `RectInternal`s, only one _can_ have a `Just` 177 | value so the order of arguments does not matter.) 178 | -} 179 | combineIntermediate : AreaInternal -> AreaInternal -> AreaInternal 180 | combineIntermediate mark1 mark2 = 181 | { x = mark2.x |> Maybe.orElse mark1.x 182 | , y = Maybe.orElse mark1.y mark2.y 183 | , fill = Maybe.orElse mark1.fill mark2.fill 184 | , stroke = Maybe.orElse mark1.stroke mark2.stroke 185 | , cursor = Maybe.orElse mark1.cursor mark2.cursor 186 | , href = Maybe.orElse mark1.href mark2.href 187 | , tooltip = Maybe.orElse mark1.tooltip mark2.tooltip 188 | } 189 | 190 | 191 | {-| Combine an `AreaInternal` with required fields from the encoding 192 | and a theme to populate any un-encoded fields. 193 | -} 194 | combineWithThemeDefaults : Theme.Area -> Interpolate -> Behaviour -> Orientation -> AreaInternal -> Maybe Mark.Area 195 | combineWithThemeDefaults theme interpolate behaviour orientation markInternal = 196 | Maybe.map2 197 | (\x y -> 198 | { x = x 199 | , y = y 200 | , interpolate = interpolate 201 | , behaviour = behaviour 202 | , orientation = orientation 203 | , fill = Maybe.withDefault theme.fill markInternal.fill 204 | , stroke = Maybe.withDefault theme.stroke markInternal.stroke 205 | , cursor = Maybe.withDefault theme.cursor markInternal.cursor 206 | , href = markInternal.href 207 | , tooltip = markInternal.tooltip 208 | } 209 | ) 210 | markInternal.x 211 | markInternal.y 212 | 213 | 214 | {-| Test for an aggregate field in one of the channels 215 | -} 216 | containsAggregate : Area data xdomain ydomain -> Bool 217 | containsAggregate encoding = 218 | Channel.isAggregate encoding.x 219 | || Position.containsAggregate encoding.y 220 | || Maybe.maybe False Fill.containsAggregate encoding.fill 221 | || Maybe.maybe False Stroke.containsAggregate encoding.stroke 222 | || Maybe.maybe False Channel.isAggregate encoding.tooltip 223 | 224 | 225 | containsVector : Area data xdomain ydomain -> Bool 226 | containsVector encoding = 227 | Channel.isVector encoding.x 228 | || Position.containsVector encoding.y 229 | || Maybe.maybe False Fill.containsVector encoding.fill 230 | || Maybe.maybe False Stroke.containsVector encoding.stroke 231 | || Maybe.maybe False Channel.isVector encoding.tooltip 232 | 233 | 234 | 235 | {- Generate a scenegraph for the encoding -} 236 | 237 | 238 | scenegraph : 239 | Theme.Area 240 | -> Scale xdomain Float 241 | -> Scale ydomain Float 242 | -> List data 243 | -> Area data xdomain ydomain 244 | -> Scenegraph 245 | scenegraph theme xScale yScale data encoding = 246 | if containsAggregate encoding then 247 | data 248 | |> List.groupBy (compareAt encoding) 249 | (extract xScale yScale encoding) 250 | |> List.filterMap 251 | (\( mark, data ) -> 252 | let 253 | vector = 254 | extractVector xScale yScale encoding data 255 | 256 | agg = 257 | summarize xScale yScale encoding data 258 | in 259 | mark 260 | |> combineIntermediate vector 261 | |> combineIntermediate agg 262 | |> combineWithThemeDefaults theme encoding.interpolate encoding.behavior encoding.orientation 263 | ) 264 | |> Scenegraph.Area 265 | else if containsVector encoding then 266 | data 267 | |> List.groupBy (compareAt encoding) 268 | (extract xScale yScale encoding) 269 | |> List.filterMap 270 | (\( mark, data ) -> 271 | let 272 | vector = 273 | extractVector xScale yScale encoding data 274 | in 275 | mark 276 | |> combineIntermediate vector 277 | |> combineWithThemeDefaults theme encoding.interpolate encoding.behavior encoding.orientation 278 | ) 279 | |> Scenegraph.Area 280 | else 281 | List.filterMap 282 | (extract xScale yScale encoding 283 | >> combineWithThemeDefaults theme encoding.interpolate encoding.behavior encoding.orientation 284 | ) 285 | data 286 | |> Scenegraph.Area 287 | 288 | 289 | 290 | -- APPLY AGGREGATE, VECTOR AND SCALAR FIELDS ----------------------------------- 291 | 292 | 293 | {-| Apply each scalar channel of an encoding to a data point. 294 | Where a channel is not a scalar, the corresponding field in the returned 295 | `RectInternal` is `Nothing`. 296 | -} 297 | extract : 298 | Scale xdomain Float 299 | -> Scale ydomain Float 300 | -> Area data xdomain ydomain 301 | -> data 302 | -> AreaInternal 303 | extract xScale yScale encoding datum = 304 | let 305 | extractMaybe getter = 306 | getter encoding 307 | |> Maybe.andThen (\channel -> Channel.extract channel datum) 308 | 309 | positionX = 310 | Field.extract encoding.x.field datum 311 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 312 | 313 | positionY = 314 | Position.extract yScale encoding.y datum 315 | |> Maybe.map (Just >> List.singleton) 316 | 317 | fill = 318 | Maybe.map (\fill -> Fill.extract fill datum) encoding.fill 319 | 320 | stroke = 321 | Maybe.map (\stroke -> Stroke.extract stroke datum) encoding.stroke 322 | in 323 | { x = positionX 324 | , y = positionY 325 | , fill = fill 326 | , stroke = stroke 327 | , cursor = encoding.cursor 328 | , href = encoding.href 329 | , tooltip = extractMaybe .tooltip 330 | } 331 | 332 | 333 | {-| Apply each vector channel of an encoding to a data point. 334 | Where a channel is not a vector, the corresponding field in the returned 335 | `RectInternal` is `Nothing`. 336 | -} 337 | extractVector : 338 | Scale xdomain Float 339 | -> Scale ydomain Float 340 | -> Area data xdomain ydomain 341 | -> List data 342 | -> AreaInternal 343 | extractVector xScale yScale encoding data = 344 | let 345 | xs = 346 | Field.extractVector encoding.x.field data 347 | |> List.map (Maybe.andThen (Scale.scale xScale)) 348 | 349 | ys = 350 | Position.extractVector yScale encoding.y data 351 | 352 | xys = 353 | List.map2 354 | (\x y -> 355 | case Maybe.map2 (,) x y of 356 | Just ( x, y ) -> 357 | ( Just x, Just y ) 358 | 359 | _ -> 360 | ( Nothing, Nothing ) 361 | ) 362 | xs 363 | ys 364 | 365 | ( xout, yout ) = 366 | case xys of 367 | [] -> 368 | ( Nothing, Nothing ) 369 | 370 | _ -> 371 | let 372 | ( x, y ) = 373 | List.unzip xys 374 | in 375 | ( Just x, Just y ) 376 | 377 | fill = 378 | Maybe.andThen 379 | (\fill -> Fill.extractVector fill data |> List.head) 380 | encoding.fill 381 | 382 | stroke = 383 | Maybe.andThen 384 | (\stroke -> Stroke.extractVector stroke data |> List.head) 385 | encoding.stroke 386 | 387 | tooltip = 388 | encoding.tooltip 389 | |> Maybe.andThen 390 | (\channel -> 391 | Channel.extractVector channel data 392 | |> List.head 393 | >> Maybe.join 394 | ) 395 | in 396 | { x = xout 397 | , y = yout 398 | , fill = fill 399 | , stroke = stroke 400 | , cursor = encoding.cursor 401 | , href = encoding.href 402 | , tooltip = tooltip 403 | } 404 | 405 | 406 | {-| Apply each aggregate channel of an encoding to a data point. 407 | Where a channel is not an aggregate, the corresponding field in the returned 408 | `RectInternal` is `Nothing`. 409 | -} 410 | summarize : 411 | Scale xdomain Float 412 | -> Scale ydomain Float 413 | -> Area data xdomain ydomain 414 | -> List data 415 | -> AreaInternal 416 | summarize xScale yScale encoding data = 417 | let 418 | summarizeMaybe getter = 419 | getter encoding 420 | |> Maybe.andThen (\channel -> Channel.summarize channel data) 421 | 422 | positionX = 423 | Field.summarize encoding.x.field data 424 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 425 | 426 | positionY = 427 | Position.summarize yScale encoding.y data 428 | |> Maybe.map (Just >> List.singleton) 429 | in 430 | { x = positionX 431 | , y = positionY 432 | , fill = Maybe.map (\fill -> Fill.summarize fill data) encoding.fill 433 | , stroke = Maybe.map (\stroke -> Stroke.summarize stroke data) encoding.stroke 434 | , cursor = encoding.cursor 435 | , href = encoding.href 436 | , tooltip = summarizeMaybe .tooltip 437 | } 438 | 439 | 440 | {-| Structural comparison of encoding domain evaluated at two data poinst 441 | -} 442 | compareAt : Area data xdomain ydomain -> data -> data -> Order 443 | compareAt encoding d1 d2 = 444 | let 445 | comp getter = 446 | Channel.compareMaybeAt (getter encoding) d1 d2 447 | in 448 | case Channel.compareAt encoding.x d1 d2 of 449 | EQ -> 450 | case Position.compareAt encoding.y d1 d2 of 451 | EQ -> 452 | case Maybe.maybe EQ (\fill -> Fill.compareAt fill d1 d2) encoding.fill of 453 | EQ -> 454 | case Maybe.maybe EQ (\stroke -> Stroke.compareAt stroke d1 d2) encoding.stroke of 455 | EQ -> 456 | comp .tooltip 457 | 458 | otherwise -> 459 | otherwise 460 | 461 | otherwise -> 462 | otherwise 463 | 464 | otherwise -> 465 | otherwise 466 | 467 | otherwise -> 468 | otherwise 469 | 470 | 471 | equalAt : Area data xdomain ydomain -> data -> data -> Bool 472 | equalAt encoding d1 d2 = 473 | let 474 | eq getter = 475 | Channel.equalMaybeAt (getter encoding) d1 d2 476 | in 477 | Channel.equalAt encoding.x d1 d2 478 | && Position.equalAt encoding.y d1 d2 479 | && Maybe.maybe True (\fill -> Fill.equalAt fill d1 d2) encoding.fill 480 | && Maybe.maybe True (\stroke -> Stroke.equalAt stroke d1 d2) encoding.stroke 481 | && eq .tooltip 482 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Fill.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Fill 2 | exposing 3 | ( Fill 4 | , containsAggregate 5 | , containsVector 6 | , extractOrElse 7 | , extractVectorOrElse 8 | , summarizeOrElse 9 | , summarize 10 | , extract 11 | , extractVector 12 | , compareAt 13 | , equalAt 14 | , empty 15 | ) 16 | 17 | {-| 18 | @docs Fill, containsAggregate, extractOrElse, summarizeOrElse 19 | -} 20 | 21 | import Color exposing (Color) 22 | import Facet.Internal.Channel as Channel exposing (ColorChannel, FloatChannel) 23 | import Facet.Maybe.Extra as Maybe 24 | import Facet.Scenegraph.Fill as Mark 25 | 26 | 27 | {-| -} 28 | type alias Fill data = 29 | { fill : Maybe (ColorChannel data) 30 | , fillOpacity : Maybe (FloatChannel data) 31 | } 32 | 33 | 34 | empty : Fill data 35 | empty = 36 | { fill = Nothing 37 | , fillOpacity = Nothing 38 | } 39 | 40 | 41 | compareAt : 42 | Fill data 43 | -> data 44 | -> data 45 | -> Order 46 | compareAt { fill, fillOpacity } d1 d2 = 47 | case Channel.compareMaybeAt fill d1 d2 of 48 | EQ -> 49 | Channel.compareMaybeAt fillOpacity d1 d2 50 | 51 | otherwise -> 52 | otherwise 53 | 54 | 55 | equalAt : 56 | Fill data 57 | -> data 58 | -> data 59 | -> Bool 60 | equalAt { fill, fillOpacity } d1 d2 = 61 | Channel.equalMaybeAt fill d1 d2 && Channel.equalMaybeAt fillOpacity d1 d2 62 | 63 | 64 | {-| -} 65 | containsAggregate : Fill data -> Bool 66 | containsAggregate { fill, fillOpacity } = 67 | Maybe.maybe False Channel.isAggregate fill 68 | || Maybe.maybe False Channel.isAggregate fillOpacity 69 | 70 | 71 | containsVector : Fill data -> Bool 72 | containsVector { fill, fillOpacity } = 73 | Maybe.maybe False Channel.isVector fill 74 | || Maybe.maybe False Channel.isVector fillOpacity 75 | 76 | 77 | {-| -} 78 | extractOrElse : Mark.Fill -> Fill data -> data -> Mark.Fill 79 | extractOrElse mark fill datum = 80 | let 81 | f a b = 82 | Maybe.orElse a <| 83 | Maybe.andThen (\ch -> Channel.extract ch datum) 84 | b 85 | in 86 | { fill = f mark.fill fill.fill 87 | , fillOpacity = f mark.fillOpacity fill.fillOpacity 88 | } 89 | 90 | 91 | extract : Fill data -> data -> Mark.Fill 92 | extract fill datum = 93 | { fill = Maybe.andThen (\ch -> Channel.extract ch datum) fill.fill 94 | , fillOpacity = Maybe.andThen (\ch -> Channel.extract ch datum) fill.fillOpacity 95 | } 96 | 97 | 98 | {-| -} 99 | extractVectorOrElse : Mark.Fill -> Fill data -> List data -> Mark.Fill 100 | extractVectorOrElse mark fill data = 101 | let 102 | f a b = 103 | b 104 | |> Maybe.andThen (\ch -> Channel.extractVector ch data |> List.head |> Maybe.join) 105 | |> Maybe.orElse a 106 | in 107 | { fill = f mark.fill fill.fill 108 | , fillOpacity = f mark.fillOpacity fill.fillOpacity 109 | } 110 | 111 | 112 | extractVector : Fill data -> List data -> List Mark.Fill 113 | extractVector fill data = 114 | let 115 | fs = 116 | fill.fill 117 | |> Maybe.map (\ch -> Channel.extractVector ch data) 118 | 119 | os = 120 | fill.fillOpacity 121 | |> Maybe.map (\ch -> Channel.extractVector ch data) 122 | in 123 | case ( fs, os ) of 124 | ( Just fills, Just opacities ) -> 125 | extractVectorHelper [] fills opacities 126 | 127 | ( Just fills, _ ) -> 128 | fills |> List.map (\fill -> { fill = fill, fillOpacity = Nothing }) 129 | 130 | ( _, Just opacities ) -> 131 | opacities |> List.map (\fillOpacity -> { fill = Nothing, fillOpacity = fillOpacity }) 132 | 133 | _ -> 134 | [] 135 | 136 | 137 | extractVectorHelper : 138 | List Mark.Fill 139 | -> List (Maybe Color) 140 | -> List (Maybe Float) 141 | -> List Mark.Fill 142 | extractVectorHelper accu fills opacities = 143 | case ( fills, opacities ) of 144 | ( [], [] ) -> 145 | List.reverse accu 146 | 147 | ( nextFill :: restFills, nextOpacity :: restOpacities ) -> 148 | extractVectorHelper ((Mark.Fill nextFill nextOpacity) :: accu) 149 | restFills 150 | restOpacities 151 | 152 | ( nextFill :: restFills, [] ) -> 153 | extractVectorHelper ((Mark.Fill nextFill Nothing) :: accu) 154 | restFills 155 | [] 156 | 157 | ( [], nextOpacity :: restOpacities ) -> 158 | extractVectorHelper ((Mark.Fill Nothing nextOpacity) :: accu) 159 | [] 160 | restOpacities 161 | 162 | 163 | summarize : Fill data -> List data -> Mark.Fill 164 | summarize fill datum = 165 | { fill = Maybe.andThen (\ch -> Channel.summarize ch datum) fill.fill 166 | , fillOpacity = Maybe.andThen (\ch -> Channel.summarize ch datum) fill.fillOpacity 167 | } 168 | 169 | 170 | {-| -} 171 | summarizeOrElse : Mark.Fill -> Fill data -> List data -> Mark.Fill 172 | summarizeOrElse mark fill data = 173 | let 174 | f a b = 175 | Maybe.orElse a <| 176 | Maybe.andThen (\ch -> Channel.summarize ch data) 177 | b 178 | in 179 | { fill = f mark.fill fill.fill 180 | , fillOpacity = f mark.fillOpacity fill.fillOpacity 181 | } 182 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Line.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Line exposing (Line, line, scenegraph, legends) 2 | 3 | import Facet.Internal.Encoding.Stroke as Stroke exposing (Stroke) 4 | import Facet.Internal.Channel as Channel exposing (TextChannel, PositionalChannel) 5 | import Facet.Internal.Field as Field 6 | import Facet.Internal.Legend exposing (LegendSpec) 7 | import Facet.List.Extra as List 8 | import Facet.Maybe.Extra as Maybe 9 | import Facet.Scale as Scale exposing (Scale) 10 | import Facet.Scenegraph as Scenegraph exposing (Scenegraph) 11 | import Facet.Scenegraph.Cursor as Cursor exposing (Cursor) 12 | import Facet.Scenegraph.Stroke as Scenegraph 13 | import Facet.Scenegraph.Interpolate exposing (Interpolate(..)) 14 | import Facet.Scenegraph.Mark as Mark exposing (Behaviour(..), Orientation(..)) 15 | import Facet.Scenegraph.Mark as Mark 16 | import Facet.Theme as Theme 17 | 18 | 19 | type alias Line data xdomain ydomain = 20 | { x : PositionalChannel data xdomain 21 | , y : PositionalChannel data ydomain 22 | , interpolate : Interpolate 23 | , behaviour : Mark.Behaviour 24 | , stroke : Maybe (Stroke data) 25 | , cursor : Maybe Cursor 26 | , href : Maybe String 27 | , tooltip : Maybe (TextChannel data) 28 | } 29 | 30 | 31 | 32 | -- Helpers --------------------------------------------------------------------- 33 | 34 | 35 | line : 36 | PositionalChannel data xdomain 37 | -> PositionalChannel data ydomain 38 | -> Interpolate 39 | -> Behaviour 40 | -> Line data xdomain ydomain 41 | line x y interpolate behaviour = 42 | Line 43 | x 44 | y 45 | interpolate 46 | behaviour 47 | Nothing 48 | Nothing 49 | Nothing 50 | Nothing 51 | 52 | 53 | 54 | -- LEGENDS --------------------------------------------------------------------- 55 | 56 | 57 | legends : Int -> Line data xdomain ydomain -> LegendSpec 58 | legends ticks encoding = 59 | { fillColor = Nothing 60 | , fillOpacity = Nothing 61 | , strokeColor = encoding.stroke |> Maybe.andThen .stroke |> Maybe.andThen (Channel.legend ticks) 62 | , strokeOpacity = encoding.stroke |> Maybe.andThen .strokeOpacity |> Maybe.andThen (Channel.legend ticks) 63 | , strokeWidth = encoding.stroke |> Maybe.andThen .strokeWidth |> Maybe.andThen (Channel.legend ticks) 64 | , strokeDash = encoding.stroke |> Maybe.andThen .strokeDash |> Maybe.andThen (Channel.legend ticks) 65 | , angle = Nothing 66 | , shape = Nothing 67 | , cornerRadius = Nothing 68 | , size = Nothing 69 | , width = Nothing 70 | } 71 | 72 | 73 | 74 | -- SCENEGRAPH ------------------------------------------------------------------ 75 | 76 | 77 | {-| An intermediate representation of an encoding which allows for all 78 | fields to possibly be `Nothing` 79 | -} 80 | type alias LineInternal = 81 | { x : Maybe (List (Maybe Float)) 82 | , y : Maybe (List (Maybe Float)) 83 | , stroke : Maybe Scenegraph.Stroke 84 | , cursor : Maybe Cursor 85 | , href : Maybe String 86 | , tooltip : Maybe String 87 | } 88 | 89 | 90 | {-| Combine `LineInternal`s with a preference for `mark2` (in practice, 91 | for each field in the two `RectInternal`s, only one _can_ have a `Just` 92 | value so the order of arguments does not matter.) 93 | -} 94 | combineIntermediate : LineInternal -> LineInternal -> LineInternal 95 | combineIntermediate mark1 mark2 = 96 | { x = Maybe.orElse mark1.x mark2.x 97 | , y = Maybe.orElse mark1.y mark2.y 98 | , stroke = Maybe.orElse mark1.stroke mark2.stroke 99 | , cursor = Maybe.orElse mark1.cursor mark2.cursor 100 | , href = Maybe.orElse mark1.href mark2.href 101 | , tooltip = Maybe.orElse mark1.tooltip mark2.tooltip 102 | } 103 | 104 | 105 | {-| Combine an `LineInternal` with required fields from the encoding 106 | and a theme to populate any un-encoded fields. 107 | -} 108 | combineWithThemeDefaults : Theme.Line -> Interpolate -> Behaviour -> LineInternal -> Maybe Mark.Line 109 | combineWithThemeDefaults theme interpolate behaviour markInternal = 110 | Maybe.map2 111 | (\x y -> 112 | { x = x 113 | , y = y 114 | , interpolate = interpolate 115 | , behaviour = behaviour 116 | , stroke = Maybe.withDefault theme.stroke markInternal.stroke 117 | , cursor = Maybe.withDefault theme.cursor markInternal.cursor 118 | , href = markInternal.href 119 | , tooltip = markInternal.tooltip 120 | } 121 | ) 122 | markInternal.x 123 | markInternal.y 124 | 125 | 126 | {-| Test for an aggregate field in one of the channels 127 | -} 128 | containsAggregate : Line data xdomain ydomain -> Bool 129 | containsAggregate encoding = 130 | Channel.isAggregate encoding.x 131 | || Channel.isAggregate encoding.y 132 | || Maybe.maybe False Stroke.containsAggregate encoding.stroke 133 | || Maybe.maybe False Channel.isAggregate encoding.tooltip 134 | 135 | 136 | containsVector : Line data xdomain ydomain -> Bool 137 | containsVector encoding = 138 | Channel.isVector encoding.x 139 | || Channel.isVector encoding.y 140 | || Maybe.maybe False Stroke.containsVector encoding.stroke 141 | || Maybe.maybe False Channel.isVector encoding.tooltip 142 | 143 | 144 | 145 | {- Generate a scenegraph for the encoding -} 146 | 147 | 148 | scenegraph : 149 | Theme.Line 150 | -> Scale xdomain Float 151 | -> Scale ydomain Float 152 | -> List data 153 | -> Line data xdomain ydomain 154 | -> Scenegraph 155 | scenegraph theme xScale yScale data encoding = 156 | if containsVector encoding then 157 | data 158 | |> List.groupBy (compareAt encoding) 159 | (extract xScale yScale encoding) 160 | |> List.filterMap 161 | (\( mark, data ) -> 162 | let 163 | vector = 164 | extractVector xScale yScale encoding data 165 | 166 | agg = 167 | summarize xScale yScale encoding data 168 | in 169 | mark 170 | |> combineIntermediate agg 171 | |> combineIntermediate vector 172 | |> combineWithThemeDefaults theme encoding.interpolate encoding.behaviour 173 | ) 174 | |> Scenegraph.Line 175 | else if containsAggregate encoding then 176 | data 177 | |> List.groupBy (compareAt encoding) 178 | (extract xScale yScale encoding) 179 | |> List.filterMap 180 | (\( mark, data ) -> 181 | let 182 | agg = 183 | summarize xScale yScale encoding data 184 | in 185 | mark 186 | |> combineIntermediate agg 187 | |> combineWithThemeDefaults theme encoding.interpolate encoding.behaviour 188 | ) 189 | |> Scenegraph.Line 190 | else 191 | List.filterMap 192 | (extract xScale yScale encoding 193 | >> combineWithThemeDefaults theme encoding.interpolate encoding.behaviour 194 | ) 195 | data 196 | |> Scenegraph.Line 197 | 198 | 199 | 200 | -- APPLY AGGREGATE, VECTOR AND SCALAR FIELDS ----------------------------------- 201 | 202 | 203 | {-| Apply each scalar channel of an encoding to a data point. 204 | Where a channel is not a scalar, the corresponding field in the returned 205 | `LineInternal` is `Nothing`. 206 | -} 207 | extract : 208 | Scale xdomain Float 209 | -> Scale ydomain Float 210 | -> Line data xdomain ydomain 211 | -> data 212 | -> LineInternal 213 | extract xScale yScale encoding datum = 214 | let 215 | extractMaybe getter = 216 | getter encoding 217 | |> Maybe.andThen (\channel -> Channel.extract channel datum) 218 | 219 | positionX = 220 | Field.extract encoding.x.field datum 221 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 222 | 223 | positionY = 224 | Field.extract encoding.y.field datum 225 | |> Maybe.andThen (Scale.scale yScale >> Maybe.map (Just >> List.singleton)) 226 | 227 | stroke = 228 | Maybe.map (\stroke -> Stroke.extract stroke datum) encoding.stroke 229 | in 230 | { x = positionX 231 | , y = positionY 232 | , stroke = stroke 233 | , cursor = encoding.cursor 234 | , href = encoding.href 235 | , tooltip = extractMaybe .tooltip 236 | } 237 | 238 | 239 | {-| Apply each vector channel of an encoding to a data point. 240 | Where a channel is not a vector, the corresponding field in the returned 241 | `LineInternal` is `Nothing`. 242 | -} 243 | extractVector : 244 | Scale xdomain Float 245 | -> Scale ydomain Float 246 | -> Line data xdomain ydomain 247 | -> List data 248 | -> LineInternal 249 | extractVector xScale yScale encoding data = 250 | let 251 | xs = 252 | Field.extractVector encoding.x.field data 253 | |> List.map (Maybe.andThen (Scale.scale xScale)) 254 | 255 | ys = 256 | Field.extractVector encoding.y.field data 257 | |> List.map (Maybe.andThen (Scale.scale yScale)) 258 | 259 | xys = 260 | List.map2 261 | (\x y -> 262 | case Maybe.map2 (,) x y of 263 | Just ( x, y ) -> 264 | ( Just x, Just y ) 265 | 266 | _ -> 267 | ( Nothing, Nothing ) 268 | ) 269 | xs 270 | ys 271 | 272 | ( xout, yout ) = 273 | case xys of 274 | [] -> 275 | ( Nothing, Nothing ) 276 | 277 | _ -> 278 | let 279 | ( x, y ) = 280 | List.unzip xys 281 | in 282 | ( Just x, Just y ) 283 | 284 | stroke = 285 | Maybe.andThen 286 | (\stroke -> Stroke.extractVector stroke data |> List.head) 287 | encoding.stroke 288 | 289 | tooltip = 290 | encoding.tooltip 291 | |> Maybe.andThen 292 | (\channel -> 293 | Channel.extractVector channel data 294 | |> List.head 295 | >> Maybe.join 296 | ) 297 | in 298 | { x = xout 299 | , y = yout 300 | , stroke = stroke 301 | , cursor = encoding.cursor 302 | , href = encoding.href 303 | , tooltip = tooltip 304 | } 305 | 306 | 307 | {-| Apply each aggregate channel of an encoding to a data point. 308 | Where a channel is not an aggregate, the corresponding field in the returned 309 | `LineInternal` is `Nothing`. 310 | -} 311 | summarize : 312 | Scale xdomain Float 313 | -> Scale ydomain Float 314 | -> Line data xdomain ydomain 315 | -> List data 316 | -> LineInternal 317 | summarize xScale yScale encoding data = 318 | let 319 | summarizeMaybe getter = 320 | getter encoding 321 | |> Maybe.andThen (\channel -> Channel.summarize channel data) 322 | 323 | positionX = 324 | Field.summarize encoding.x.field data 325 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 326 | 327 | positionY = 328 | Field.summarize encoding.x.field data 329 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 330 | in 331 | { x = positionX 332 | , y = positionY 333 | , stroke = Maybe.map (\stroke -> Stroke.summarize stroke data) encoding.stroke 334 | , cursor = encoding.cursor 335 | , href = encoding.href 336 | , tooltip = summarizeMaybe .tooltip 337 | } 338 | 339 | 340 | {-| Structural comparison of encoding domain evaluated at two data poinst 341 | -} 342 | compareAt : Line data xdomain ydomain -> data -> data -> Order 343 | compareAt encoding d1 d2 = 344 | let 345 | comp getter = 346 | Channel.compareMaybeAt (getter encoding) d1 d2 347 | in 348 | case Channel.compareAt encoding.x d1 d2 of 349 | EQ -> 350 | case Channel.compareAt encoding.y d1 d2 of 351 | EQ -> 352 | case Maybe.maybe EQ (\stroke -> Stroke.compareAt stroke d1 d2) encoding.stroke of 353 | EQ -> 354 | comp .tooltip 355 | 356 | otherwise -> 357 | otherwise 358 | 359 | otherwise -> 360 | otherwise 361 | 362 | otherwise -> 363 | otherwise 364 | 365 | 366 | equalAt : Line data xdomain ydomain -> data -> data -> Bool 367 | equalAt encoding d1 d2 = 368 | let 369 | eq getter = 370 | Channel.equalMaybeAt (getter encoding) d1 d2 371 | in 372 | Channel.equalAt encoding.x d1 d2 373 | && Channel.equalAt encoding.y d1 d2 374 | && Maybe.maybe True (\stroke -> Stroke.equalAt stroke d1 d2) encoding.stroke 375 | && eq .tooltip 376 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Polygon.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Polygon exposing (Polygon, polygon, scenegraph, legends) 2 | 3 | import Facet.Internal.Encoding.Fill as Fill exposing (Fill) 4 | import Facet.Internal.Encoding.Stroke as Stroke exposing (Stroke) 5 | import Facet.Internal.Channel as Channel exposing (TextChannel, FloatChannel, PositionalChannel) 6 | import Facet.Internal.Field as Field 7 | import Facet.Internal.Legend exposing (LegendSpec) 8 | import Facet.List.Extra as List 9 | import Facet.Maybe.Extra as Maybe 10 | import Facet.Scale as Scale exposing (Scale) 11 | import Facet.Scenegraph as Scenegraph exposing (Scenegraph) 12 | import Facet.Scenegraph.Cursor as Cursor exposing (Cursor) 13 | import Facet.Scenegraph.Fill as Scenegraph 14 | import Facet.Scenegraph.Stroke as Scenegraph 15 | import Facet.Scenegraph.Interpolate exposing (Interpolate(..)) 16 | import Facet.Scenegraph.Mark as Mark exposing (Behaviour(..), Orientation(..)) 17 | import Facet.Theme as Theme 18 | 19 | 20 | type alias Polygon data xdomain ydomain = 21 | { x : PositionalChannel data xdomain 22 | , y : PositionalChannel data ydomain 23 | , interpolate : Interpolate 24 | , behavior : Mark.Behaviour 25 | , fill : Maybe (Fill data) 26 | , stroke : Maybe (Stroke data) 27 | , cursor : Maybe Cursor 28 | , href : Maybe String 29 | , tooltip : Maybe (TextChannel data) 30 | } 31 | 32 | 33 | 34 | -- Helpers --------------------------------------------------------------------- 35 | 36 | 37 | polygon : 38 | PositionalChannel data xdomain 39 | -> PositionalChannel data number 40 | -> Interpolate 41 | -> Behaviour 42 | -> Polygon data xdomain number 43 | polygon x y interpolate behaviour = 44 | Polygon x 45 | y 46 | interpolate 47 | behaviour 48 | Nothing 49 | Nothing 50 | Nothing 51 | Nothing 52 | Nothing 53 | 54 | 55 | 56 | -- LEGENDS --------------------------------------------------------------------- 57 | 58 | 59 | legends : Int -> Polygon data xdomain ydomain -> LegendSpec 60 | legends ticks encoding = 61 | { fillColor = encoding.fill |> Maybe.andThen .fill |> Maybe.andThen (Channel.legend ticks) 62 | , fillOpacity = encoding.fill |> Maybe.andThen .fillOpacity |> Maybe.andThen (Channel.legend ticks) 63 | , strokeColor = encoding.stroke |> Maybe.andThen .stroke |> Maybe.andThen (Channel.legend ticks) 64 | , strokeOpacity = encoding.stroke |> Maybe.andThen .strokeOpacity |> Maybe.andThen (Channel.legend ticks) 65 | , strokeWidth = encoding.stroke |> Maybe.andThen .strokeWidth |> Maybe.andThen (Channel.legend ticks) 66 | , strokeDash = encoding.stroke |> Maybe.andThen .strokeDash |> Maybe.andThen (Channel.legend ticks) 67 | , angle = Nothing 68 | , shape = Nothing 69 | , cornerRadius = Nothing 70 | , size = Nothing 71 | , width = Nothing 72 | } 73 | 74 | 75 | 76 | -- SCENEGRAPH ------------------------------------------------------------------ 77 | 78 | 79 | {-| An intermediate representation of an encoding which allows for all 80 | fields to possibly be `Nothing` 81 | -} 82 | type alias PolygonInternal = 83 | { x : Maybe (List (Maybe Float)) 84 | , y : Maybe (List (Maybe Float)) 85 | , fill : Maybe Scenegraph.Fill 86 | , stroke : Maybe Scenegraph.Stroke 87 | , cursor : Maybe Cursor 88 | , href : Maybe String 89 | , tooltip : Maybe String 90 | } 91 | 92 | 93 | {-| Combine `PolygonInternal`s with a preference for `mark2` (in practice, 94 | for each field in the two `RectInternal`s, only one _can_ have a `Just` 95 | value so the order of arguments does not matter.) 96 | -} 97 | combineIntermediate : PolygonInternal -> PolygonInternal -> PolygonInternal 98 | combineIntermediate mark1 mark2 = 99 | { x = mark2.x |> Maybe.orElse mark1.x 100 | , y = Maybe.orElse mark1.y mark2.y 101 | , fill = Maybe.orElse mark1.fill mark2.fill 102 | , stroke = Maybe.orElse mark1.stroke mark2.stroke 103 | , cursor = Maybe.orElse mark1.cursor mark2.cursor 104 | , href = Maybe.orElse mark1.href mark2.href 105 | , tooltip = Maybe.orElse mark1.tooltip mark2.tooltip 106 | } 107 | 108 | 109 | {-| Combine an `PathInternal` with required fields from the encoding 110 | and a theme to populate any un-encoded fields. 111 | -} 112 | combineWithThemeDefaults : Theme.Polygon -> Interpolate -> Behaviour -> PolygonInternal -> Maybe Mark.Polygon 113 | combineWithThemeDefaults theme interpolate behaviour markInternal = 114 | Maybe.map2 115 | (\x y -> 116 | { x = x 117 | , y = y 118 | , interpolate = interpolate 119 | , behaviour = behaviour 120 | , fill = Maybe.withDefault theme.fill markInternal.fill 121 | , stroke = Maybe.withDefault theme.stroke markInternal.stroke 122 | , cursor = Maybe.withDefault theme.cursor markInternal.cursor 123 | , href = markInternal.href 124 | , tooltip = markInternal.tooltip 125 | } 126 | ) 127 | markInternal.x 128 | markInternal.y 129 | 130 | 131 | {-| Test for an aggregate field in one of the channels 132 | -} 133 | containsAggregate : Polygon data xdomain ydomain -> Bool 134 | containsAggregate encoding = 135 | Channel.isAggregate encoding.x 136 | || Channel.isAggregate encoding.y 137 | || Maybe.maybe False Fill.containsAggregate encoding.fill 138 | || Maybe.maybe False Stroke.containsAggregate encoding.stroke 139 | || Maybe.maybe False Channel.isAggregate encoding.tooltip 140 | 141 | 142 | containsVector : Polygon data xdomain ydomain -> Bool 143 | containsVector encoding = 144 | Channel.isVector encoding.x 145 | || Channel.isVector encoding.y 146 | || Maybe.maybe False Fill.containsVector encoding.fill 147 | || Maybe.maybe False Stroke.containsVector encoding.stroke 148 | || Maybe.maybe False Channel.isVector encoding.tooltip 149 | 150 | 151 | 152 | {- Generate a scenegraph for the encoding -} 153 | 154 | 155 | scenegraph : 156 | Theme.Area 157 | -> Scale xdomain Float 158 | -> Scale ydomain Float 159 | -> List data 160 | -> Polygon data xdomain ydomain 161 | -> Scenegraph 162 | scenegraph theme xScale yScale data encoding = 163 | if containsAggregate encoding then 164 | data 165 | |> List.groupBy (compareAt encoding) 166 | (extract xScale yScale encoding) 167 | |> List.filterMap 168 | (\( mark, data ) -> 169 | let 170 | vector = 171 | extractVector xScale yScale encoding data 172 | 173 | agg = 174 | summarize xScale yScale encoding data 175 | in 176 | mark 177 | |> combineIntermediate vector 178 | |> combineIntermediate agg 179 | |> combineWithThemeDefaults theme encoding.interpolate encoding.behavior 180 | ) 181 | |> Scenegraph.Polygon 182 | else if containsVector encoding then 183 | data 184 | |> List.groupBy (compareAt encoding) 185 | (extract xScale yScale encoding) 186 | |> List.filterMap 187 | (\( mark, data ) -> 188 | let 189 | vector = 190 | extractVector xScale yScale encoding data 191 | in 192 | mark 193 | |> combineIntermediate vector 194 | |> combineWithThemeDefaults theme encoding.interpolate encoding.behavior 195 | ) 196 | |> Scenegraph.Polygon 197 | else 198 | List.filterMap 199 | (extract xScale yScale encoding 200 | >> combineWithThemeDefaults theme encoding.interpolate encoding.behavior 201 | ) 202 | data 203 | |> Scenegraph.Polygon 204 | 205 | 206 | 207 | -- APPLY AGGREGATE, VECTOR AND SCALAR FIELDS ----------------------------------- 208 | 209 | 210 | {-| Apply each scalar channel of an encoding to a data point. 211 | Where a channel is not a scalar, the corresponding field in the returned 212 | `RectInternal` is `Nothing`. 213 | -} 214 | extract : 215 | Scale xdomain Float 216 | -> Scale ydomain Float 217 | -> Polygon data xdomain ydomain 218 | -> data 219 | -> PolygonInternal 220 | extract xScale yScale encoding datum = 221 | let 222 | extractMaybe getter = 223 | getter encoding 224 | |> Maybe.andThen (\channel -> Channel.extract channel datum) 225 | 226 | positionX = 227 | Field.extract encoding.x.field datum 228 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 229 | 230 | positionY = 231 | Field.extract encoding.y.field datum 232 | |> Maybe.andThen (Scale.scale yScale >> Maybe.map (Just >> List.singleton)) 233 | 234 | fill = 235 | Maybe.map (\fill -> Fill.extract fill datum) encoding.fill 236 | 237 | stroke = 238 | Maybe.map (\stroke -> Stroke.extract stroke datum) encoding.stroke 239 | in 240 | { x = positionX 241 | , y = positionY 242 | , fill = fill 243 | , stroke = stroke 244 | , cursor = encoding.cursor 245 | , href = encoding.href 246 | , tooltip = extractMaybe .tooltip 247 | } 248 | 249 | 250 | {-| Apply each vector channel of an encoding to a data point. 251 | Where a channel is not a vector, the corresponding field in the returned 252 | `RectInternal` is `Nothing`. 253 | -} 254 | extractVector : 255 | Scale xdomain Float 256 | -> Scale ydomain Float 257 | -> Polygon data xdomain ydomain 258 | -> List data 259 | -> PolygonInternal 260 | extractVector xScale yScale encoding data = 261 | let 262 | xs = 263 | Field.extractVector encoding.x.field data 264 | |> List.map (Maybe.andThen (Scale.scale xScale)) 265 | 266 | ys = 267 | Field.extractVector encoding.y.field data 268 | |> List.map (Maybe.andThen (Scale.scale yScale)) 269 | 270 | xys = 271 | List.map2 272 | (\x y -> 273 | case Maybe.map2 (,) x y of 274 | Just ( x, y ) -> 275 | ( Just x, Just y ) 276 | 277 | _ -> 278 | ( Nothing, Nothing ) 279 | ) 280 | xs 281 | ys 282 | 283 | ( xout, yout ) = 284 | case xys of 285 | [] -> 286 | ( Nothing, Nothing ) 287 | 288 | _ -> 289 | let 290 | ( x, y ) = 291 | List.unzip xys 292 | in 293 | ( Just x, Just y ) 294 | 295 | fill = 296 | Maybe.andThen 297 | (\fill -> Fill.extractVector fill data |> List.head) 298 | encoding.fill 299 | 300 | stroke = 301 | Maybe.andThen 302 | (\stroke -> Stroke.extractVector stroke data |> List.head) 303 | encoding.stroke 304 | 305 | tooltip = 306 | encoding.tooltip 307 | |> Maybe.andThen 308 | (\channel -> 309 | Channel.extractVector channel data 310 | |> List.head 311 | >> Maybe.join 312 | ) 313 | in 314 | { x = xout 315 | , y = yout 316 | , fill = fill 317 | , stroke = stroke 318 | , cursor = encoding.cursor 319 | , href = encoding.href 320 | , tooltip = tooltip 321 | } 322 | 323 | 324 | {-| Apply each aggregate channel of an encoding to a data point. 325 | Where a channel is not an aggregate, the corresponding field in the returned 326 | `RectInternal` is `Nothing`. 327 | -} 328 | summarize : 329 | Scale xdomain Float 330 | -> Scale ydomain Float 331 | -> Polygon data xdomain ydomain 332 | -> List data 333 | -> PolygonInternal 334 | summarize xScale yScale encoding data = 335 | let 336 | summarizeMaybe getter = 337 | getter encoding 338 | |> Maybe.andThen (\channel -> Channel.summarize channel data) 339 | 340 | positionX = 341 | Field.summarize encoding.x.field data 342 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 343 | 344 | positionY = 345 | Field.summarize encoding.y.field data 346 | |> Maybe.andThen (Scale.scale yScale >> Maybe.map (Just >> List.singleton)) 347 | in 348 | { x = positionX 349 | , y = positionY 350 | , fill = Maybe.map (\fill -> Fill.summarize fill data) encoding.fill 351 | , stroke = Maybe.map (\stroke -> Stroke.summarize stroke data) encoding.stroke 352 | , cursor = encoding.cursor 353 | , href = encoding.href 354 | , tooltip = summarizeMaybe .tooltip 355 | } 356 | 357 | 358 | {-| Structural comparison of encoding domain evaluated at two data poinst 359 | -} 360 | compareAt : Polygon data xdomain ydomain -> data -> data -> Order 361 | compareAt encoding d1 d2 = 362 | let 363 | comp getter = 364 | Channel.compareMaybeAt (getter encoding) d1 d2 365 | in 366 | case Channel.compareAt encoding.x d1 d2 of 367 | EQ -> 368 | case Channel.compareAt encoding.y d1 d2 of 369 | EQ -> 370 | case Maybe.maybe EQ (\fill -> Fill.compareAt fill d1 d2) encoding.fill of 371 | EQ -> 372 | case Maybe.maybe EQ (\stroke -> Stroke.compareAt stroke d1 d2) encoding.stroke of 373 | EQ -> 374 | comp .tooltip 375 | 376 | otherwise -> 377 | otherwise 378 | 379 | otherwise -> 380 | otherwise 381 | 382 | otherwise -> 383 | otherwise 384 | 385 | otherwise -> 386 | otherwise 387 | 388 | 389 | equalAt : Polygon data xdomain ydomain -> data -> data -> Bool 390 | equalAt encoding d1 d2 = 391 | let 392 | eq getter = 393 | Channel.equalMaybeAt (getter encoding) d1 d2 394 | in 395 | Channel.equalAt encoding.x d1 d2 396 | && Channel.equalAt encoding.y d1 d2 397 | && Maybe.maybe True (\fill -> Fill.equalAt fill d1 d2) encoding.fill 398 | && Maybe.maybe True (\stroke -> Stroke.equalAt stroke d1 d2) encoding.stroke 399 | && eq .tooltip 400 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Position.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Position 2 | exposing 3 | ( Position(..) 4 | , extract 5 | , extractVector 6 | , summarize 7 | , containsAggregate 8 | , containsVector 9 | , compareAt 10 | , equalAt 11 | , isConstant 12 | ) 13 | 14 | import Facet.Internal.Channel as Channel exposing (PositionalChannel, FloatChannel) 15 | import Facet.Internal.Field as Field 16 | import Facet.Scale as Scale exposing (Scale) 17 | import Facet.Scenegraph.Position as SG 18 | import Facet.List.Extra as List 19 | 20 | 21 | {-| TODO: this needs overhauling when the corresponding changes are made in `Scenegraph` 22 | -} 23 | type Position data domain 24 | = PrimarySecondary (PositionalChannel data domain) (PositionalChannel data domain) 25 | | PrimaryExtent (PositionalChannel data domain) (FloatChannel data) 26 | | SecondaryExtent (PositionalChannel data domain) (FloatChannel data) 27 | | CenterExtent (PositionalChannel data domain) (FloatChannel data) 28 | 29 | 30 | isConstant : Position data domain -> Bool 31 | isConstant position = 32 | case position of 33 | PrimarySecondary x x2 -> 34 | Field.isConstant x.field && Field.isConstant x2.field 35 | 36 | PrimaryExtent x _ -> 37 | Field.isConstant x.field 38 | 39 | SecondaryExtent x _ -> 40 | Field.isConstant x.field 41 | 42 | CenterExtent x _ -> 43 | Field.isConstant x.field 44 | 45 | 46 | compareAt : Position data domain -> data -> data -> Order 47 | compareAt position d1 d2 = 48 | case position of 49 | PrimarySecondary position1 position2 -> 50 | case Channel.compareAt position1 d1 d2 of 51 | EQ -> 52 | Channel.compareAt position2 d1 d2 53 | 54 | otherwise -> 55 | otherwise 56 | 57 | PrimaryExtent position extent -> 58 | case Channel.compareAt position d1 d2 of 59 | EQ -> 60 | Channel.compareAt extent d1 d2 61 | 62 | otherwise -> 63 | otherwise 64 | 65 | SecondaryExtent position extent -> 66 | case Channel.compareAt position d1 d2 of 67 | EQ -> 68 | Channel.compareAt extent d1 d2 69 | 70 | otherwise -> 71 | otherwise 72 | 73 | CenterExtent position extent -> 74 | case Channel.compareAt position d1 d2 of 75 | EQ -> 76 | Channel.compareAt extent d1 d2 77 | 78 | otherwise -> 79 | otherwise 80 | 81 | 82 | equalAt : Position data domain -> data -> data -> Bool 83 | equalAt position d1 d2 = 84 | case position of 85 | PrimarySecondary position1 position2 -> 86 | Channel.equalAt position1 d1 d2 87 | && Channel.equalAt position2 d1 d2 88 | 89 | PrimaryExtent position extent -> 90 | Channel.equalAt position d1 d2 91 | && Channel.equalAt extent d1 d2 92 | 93 | SecondaryExtent position extent -> 94 | Channel.equalAt position d1 d2 95 | && Channel.equalAt extent d1 d2 96 | 97 | CenterExtent position extent -> 98 | Channel.equalAt position d1 d2 99 | && Channel.equalAt extent d1 d2 100 | 101 | 102 | extract : Scale domain Float -> Position data domain -> data -> Maybe SG.Position 103 | extract scale position datum = 104 | case position of 105 | PrimarySecondary primary secondary -> 106 | Maybe.map2 107 | SG.PrimarySecondary 108 | (Field.extract primary.field datum |> Maybe.andThen (Scale.scale scale)) 109 | (Field.extract secondary.field datum |> Maybe.andThen (Scale.scale scale)) 110 | 111 | PrimaryExtent { field } extent -> 112 | Maybe.map2 113 | SG.PrimaryExtent 114 | (Field.extract field datum |> Maybe.andThen (Scale.scale scale)) 115 | (Channel.extract extent datum) 116 | 117 | SecondaryExtent { field } extent -> 118 | Maybe.map2 119 | SG.SecondaryExtent 120 | (Field.extract field datum |> Maybe.andThen (Scale.scale scale)) 121 | (Channel.extract extent datum) 122 | 123 | CenterExtent { field } extent -> 124 | Maybe.map2 125 | SG.CenterExtent 126 | (Field.extract field datum |> Maybe.andThen (Scale.scale scale)) 127 | (Channel.extract extent datum) 128 | 129 | 130 | extractVector : Scale domain Float -> Position data domain -> List data -> List (Maybe SG.Position) 131 | extractVector scale position data = 132 | case position of 133 | PrimarySecondary primary secondary -> 134 | let 135 | xs = 136 | data 137 | |> Field.extractVector primary.field 138 | |> List.map (Maybe.andThen (Scale.scale scale)) 139 | 140 | ys = 141 | data 142 | |> Field.extractVector secondary.field 143 | |> List.map (Maybe.andThen (Scale.scale scale)) 144 | in 145 | List.zipCycle xs ys 146 | |> List.map (\( x, y ) -> Maybe.map2 SG.PrimarySecondary x y) 147 | 148 | PrimaryExtent { field } extent -> 149 | let 150 | xs = 151 | data 152 | |> Field.extractVector field 153 | |> List.map (Maybe.andThen (Scale.scale scale)) 154 | 155 | ys = 156 | data |> Channel.extractVector extent 157 | in 158 | List.zipCycle xs ys 159 | |> List.map (\( x, y ) -> Maybe.map2 SG.PrimaryExtent x y) 160 | 161 | SecondaryExtent { field } extent -> 162 | let 163 | xs = 164 | data 165 | |> Field.extractVector field 166 | |> List.map (Maybe.andThen (Scale.scale scale)) 167 | 168 | ys = 169 | data |> Channel.extractVector extent 170 | in 171 | List.zipCycle xs ys 172 | |> List.map (\( x, y ) -> Maybe.map2 SG.SecondaryExtent x y) 173 | 174 | CenterExtent { field } extent -> 175 | let 176 | xs = 177 | data 178 | |> Field.extractVector field 179 | |> List.map (Maybe.andThen (Scale.scale scale)) 180 | 181 | ys = 182 | data |> Channel.extractVector extent 183 | in 184 | List.zipCycle xs ys 185 | |> List.map (\( x, y ) -> Maybe.map2 SG.CenterExtent x y) 186 | 187 | 188 | summarize : Scale domain Float -> Position data domain -> List data -> Maybe SG.Position 189 | summarize scale position data = 190 | case position of 191 | PrimarySecondary primary secondary -> 192 | Maybe.map2 193 | SG.PrimarySecondary 194 | (Field.summarize primary.field data |> Maybe.andThen (Scale.scale scale)) 195 | (Field.summarize secondary.field data |> Maybe.andThen (Scale.scale scale)) 196 | 197 | PrimaryExtent { field } extent -> 198 | Maybe.map2 199 | SG.PrimaryExtent 200 | (Field.summarize field data |> Maybe.andThen (Scale.scale scale)) 201 | (Channel.summarize extent data) 202 | 203 | SecondaryExtent { field } extent -> 204 | Maybe.map2 205 | SG.SecondaryExtent 206 | (Field.summarize field data |> Maybe.andThen (Scale.scale scale)) 207 | (Channel.summarize extent data) 208 | 209 | CenterExtent { field } extent -> 210 | Maybe.map2 211 | SG.CenterExtent 212 | (Field.summarize field data |> Maybe.andThen (Scale.scale scale)) 213 | (Channel.summarize extent data) 214 | 215 | 216 | containsAggregate : Position data domain -> Bool 217 | containsAggregate position = 218 | case position of 219 | PrimarySecondary primary secondary -> 220 | primary.isAggregate || secondary.isAggregate 221 | 222 | PrimaryExtent primary extent -> 223 | primary.isAggregate || extent.isAggregate 224 | 225 | SecondaryExtent secondary extent -> 226 | secondary.isAggregate || extent.isAggregate 227 | 228 | CenterExtent center extent -> 229 | center.isAggregate || extent.isAggregate 230 | 231 | 232 | containsVector : Position data domain -> Bool 233 | containsVector position = 234 | case position of 235 | PrimarySecondary primary secondary -> 236 | primary.isVector || secondary.isVector 237 | 238 | PrimaryExtent primary extent -> 239 | primary.isVector || extent.isVector 240 | 241 | SecondaryExtent secondary extent -> 242 | secondary.isVector || extent.isVector 243 | 244 | CenterExtent center extent -> 245 | center.isVector || extent.isVector 246 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Rect.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Rect exposing (Rect, bar, rect, scenegraph, legends) 2 | 3 | import Facet.Internal.Channel as Channel exposing (FloatChannel, PositionalChannel, TextChannel) 4 | import Facet.Internal.Encoding.Fill as Fill exposing (Fill) 5 | import Facet.Internal.Encoding.Position as Position exposing (Position(..)) 6 | import Facet.Internal.Encoding.Stroke as Stroke exposing (Stroke) 7 | import Facet.Internal.Legend exposing (LegendSpec) 8 | import Facet.Maybe.Extra as Maybe 9 | import Facet.List.Extra as List 10 | import Facet.Scale as Scale exposing (Scale) 11 | import Facet.Scenegraph as Scenegraph exposing (Scenegraph) 12 | import Facet.Scenegraph.Mark as Mark 13 | import Facet.Scenegraph.Cursor as Cursor exposing (Cursor) 14 | import Facet.Scenegraph.Fill as Scenegraph 15 | import Facet.Scenegraph.Stroke as Scenegraph 16 | import Facet.Scenegraph.Position as ScenegraphPosition 17 | import Facet.Theme as Theme 18 | 19 | 20 | {-| Rectangles, as in bar charts and timelines 21 | -} 22 | type alias Rect data xdomain ydomain = 23 | { x : Position data xdomain 24 | , y : Position data ydomain 25 | , cornerRadius : Maybe (FloatChannel data) 26 | , fill : Maybe (Fill data) 27 | , stroke : Maybe (Stroke data) 28 | , cursor : Maybe Cursor 29 | , href : Maybe String 30 | , tooltip : Maybe (TextChannel data) 31 | } 32 | 33 | 34 | 35 | -- Helpers --------------------------------------------------------------------- 36 | 37 | 38 | bar : 39 | PositionalChannel data xdomain 40 | -> FloatChannel data 41 | -> PositionalChannel data ydomain 42 | -> PositionalChannel data ydomain 43 | -> Rect data xdomain ydomain 44 | bar x width y y2 = 45 | Rect 46 | (CenterExtent x width) 47 | (PrimarySecondary y y2) 48 | Nothing 49 | Nothing 50 | Nothing 51 | Nothing 52 | Nothing 53 | Nothing 54 | 55 | 56 | rect : 57 | PositionalChannel data xdomain 58 | -> PositionalChannel data xdomain 59 | -> PositionalChannel data ydomain 60 | -> PositionalChannel data ydomain 61 | -> Rect data xdomain ydomain 62 | rect x x2 y y2 = 63 | Rect 64 | (PrimarySecondary x x2) 65 | (PrimarySecondary y y2) 66 | Nothing 67 | Nothing 68 | Nothing 69 | Nothing 70 | Nothing 71 | Nothing 72 | 73 | 74 | 75 | -- LEGENDS --------------------------------------------------------------------- 76 | 77 | 78 | legends : Int -> Rect data xdomain ydomain -> LegendSpec 79 | legends ticks encoding = 80 | { fillColor = encoding.fill |> Maybe.andThen .fill |> Maybe.andThen (Channel.legend ticks) 81 | , fillOpacity = encoding.fill |> Maybe.andThen .fillOpacity |> Maybe.andThen (Channel.legend ticks) 82 | , strokeColor = encoding.stroke |> Maybe.andThen .stroke |> Maybe.andThen (Channel.legend ticks) 83 | , strokeOpacity = encoding.stroke |> Maybe.andThen .strokeOpacity |> Maybe.andThen (Channel.legend ticks) 84 | , strokeWidth = encoding.stroke |> Maybe.andThen .strokeWidth |> Maybe.andThen (Channel.legend ticks) 85 | , strokeDash = encoding.stroke |> Maybe.andThen .strokeDash |> Maybe.andThen (Channel.legend ticks) 86 | , angle = Nothing 87 | , shape = Nothing 88 | , cornerRadius = encoding.cornerRadius |> Maybe.andThen (Channel.legend ticks) 89 | , size = Nothing 90 | , width = Nothing 91 | } 92 | 93 | 94 | 95 | -- SCENEGRAPH ------------------------------------------------------------------ 96 | 97 | 98 | {-| An intermediate representation of an encoding which allows for all 99 | fields to possibly be `Nothing` 100 | -} 101 | type alias RectInternal = 102 | { x : Maybe ScenegraphPosition.Position 103 | , y : Maybe ScenegraphPosition.Position 104 | , cornerRadius : Maybe Float 105 | , fill : Maybe Scenegraph.Fill 106 | , stroke : Maybe Scenegraph.Stroke 107 | , cursor : Maybe Cursor 108 | , href : Maybe String 109 | , tooltip : Maybe String 110 | } 111 | 112 | 113 | {-| Combine `RectInternal`s with a preference for `mark2` (in practice, 114 | for each field in the two `RectInternal`s, only one _can_ have a `Just` 115 | value so the order of arguments does not matter.) 116 | -} 117 | combineIntermediate : RectInternal -> RectInternal -> RectInternal 118 | combineIntermediate mark1 mark2 = 119 | { x = mark2.x |> Maybe.orElse mark1.x 120 | , y = Maybe.orElse mark1.y mark2.y 121 | , cornerRadius = Maybe.orElse mark1.cornerRadius mark2.cornerRadius 122 | , fill = Maybe.orElse mark1.fill mark2.fill 123 | , stroke = Maybe.orElse mark1.stroke mark2.stroke 124 | , cursor = Maybe.orElse mark1.cursor mark2.cursor 125 | , href = Maybe.orElse mark1.href mark2.href 126 | , tooltip = Maybe.orElse mark1.tooltip mark2.tooltip 127 | } 128 | 129 | 130 | {-| Combine an `RectInternal` with a theme to populate any un-encoded fields. 131 | -} 132 | combineWithThemeDefaults : Theme.Rect -> RectInternal -> Maybe Mark.Rect 133 | combineWithThemeDefaults theme markInternal = 134 | Maybe.map2 135 | (\x y -> 136 | { x = x 137 | , y = y 138 | , cornerRadius = Maybe.withDefault theme.cornerRadius markInternal.cornerRadius 139 | , fill = Maybe.withDefault theme.fill markInternal.fill 140 | , stroke = Maybe.withDefault theme.stroke markInternal.stroke 141 | , cursor = Maybe.withDefault theme.cursor markInternal.cursor 142 | , href = markInternal.href 143 | , tooltip = markInternal.tooltip 144 | } 145 | ) 146 | markInternal.x 147 | markInternal.y 148 | 149 | 150 | {-| Test for an aggregate field in one of the channels 151 | -} 152 | containsAggregate : Rect data xdomain ydomain -> Bool 153 | containsAggregate encoding = 154 | Position.containsAggregate encoding.x 155 | || Position.containsAggregate encoding.y 156 | || Maybe.maybe False Channel.isAggregate encoding.cornerRadius 157 | || Maybe.maybe False Fill.containsAggregate encoding.fill 158 | || Maybe.maybe False Stroke.containsAggregate encoding.stroke 159 | || Maybe.maybe False Channel.isAggregate encoding.tooltip 160 | 161 | 162 | {-| Test for an vector field in one of the channels 163 | -} 164 | containsVector : Rect data xdomain ydomain -> Bool 165 | containsVector encoding = 166 | Position.containsVector encoding.x 167 | || Position.containsVector encoding.y 168 | || Maybe.maybe False Channel.isVector encoding.cornerRadius 169 | || Maybe.maybe False Fill.containsVector encoding.fill 170 | || Maybe.maybe False Stroke.containsVector encoding.stroke 171 | || Maybe.maybe False Channel.isVector encoding.tooltip 172 | 173 | 174 | {-| Generate a scenegraph for the encoding 175 | -} 176 | scenegraph : 177 | Theme.Rect 178 | -> Scale xdomain Float 179 | -> Scale ydomain Float 180 | -> List data 181 | -> Rect data xdomain ydomain 182 | -> Scenegraph 183 | scenegraph theme xScale yScale data encoding = 184 | {- if the encoding contains an aggregate field we need to 185 | group the data by all non-aggregate fields before applying 186 | the aggregate fields 187 | -} 188 | if containsAggregate encoding then 189 | data 190 | |> List.groupBy (compareAt encoding) 191 | (extract xScale yScale encoding) 192 | |> List.filterMap 193 | (\( mark, data ) -> 194 | let 195 | vector = 196 | extractVector xScale yScale encoding data 197 | 198 | agg = 199 | summarize xScale yScale encoding data 200 | in 201 | mark 202 | |> combineIntermediate vector 203 | |> combineIntermediate agg 204 | |> combineWithThemeDefaults theme 205 | ) 206 | |> Scenegraph.Rect 207 | else if containsVector encoding then 208 | data 209 | |> List.groupBy (compareAt encoding) 210 | (extract xScale yScale encoding) 211 | |> List.filterMap 212 | (\( mark, data ) -> 213 | let 214 | vector = 215 | extractVector xScale yScale encoding data 216 | in 217 | mark 218 | |> combineIntermediate vector 219 | |> combineWithThemeDefaults theme 220 | ) 221 | |> Scenegraph.Rect 222 | else 223 | List.filterMap 224 | (extract xScale yScale encoding 225 | >> combineWithThemeDefaults theme 226 | ) 227 | data 228 | |> Scenegraph.Rect 229 | 230 | 231 | 232 | -- APPLY AGGREGATE, VECTOR AND SCALAR FIELDS ----------------------------------- 233 | 234 | 235 | {-| Apply each scalar channel of an encoding to a data point. 236 | Where a channel is not a scalar, the corresponding field in the returned 237 | `RectInternal` is `Nothing`. 238 | -} 239 | extract : 240 | Scale xdomain Float 241 | -> Scale ydomain Float 242 | -> Rect data xdomain ydomain 243 | -> data 244 | -> RectInternal 245 | extract xScale yScale encoding datum = 246 | let 247 | extractMaybe getter = 248 | Maybe.andThen (\channel -> Channel.extract channel datum) <| 249 | getter encoding 250 | 251 | positionX = 252 | Position.extract xScale encoding.x datum 253 | 254 | positionY = 255 | Position.extract yScale encoding.y datum 256 | in 257 | { x = positionX 258 | , y = positionY 259 | , cornerRadius = extractMaybe .cornerRadius 260 | , fill = Maybe.map (\fill -> Fill.extract fill datum) encoding.fill 261 | , stroke = Maybe.map (\stroke -> Stroke.extract stroke datum) encoding.stroke 262 | , cursor = encoding.cursor 263 | , href = encoding.href 264 | , tooltip = extractMaybe .tooltip 265 | } 266 | 267 | 268 | {-| Apply each vector channel of an encoding to a data point. 269 | Where a channel is not a vector, the corresponding field in the returned 270 | `RectInternal` is `Nothing`. 271 | -} 272 | extractVector : 273 | Scale xdomain Float 274 | -> Scale ydomain Float 275 | -> Rect data xdomain ydomain 276 | -> List data 277 | -> RectInternal 278 | extractVector xScale yScale encoding data = 279 | let 280 | extractVectorMaybe getter = 281 | getter encoding 282 | |> Maybe.andThen 283 | (\channel -> 284 | Channel.extractVector channel data 285 | |> List.head 286 | |> Maybe.join 287 | ) 288 | 289 | positionX = 290 | Position.extractVector xScale encoding.x data 291 | |> List.head 292 | |> Maybe.join 293 | 294 | positionY = 295 | Position.extractVector yScale encoding.y data 296 | |> List.head 297 | |> Maybe.join 298 | 299 | fill = 300 | Maybe.andThen 301 | (\fill -> Fill.extractVector fill data |> List.head) 302 | encoding.fill 303 | 304 | stroke = 305 | Maybe.andThen 306 | (\stroke -> Stroke.extractVector stroke data |> List.head) 307 | encoding.stroke 308 | in 309 | { x = positionX 310 | , y = positionY 311 | , cornerRadius = extractVectorMaybe .cornerRadius 312 | , fill = fill 313 | , stroke = stroke 314 | , cursor = encoding.cursor 315 | , href = encoding.href 316 | , tooltip = extractVectorMaybe .tooltip 317 | } 318 | 319 | 320 | {-| Apply each aggregate channel of an encoding to a data point. 321 | Where a channel is not an aggregate, the corresponding field in the returned 322 | `RectInternal` is `Nothing`. 323 | -} 324 | summarize : 325 | Scale xdomain Float 326 | -> Scale ydomain Float 327 | -> Rect data xdomain ydomain 328 | -> List data 329 | -> RectInternal 330 | summarize xScale yScale encoding data = 331 | let 332 | summarizeMaybe getter = 333 | getter encoding 334 | |> Maybe.andThen (\channel -> Channel.summarize channel data) 335 | 336 | positionX = 337 | Position.summarize xScale encoding.x data 338 | 339 | positionY = 340 | Position.summarize yScale encoding.y data 341 | in 342 | { x = positionX 343 | , y = positionY 344 | , cornerRadius = summarizeMaybe .cornerRadius 345 | , fill = Maybe.map (\fill -> Fill.summarize fill data) encoding.fill 346 | , stroke = Maybe.map (\stroke -> Stroke.summarize stroke data) encoding.stroke 347 | , cursor = encoding.cursor 348 | , href = encoding.href 349 | , tooltip = summarizeMaybe .tooltip 350 | } 351 | 352 | 353 | 354 | -- Comparison and equality ----------------------------------------------------- 355 | 356 | 357 | {-| Compare the encoding at two data points 358 | -} 359 | compareAt : Rect data xdomain ydomain -> data -> data -> Order 360 | compareAt encoding d1 d2 = 361 | let 362 | comp getter = 363 | Channel.compareMaybeAt (getter encoding) d1 d2 364 | in 365 | case Position.compareAt encoding.x d1 d2 of 366 | EQ -> 367 | case Position.compareAt encoding.y d1 d2 of 368 | EQ -> 369 | case comp .cornerRadius of 370 | EQ -> 371 | case Maybe.maybe EQ (\fill -> Fill.compareAt fill d1 d2) encoding.fill of 372 | EQ -> 373 | case Maybe.maybe EQ (\stroke -> Stroke.compareAt stroke d1 d2) encoding.stroke of 374 | EQ -> 375 | comp .tooltip 376 | 377 | otherwise -> 378 | otherwise 379 | 380 | otherwise -> 381 | otherwise 382 | 383 | otherwise -> 384 | otherwise 385 | 386 | otherwise -> 387 | otherwise 388 | 389 | otherwise -> 390 | otherwise 391 | 392 | 393 | {-| Test for structural equality of the encoding at two data points 394 | -} 395 | equalAt : Rect data xdomain ydomain -> data -> data -> Bool 396 | equalAt encoding d1 d2 = 397 | let 398 | eq getter = 399 | Channel.equalMaybeAt (getter encoding) d1 d2 400 | in 401 | Position.equalAt encoding.x d1 d2 402 | && Position.equalAt encoding.y d1 d2 403 | && eq .cornerRadius 404 | && Maybe.maybe True (\fill -> Fill.equalAt fill d1 d2) encoding.fill 405 | && Maybe.maybe True (\stroke -> Stroke.equalAt stroke d1 d2) encoding.stroke 406 | && eq .tooltip 407 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Rule.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Rule exposing (Rule, rule, scenegraph, legends) 2 | 3 | import Facet.Internal.Channel as Channel exposing (TextChannel, PositionalChannel) 4 | import Facet.Internal.Encoding.Position as Position exposing (Position(..)) 5 | import Facet.Internal.Encoding.Stroke as Stroke exposing (Stroke) 6 | import Facet.Internal.Legend exposing (LegendSpec) 7 | import Facet.List.Extra as List 8 | import Facet.Maybe.Extra as Maybe 9 | import Facet.Scale as Scale exposing (Scale) 10 | import Facet.Scenegraph as Scenegraph exposing (Scenegraph) 11 | import Facet.Scenegraph.Cursor as Cursor exposing (Cursor) 12 | import Facet.Scenegraph.Position as Scenegraph 13 | import Facet.Scenegraph.Stroke as Scenegraph 14 | import Facet.Scenegraph.Mark as Mark 15 | import Facet.Theme as Theme 16 | 17 | 18 | type alias Rule data xdomain ydomain = 19 | { x : Position data xdomain 20 | , y : Position data ydomain 21 | , stroke : Maybe (Stroke data) 22 | , cursor : Maybe Cursor 23 | , href : Maybe String 24 | , tooltip : Maybe (TextChannel data) 25 | } 26 | 27 | 28 | 29 | -- Helpers --------------------------------------------------------------------- 30 | 31 | 32 | rule : 33 | PositionalChannel data xdomain 34 | -> PositionalChannel data xdomain 35 | -> PositionalChannel data ydomain 36 | -> PositionalChannel data ydomain 37 | -> Rule data xdomain ydomain 38 | rule x x2 y y2 = 39 | let 40 | xpos = 41 | PrimarySecondary x x2 42 | 43 | ypos = 44 | PrimarySecondary y y2 45 | in 46 | Rule xpos ypos Nothing Nothing Nothing Nothing 47 | 48 | 49 | 50 | -- LEGENDS --------------------------------------------------------------------- 51 | 52 | 53 | legends : Int -> Rule data xdomain ydomain -> LegendSpec 54 | legends ticks encoding = 55 | { fillColor = Nothing 56 | , fillOpacity = Nothing 57 | , strokeColor = encoding.stroke |> Maybe.andThen .stroke |> Maybe.andThen (Channel.legend ticks) 58 | , strokeOpacity = encoding.stroke |> Maybe.andThen .strokeOpacity |> Maybe.andThen (Channel.legend ticks) 59 | , strokeWidth = encoding.stroke |> Maybe.andThen .strokeWidth |> Maybe.andThen (Channel.legend ticks) 60 | , strokeDash = encoding.stroke |> Maybe.andThen .strokeDash |> Maybe.andThen (Channel.legend ticks) 61 | , angle = Nothing 62 | , shape = Nothing 63 | , cornerRadius = Nothing 64 | , size = Nothing 65 | , width = Nothing 66 | } 67 | 68 | 69 | 70 | -- SCENEGRAPH ------------------------------------------------------------------ 71 | 72 | 73 | {-| An intermediate representation of an encoding which allows for all 74 | fields to possibly be `Nothing` 75 | -} 76 | type alias RuleInternal = 77 | { x : Maybe Scenegraph.Position 78 | , y : Maybe Scenegraph.Position 79 | , stroke : Maybe Scenegraph.Stroke 80 | , cursor : Maybe Cursor 81 | , href : Maybe String 82 | , tooltip : Maybe String 83 | } 84 | 85 | 86 | {-| Combine `RuleInternal`s with a preference for `mark2` (in practice, 87 | for each field in the two `RuleInternal`s, only one _can_ have a `Just` 88 | value so the order of arguments does not matter.) 89 | -} 90 | combineIntermediate : RuleInternal -> RuleInternal -> RuleInternal 91 | combineIntermediate mark1 mark2 = 92 | { x = Maybe.orElse mark1.x mark2.x 93 | , y = Maybe.orElse mark1.y mark2.y 94 | , stroke = Maybe.orElse mark1.stroke mark2.stroke 95 | , cursor = Maybe.orElse mark1.cursor mark2.cursor 96 | , href = Maybe.orElse mark1.href mark2.href 97 | , tooltip = Maybe.orElse mark1.tooltip mark2.tooltip 98 | } 99 | 100 | 101 | {-| Combine an `LineInternal` with required fields from the encoding 102 | and a theme to populate any un-encoded fields. 103 | -} 104 | combineWithThemeDefaults : Theme.Rule -> RuleInternal -> Maybe Mark.Rule 105 | combineWithThemeDefaults theme markInternal = 106 | Maybe.map2 107 | (\x y -> 108 | { x = x 109 | , y = y 110 | , stroke = Maybe.withDefault theme.stroke markInternal.stroke 111 | , cursor = Maybe.withDefault theme.cursor markInternal.cursor 112 | , href = markInternal.href 113 | , tooltip = markInternal.tooltip 114 | } 115 | ) 116 | markInternal.x 117 | markInternal.y 118 | 119 | 120 | {-| Test for an aggregate field in one of the channels 121 | -} 122 | containsAggregate : Rule data xdomain ydomain -> Bool 123 | containsAggregate encoding = 124 | Position.containsAggregate encoding.x 125 | || Position.containsAggregate encoding.y 126 | || Maybe.maybe False Stroke.containsAggregate encoding.stroke 127 | || Maybe.maybe False Channel.isAggregate encoding.tooltip 128 | 129 | 130 | containsVector : Rule data xdomain ydomain -> Bool 131 | containsVector encoding = 132 | Position.containsVector encoding.x 133 | || Position.containsVector encoding.y 134 | || Maybe.maybe False Stroke.containsVector encoding.stroke 135 | || Maybe.maybe False Channel.isVector encoding.tooltip 136 | 137 | 138 | 139 | {- Generate a scenegraph for the encoding -} 140 | 141 | 142 | scenegraph : 143 | Theme.Rule 144 | -> Scale xdomain Float 145 | -> Scale ydomain Float 146 | -> List data 147 | -> Rule data xdomain ydomain 148 | -> Scenegraph 149 | scenegraph theme xScale yScale data encoding = 150 | if containsAggregate encoding then 151 | data 152 | |> List.groupBy (compareAt encoding) 153 | (extract xScale yScale encoding) 154 | |> List.filterMap 155 | (\( mark, data ) -> 156 | let 157 | vector = 158 | extractVector xScale yScale encoding data 159 | 160 | agg = 161 | summarize xScale yScale encoding data 162 | in 163 | mark 164 | |> combineIntermediate vector 165 | |> combineIntermediate agg 166 | |> combineWithThemeDefaults theme 167 | ) 168 | |> Scenegraph.Rule 169 | else if containsVector encoding then 170 | data 171 | |> List.groupBy (compareAt encoding) 172 | (extract xScale yScale encoding) 173 | |> List.filterMap 174 | (\( mark, data ) -> 175 | let 176 | vector = 177 | extractVector xScale yScale encoding data 178 | in 179 | mark 180 | |> combineIntermediate vector 181 | |> combineWithThemeDefaults theme 182 | ) 183 | |> Scenegraph.Rule 184 | else 185 | List.filterMap 186 | (extract xScale yScale encoding 187 | >> combineWithThemeDefaults theme 188 | ) 189 | data 190 | |> Scenegraph.Rule 191 | 192 | 193 | 194 | -- APPLY AGGREGATE, VECTOR AND SCALAR FIELDS ----------------------------------- 195 | 196 | 197 | {-| Apply each scalar channel of an encoding to a data point. 198 | Where a channel is not a scalar, the corresponding field in the returned 199 | `RuleInternal` is `Nothing`. 200 | -} 201 | extract : 202 | Scale xdomain Float 203 | -> Scale ydomain Float 204 | -> Rule data xdomain ydomain 205 | -> data 206 | -> RuleInternal 207 | extract xScale yScale encoding datum = 208 | let 209 | extractMaybe getter = 210 | getter encoding 211 | |> Maybe.andThen (\channel -> Channel.extract channel datum) 212 | 213 | positionX = 214 | Position.extract xScale encoding.x datum 215 | 216 | positionY = 217 | Position.extract yScale encoding.y datum 218 | 219 | stroke = 220 | Maybe.map (\stroke -> Stroke.extract stroke datum) encoding.stroke 221 | in 222 | { x = positionX 223 | , y = positionY 224 | , stroke = stroke 225 | , cursor = encoding.cursor 226 | , href = encoding.href 227 | , tooltip = extractMaybe .tooltip 228 | } 229 | 230 | 231 | {-| Apply each vector channel of an encoding to a data point. 232 | Where a channel is not a vector, the corresponding field in the returned 233 | `RuleInternal` is `Nothing`. 234 | -} 235 | extractVector : 236 | Scale xdomain Float 237 | -> Scale ydomain Float 238 | -> Rule data xdomain ydomain 239 | -> List data 240 | -> RuleInternal 241 | extractVector xScale yScale encoding data = 242 | let 243 | xs = 244 | Position.extractVector xScale encoding.x data 245 | |> List.head 246 | |> Maybe.join 247 | 248 | ys = 249 | Position.extractVector yScale encoding.y data 250 | |> List.head 251 | |> Maybe.join 252 | 253 | ( x, y ) = 254 | Maybe.map2 (\x y -> ( Just x, Just y )) xs ys 255 | |> Maybe.withDefault ( Nothing, Nothing ) 256 | 257 | stroke = 258 | Maybe.andThen 259 | (\stroke -> Stroke.extractVector stroke data |> List.head) 260 | encoding.stroke 261 | 262 | tooltip = 263 | encoding.tooltip 264 | |> Maybe.andThen 265 | (\channel -> 266 | Channel.extractVector channel data 267 | |> List.head 268 | >> Maybe.join 269 | ) 270 | in 271 | { x = x 272 | , y = y 273 | , stroke = stroke 274 | , cursor = encoding.cursor 275 | , href = encoding.href 276 | , tooltip = tooltip 277 | } 278 | 279 | 280 | {-| Apply each aggregate channel of an encoding to a data point. 281 | Where a channel is not an aggregate, the corresponding field in the returned 282 | `RuleInternal` is `Nothing`. 283 | -} 284 | summarize : 285 | Scale xdomain Float 286 | -> Scale ydomain Float 287 | -> Rule data xdomain ydomain 288 | -> List data 289 | -> RuleInternal 290 | summarize xScale yScale encoding data = 291 | let 292 | summarizeMaybe getter = 293 | getter encoding 294 | |> Maybe.andThen (\channel -> Channel.summarize channel data) 295 | 296 | positionX = 297 | Position.summarize xScale encoding.x data 298 | 299 | positionY = 300 | Position.summarize yScale encoding.y data 301 | in 302 | { x = positionX 303 | , y = positionY 304 | , stroke = Maybe.map (\stroke -> Stroke.summarize stroke data) encoding.stroke 305 | , cursor = encoding.cursor 306 | , href = encoding.href 307 | , tooltip = summarizeMaybe .tooltip 308 | } 309 | 310 | 311 | {-| Structural comparison of encoding domain evaluated at two data poinst 312 | -} 313 | compareAt : Rule data xdomain ydomain -> data -> data -> Order 314 | compareAt encoding d1 d2 = 315 | let 316 | comp getter = 317 | Channel.compareMaybeAt (getter encoding) d1 d2 318 | in 319 | case Position.compareAt encoding.x d1 d2 of 320 | EQ -> 321 | case Position.compareAt encoding.y d1 d2 of 322 | EQ -> 323 | case Maybe.maybe EQ (\stroke -> Stroke.compareAt stroke d1 d2) encoding.stroke of 324 | EQ -> 325 | comp .tooltip 326 | 327 | otherwise -> 328 | otherwise 329 | 330 | otherwise -> 331 | otherwise 332 | 333 | otherwise -> 334 | otherwise 335 | 336 | 337 | equalAt : Rule data xdomain ydomain -> data -> data -> Bool 338 | equalAt encoding d1 d2 = 339 | let 340 | eq getter = 341 | Channel.equalMaybeAt (getter encoding) d1 d2 342 | in 343 | Position.equalAt encoding.x d1 d2 344 | && Position.equalAt encoding.y d1 d2 345 | && Maybe.maybe True (\stroke -> Stroke.equalAt stroke d1 d2) encoding.stroke 346 | && eq .tooltip 347 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Stroke.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Stroke 2 | exposing 3 | ( Stroke 4 | , containsAggregate 5 | , containsVector 6 | , extract 7 | , extractVector 8 | , summarize 9 | , equalAt 10 | , compareAt 11 | , empty 12 | ) 13 | 14 | {-| 15 | @docs Stroke, containsAggregate, extractOrElse, summarizeOrElse 16 | -} 17 | 18 | import Color exposing (Color) 19 | import Facet.Internal.Channel as Channel exposing (ColorChannel, FloatChannel, StrokeDashChannel) 20 | import Facet.Maybe.Extra as Maybe 21 | import Facet.Scenegraph.Stroke as Mark 22 | 23 | 24 | {-| -} 25 | type alias Stroke data = 26 | { stroke : Maybe (ColorChannel data) 27 | , strokeOpacity : Maybe (FloatChannel data) 28 | , strokeWidth : Maybe (FloatChannel data) 29 | , strokeDash : Maybe (StrokeDashChannel data) 30 | } 31 | 32 | 33 | empty : Stroke data 34 | empty = 35 | Stroke Nothing Nothing Nothing Nothing 36 | 37 | 38 | equalAt : Stroke data -> data -> data -> Bool 39 | equalAt stroke d1 d2 = 40 | let 41 | eq getter = 42 | Channel.equalMaybeAt (getter stroke) d1 d2 43 | in 44 | eq .stroke 45 | && eq .strokeOpacity 46 | && eq .strokeWidth 47 | && eq .strokeDash 48 | 49 | 50 | {-| Structural comparison of `Stroke` encoding domain evaluated at two data poinst 51 | -} 52 | compareAt : Stroke data -> data -> data -> Order 53 | compareAt stroke d1 d2 = 54 | let 55 | comp getter = 56 | Channel.compareMaybeAt (getter stroke) d1 d2 57 | in 58 | case comp .stroke of 59 | EQ -> 60 | case comp .strokeOpacity of 61 | EQ -> 62 | case comp .strokeWidth of 63 | EQ -> 64 | comp .strokeDash 65 | 66 | otherwise -> 67 | otherwise 68 | 69 | otherwise -> 70 | otherwise 71 | 72 | otherwise -> 73 | otherwise 74 | 75 | 76 | {-| -} 77 | containsAggregate : 78 | Stroke data 79 | -> Bool 80 | containsAggregate stroke = 81 | Maybe.maybe False Channel.isAggregate stroke.stroke 82 | || Maybe.maybe False Channel.isAggregate stroke.strokeOpacity 83 | || Maybe.maybe False Channel.isAggregate stroke.strokeWidth 84 | || Maybe.maybe False Channel.isAggregate stroke.strokeDash 85 | 86 | 87 | {-| -} 88 | containsVector : 89 | Stroke data 90 | -> Bool 91 | containsVector stroke = 92 | Maybe.maybe False Channel.isVector stroke.stroke 93 | || Maybe.maybe False Channel.isVector stroke.strokeOpacity 94 | || Maybe.maybe False Channel.isVector stroke.strokeWidth 95 | || Maybe.maybe False Channel.isVector stroke.strokeDash 96 | 97 | 98 | extract : Stroke data -> data -> Mark.Stroke 99 | extract stroke datum = 100 | let 101 | f = 102 | Maybe.andThen (\ch -> Channel.extract ch datum) 103 | in 104 | { stroke = f stroke.stroke 105 | , strokeOpacity = f stroke.strokeOpacity 106 | , strokeWidth = f stroke.strokeWidth 107 | , strokeDash = f stroke.strokeDash 108 | , strokeLineCap = Nothing 109 | , strokeLineJoin = Nothing 110 | } 111 | 112 | 113 | extractVector : Stroke data -> List data -> List Mark.Stroke 114 | extractVector stroke data = 115 | let 116 | f getter = 117 | getter stroke 118 | |> Maybe.map (\ch -> Channel.extractVector ch data) 119 | |> Maybe.withDefault [] 120 | 121 | strokes = 122 | f .stroke 123 | 124 | opacities = 125 | f .strokeOpacity 126 | 127 | widths = 128 | f .strokeWidth 129 | 130 | -- List(Maybe(List Float)) 131 | dashes = 132 | f .strokeDash 133 | in 134 | extractVectorHelper [] strokes opacities widths dashes 135 | 136 | 137 | extractVectorHelper : 138 | List Mark.Stroke 139 | -> List (Maybe Color) 140 | -> List (Maybe Float) 141 | -> List (Maybe Float) 142 | -> List (Maybe Mark.StrokeDash) 143 | -> List Mark.Stroke 144 | extractVectorHelper accu strokes opacities widths dashes = 145 | case ( strokes, opacities, widths, dashes ) of 146 | ( [], [], [], [] ) -> 147 | List.reverse accu 148 | 149 | _ -> 150 | let 151 | ( nextStroke, restStroke ) = 152 | headTail strokes 153 | 154 | ( nextOpacity, restOpacity ) = 155 | headTail opacities 156 | 157 | ( nextWidth, restWidth ) = 158 | headTail widths 159 | 160 | ( nextDash, restDash ) = 161 | headTail dashes 162 | 163 | stroke = 164 | Mark.Stroke nextStroke nextOpacity nextWidth Nothing nextDash Nothing 165 | in 166 | extractVectorHelper (stroke :: accu) 167 | restStroke 168 | restOpacity 169 | restWidth 170 | restDash 171 | 172 | 173 | headTail : List (Maybe a) -> ( Maybe a, List b ) 174 | headTail xs = 175 | case xs of 176 | [] -> 177 | ( Nothing, [] ) 178 | 179 | x :: xs -> 180 | ( x, [] ) 181 | 182 | 183 | summarize : Stroke data -> List data -> Mark.Stroke 184 | summarize stroke data = 185 | let 186 | f = 187 | Maybe.andThen (\ch -> Channel.summarize ch data) 188 | in 189 | { stroke = f stroke.stroke 190 | , strokeOpacity = f stroke.strokeOpacity 191 | , strokeWidth = f stroke.strokeWidth 192 | , strokeLineCap = Nothing 193 | , strokeDash = f stroke.strokeDash 194 | , strokeLineJoin = Nothing 195 | } 196 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Symbol.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Symbol 2 | exposing 3 | ( Symbol 4 | , symbol 5 | , point 6 | , arrow 7 | , cross 8 | , square 9 | , diamond 10 | , triangleUp 11 | , triangleDown 12 | , triangleLeft 13 | , triangleRight 14 | , shape 15 | , scenegraph 16 | , legends 17 | ) 18 | 19 | import Facet.Internal.Channel as Channel exposing (ShapeChannel, FloatChannel, PositionalChannel, TextChannel) 20 | import Facet.Internal.Encoding.Fill as Fill exposing (Fill) 21 | import Facet.Internal.Encoding.Stroke as Stroke exposing (Stroke) 22 | import Facet.Internal.Field as Field 23 | import Facet.Internal.Legend exposing (LegendSpec) 24 | import Facet.Maybe.Extra as Maybe 25 | import Facet.List.Extra as List 26 | import Facet.Scale as Scale exposing (Scale) 27 | import Facet.Scenegraph as Scenegraph exposing (Scenegraph) 28 | import Facet.Scenegraph.Cursor as Cursor exposing (Cursor) 29 | import Facet.Scenegraph.Fill as Scenegraph 30 | import Facet.Scenegraph.Stroke as Scenegraph 31 | import Facet.Scenegraph.Shape as Scenegraph 32 | import Facet.Scenegraph.Mark as Mark 33 | import Facet.Theme as Theme 34 | import Path.LowLevel exposing (SubPath) 35 | 36 | 37 | {-| Plotting symbols, including circles, squares and other shapes 38 | -} 39 | type alias Symbol data xdomain ydomain = 40 | { shape : Maybe (ShapeChannel data) 41 | , size : Maybe (FloatChannel data) 42 | , angle : Maybe (FloatChannel data) 43 | , x : PositionalChannel data xdomain 44 | , y : PositionalChannel data ydomain 45 | , fill : Maybe (Fill data) 46 | , stroke : Maybe (Stroke data) 47 | , cursor : Maybe Cursor 48 | , href : Maybe String 49 | , tooltip : Maybe (TextChannel data) 50 | } 51 | 52 | 53 | 54 | -- Helpers --------------------------------------------------------------------- 55 | 56 | 57 | {-| Construct a `Symbol` encoding providing an explicit `ShapeChannel`. 58 | -} 59 | symbol : 60 | ShapeChannel data 61 | -> PositionalChannel data xdomain 62 | -> PositionalChannel data ydomain 63 | -> Symbol data xdomain ydomain 64 | symbol shape x y = 65 | Symbol 66 | (Just shape) 67 | Nothing 68 | Nothing 69 | x 70 | y 71 | Nothing 72 | Nothing 73 | Nothing 74 | Nothing 75 | Nothing 76 | 77 | 78 | {-| Construct a `Symbol` encoding defaulting the `shape` to whatever is set 79 | in the theme applied 80 | -} 81 | point : 82 | PositionalChannel data xdomain 83 | -> PositionalChannel data ydomain 84 | -> Symbol data xdomain ydomain 85 | point x y = 86 | Symbol 87 | Nothing 88 | Nothing 89 | Nothing 90 | x 91 | y 92 | Nothing 93 | Nothing 94 | Nothing 95 | Nothing 96 | Nothing 97 | 98 | 99 | arrow : 100 | PositionalChannel data xdomain 101 | -> PositionalChannel data ydomain 102 | -> Symbol data xdomain ydomain 103 | arrow x y = 104 | symbolHelper x y Scenegraph.Arrow 105 | 106 | 107 | cross : 108 | PositionalChannel data xdomain 109 | -> PositionalChannel data ydomain 110 | -> Symbol data xdomain ydomain 111 | cross x y = 112 | symbolHelper x y Scenegraph.Cross 113 | 114 | 115 | square : 116 | PositionalChannel data xdomain 117 | -> PositionalChannel data ydomain 118 | -> Symbol data xdomain ydomain 119 | square x y = 120 | symbolHelper x y Scenegraph.Square 121 | 122 | 123 | diamond : 124 | PositionalChannel data xdomain 125 | -> PositionalChannel data ydomain 126 | -> Symbol data xdomain ydomain 127 | diamond x y = 128 | symbolHelper x y Scenegraph.Diamond 129 | 130 | 131 | triangleUp : 132 | PositionalChannel data xdomain 133 | -> PositionalChannel data ydomain 134 | -> Symbol data xdomain ydomain 135 | triangleUp x y = 136 | symbolHelper x y Scenegraph.TriangleUp 137 | 138 | 139 | triangleDown : 140 | PositionalChannel data xdomain 141 | -> PositionalChannel data ydomain 142 | -> Symbol data xdomain ydomain 143 | triangleDown x y = 144 | symbolHelper x y Scenegraph.TriangleDown 145 | 146 | 147 | triangleLeft : 148 | PositionalChannel data xdomain 149 | -> PositionalChannel data ydomain 150 | -> Symbol data xdomain ydomain 151 | triangleLeft x y = 152 | symbolHelper x y Scenegraph.TriangleLeft 153 | 154 | 155 | triangleRight : 156 | PositionalChannel data xdomain 157 | -> PositionalChannel data ydomain 158 | -> Symbol data xdomain ydomain 159 | triangleRight x y = 160 | symbolHelper x y Scenegraph.TriangleRight 161 | 162 | 163 | shape : 164 | PositionalChannel data xdomain 165 | -> PositionalChannel data ydomain 166 | -> SubPath 167 | -> Symbol data xdomain ydomain 168 | shape x y subpath = 169 | symbolHelper x y <| Scenegraph.Custom subpath 170 | 171 | 172 | symbolHelper : 173 | PositionalChannel data xdomain 174 | -> PositionalChannel data ydomain 175 | -> Scenegraph.Shape 176 | -> Symbol data xdomain ydomain 177 | symbolHelper x y shape = 178 | symbol 179 | (Channel.shape toString (Scale.constant shape) (Field.constant 0)) 180 | x 181 | y 182 | 183 | 184 | 185 | -- LEGENDS --------------------------------------------------------------------- 186 | 187 | 188 | legends : Int -> Symbol data xdomain ydomain -> LegendSpec 189 | legends ticks encoding = 190 | { fillColor = encoding.fill |> Maybe.andThen .fill |> Maybe.andThen (Channel.legend ticks) 191 | , fillOpacity = encoding.fill |> Maybe.andThen .fillOpacity |> Maybe.andThen (Channel.legend ticks) 192 | , strokeColor = encoding.stroke |> Maybe.andThen .stroke |> Maybe.andThen (Channel.legend ticks) 193 | , strokeOpacity = encoding.stroke |> Maybe.andThen .strokeOpacity |> Maybe.andThen (Channel.legend ticks) 194 | , strokeWidth = encoding.stroke |> Maybe.andThen .strokeWidth |> Maybe.andThen (Channel.legend ticks) 195 | , strokeDash = encoding.stroke |> Maybe.andThen .strokeDash |> Maybe.andThen (Channel.legend ticks) 196 | , angle = encoding.angle |> Maybe.andThen (Channel.legend ticks) 197 | , shape = encoding.shape |> Maybe.andThen (Channel.legend ticks) 198 | , cornerRadius = Nothing 199 | , size = encoding.size |> Maybe.andThen (Channel.legend ticks) 200 | , width = Nothing 201 | } 202 | 203 | 204 | 205 | -- SCENEGRAPH ------------------------------------------------------------------ 206 | 207 | 208 | {-| An intermediate representation of an encoding which allows for all 209 | fields to possibly be `Nothing` 210 | -} 211 | type alias SymbolInternal = 212 | { shape : Maybe Scenegraph.Shape 213 | , size : Maybe Float 214 | , angle : Maybe Float 215 | , x : Maybe Float 216 | , y : Maybe Float 217 | , fill : Maybe Scenegraph.Fill 218 | , stroke : Maybe Scenegraph.Stroke 219 | , cursor : Maybe Cursor 220 | , href : Maybe String 221 | , tooltip : Maybe String 222 | } 223 | 224 | 225 | {-| Combine `RectInternal`s with a preference for `mark2` (in practice, 226 | for each field in the two `RectInternal`s, only one _can_ have a `Just` 227 | value so the order of arguments does not matter.) 228 | -} 229 | combineIntermediate : SymbolInternal -> SymbolInternal -> SymbolInternal 230 | combineIntermediate mark1 mark2 = 231 | { x = mark2.x |> Maybe.orElse mark1.x 232 | , y = Maybe.orElse mark1.y mark2.y 233 | , shape = Maybe.orElse mark1.shape mark2.shape 234 | , size = Maybe.orElse mark1.size mark2.size 235 | , angle = Maybe.orElse mark1.angle mark2.angle 236 | , fill = Maybe.orElse mark1.fill mark2.fill 237 | , stroke = Maybe.orElse mark1.stroke mark2.stroke 238 | , cursor = Maybe.orElse mark1.cursor mark2.cursor 239 | , href = Maybe.orElse mark1.href mark2.href 240 | , tooltip = Maybe.orElse mark1.tooltip mark2.tooltip 241 | } 242 | 243 | 244 | {-| Combine an `SymbolInternal` with a theme to populate any un-encoded fields. 245 | -} 246 | combineWithThemeDefaults : Theme.Symbol -> SymbolInternal -> Maybe Mark.Symbol 247 | combineWithThemeDefaults theme markInternal = 248 | Maybe.map2 249 | (\x y -> 250 | { x = x 251 | , y = y 252 | , shape = Maybe.withDefault theme.shape markInternal.shape 253 | , size = Maybe.withDefault theme.size markInternal.size 254 | , angle = Maybe.withDefault theme.angle markInternal.angle 255 | , fill = Maybe.withDefault theme.fill markInternal.fill 256 | , stroke = Maybe.withDefault theme.stroke markInternal.stroke 257 | , cursor = Maybe.withDefault theme.cursor markInternal.cursor 258 | , href = markInternal.href 259 | , tooltip = markInternal.tooltip 260 | } 261 | ) 262 | markInternal.x 263 | markInternal.y 264 | 265 | 266 | {-| Test for an aggregate field in one of the channels 267 | -} 268 | containsAggregate : Symbol data xdomain ydomain -> Bool 269 | containsAggregate encoding = 270 | Maybe.maybe False Channel.isAggregate encoding.shape 271 | || Maybe.maybe False Channel.isAggregate encoding.size 272 | || Maybe.maybe False Channel.isAggregate encoding.angle 273 | || Channel.isAggregate encoding.x 274 | || Channel.isAggregate encoding.y 275 | || Maybe.maybe False Fill.containsAggregate encoding.fill 276 | || Maybe.maybe False Stroke.containsAggregate encoding.stroke 277 | || Maybe.maybe False Channel.isAggregate encoding.tooltip 278 | 279 | 280 | containsVector : Symbol data xdomain ydomain -> Bool 281 | containsVector encoding = 282 | Maybe.maybe False Channel.isVector encoding.shape 283 | || Maybe.maybe False Channel.isVector encoding.size 284 | || Maybe.maybe False Channel.isVector encoding.angle 285 | || Channel.isVector encoding.x 286 | || Channel.isVector encoding.y 287 | || Maybe.maybe False Fill.containsVector encoding.fill 288 | || Maybe.maybe False Stroke.containsVector encoding.stroke 289 | || Maybe.maybe False Channel.isVector encoding.tooltip 290 | 291 | 292 | scenegraph : 293 | Theme.Symbol 294 | -> Scale xdomain Float 295 | -> Scale ydomain Float 296 | -> List data 297 | -> Symbol data xdomain ydomain 298 | -> Scenegraph 299 | scenegraph theme xScale yScale data encoding = 300 | if containsAggregate encoding then 301 | data 302 | |> List.groupBy (compareAt encoding) 303 | (extract xScale yScale encoding) 304 | |> List.filterMap 305 | (\( mark, data ) -> 306 | let 307 | vector = 308 | extractVector xScale yScale encoding data 309 | 310 | agg = 311 | summarize xScale yScale encoding data 312 | in 313 | mark 314 | |> combineIntermediate vector 315 | |> combineIntermediate agg 316 | |> combineWithThemeDefaults theme 317 | ) 318 | |> Scenegraph.Symbol 319 | else if containsVector encoding then 320 | data 321 | |> List.groupBy (compareAt encoding) 322 | (extract xScale yScale encoding) 323 | |> List.filterMap 324 | (\( mark, data ) -> 325 | let 326 | vector = 327 | extractVector xScale yScale encoding data 328 | in 329 | mark 330 | |> combineIntermediate vector 331 | |> combineWithThemeDefaults theme 332 | ) 333 | |> Scenegraph.Symbol 334 | else 335 | List.filterMap 336 | (extract xScale yScale encoding 337 | >> combineWithThemeDefaults theme 338 | ) 339 | data 340 | |> Scenegraph.Symbol 341 | 342 | 343 | 344 | -- 345 | -- 346 | -- APPLY AGGREGATE, VECTOR AND SCALAR FIELDS ----------------------------------- 347 | 348 | 349 | {-| Apply each scalar channel of an encoding to a data point. 350 | Where a channel is not a scalar, the corresponding field in the returned 351 | `SymbolInternal` is `Nothing`. 352 | -} 353 | extract : 354 | Scale xdomain Float 355 | -> Scale ydomain Float 356 | -> Symbol data xdomain ydomain 357 | -> data 358 | -> SymbolInternal 359 | extract xScale yScale encoding datum = 360 | let 361 | extractMaybe getter = 362 | Maybe.andThen (\channel -> Channel.extract channel datum) <| 363 | getter encoding 364 | 365 | positionX = 366 | Field.extract encoding.x.field datum 367 | |> Maybe.andThen (Scale.scale xScale) 368 | 369 | positionY = 370 | Field.extract encoding.y.field datum 371 | |> Maybe.andThen (Scale.scale yScale) 372 | in 373 | { x = positionX 374 | , y = positionY 375 | , size = extractMaybe .size 376 | , angle = extractMaybe .angle 377 | , shape = extractMaybe .shape 378 | , fill = Maybe.map (\fill -> Fill.extract fill datum) encoding.fill 379 | , stroke = Maybe.map (\stroke -> Stroke.extract stroke datum) encoding.stroke 380 | , cursor = encoding.cursor 381 | , href = encoding.href 382 | , tooltip = extractMaybe .tooltip 383 | } 384 | 385 | 386 | {-| Apply each vector channel of an encoding to a data point. 387 | Where a channel is not a vector, the corresponding field in the returned 388 | `SymbolInternal` is `Nothing`. 389 | -} 390 | extractVector : 391 | Scale xdomain Float 392 | -> Scale ydomain Float 393 | -> Symbol data xdomain ydomain 394 | -> List data 395 | -> SymbolInternal 396 | extractVector xScale yScale encoding data = 397 | let 398 | extractVectorMaybe getter = 399 | getter encoding 400 | |> Maybe.andThen 401 | (\channel -> 402 | Channel.extractVector channel data 403 | |> List.head 404 | |> Maybe.join 405 | ) 406 | 407 | positionX = 408 | Field.extractVector encoding.x.field data 409 | |> List.head 410 | |> Maybe.join 411 | |> Maybe.andThen 412 | (Scale.scale xScale) 413 | 414 | positionY = 415 | Field.extractVector encoding.y.field data 416 | |> List.head 417 | |> Maybe.join 418 | |> Maybe.andThen 419 | (Scale.scale yScale) 420 | 421 | fill = 422 | Maybe.andThen 423 | (\fill -> Fill.extractVector fill data |> List.head) 424 | encoding.fill 425 | 426 | stroke = 427 | Maybe.andThen 428 | (\stroke -> Stroke.extractVector stroke data |> List.head) 429 | encoding.stroke 430 | in 431 | { x = positionX 432 | , y = positionY 433 | , size = extractVectorMaybe .size 434 | , angle = extractVectorMaybe .angle 435 | , shape = extractVectorMaybe .shape 436 | , fill = fill 437 | , stroke = stroke 438 | , cursor = encoding.cursor 439 | , href = encoding.href 440 | , tooltip = extractVectorMaybe .tooltip 441 | } 442 | 443 | 444 | {-| Apply each aggregate channel of an encoding to a data point. 445 | Where a channel is not an aggregate, the corresponding field in the returned 446 | `SymbolInternal` is `Nothing`. 447 | -} 448 | summarize : 449 | Scale xdomain Float 450 | -> Scale ydomain Float 451 | -> Symbol data xdomain ydomain 452 | -> List data 453 | -> SymbolInternal 454 | summarize xScale yScale encoding data = 455 | let 456 | summarizeMaybe getter = 457 | getter encoding 458 | |> Maybe.andThen (\channel -> Channel.summarize channel data) 459 | 460 | positionX = 461 | Field.summarize encoding.x.field data 462 | |> Maybe.andThen (Scale.scale xScale) 463 | 464 | positionY = 465 | Field.summarize encoding.y.field data 466 | |> Maybe.andThen (Scale.scale yScale) 467 | in 468 | { x = positionX 469 | , y = positionY 470 | , size = summarizeMaybe .size 471 | , angle = summarizeMaybe .angle 472 | , shape = summarizeMaybe .shape 473 | , fill = Maybe.map (\fill -> Fill.summarize fill data) encoding.fill 474 | , stroke = Maybe.map (\stroke -> Stroke.summarize stroke data) encoding.stroke 475 | , cursor = encoding.cursor 476 | , href = encoding.href 477 | , tooltip = summarizeMaybe .tooltip 478 | } 479 | 480 | 481 | {-| Structural comparison of `Stroke` encoding domain evaluated at two data poinst 482 | -} 483 | compareAt : Symbol data xdomain ydomain -> data -> data -> Order 484 | compareAt encoding d1 d2 = 485 | let 486 | comp getter = 487 | Channel.compareMaybeAt (getter encoding) d1 d2 488 | in 489 | case Channel.compareAt encoding.x d1 d2 of 490 | EQ -> 491 | case Channel.compareAt encoding.y d1 d2 of 492 | EQ -> 493 | case comp .shape of 494 | EQ -> 495 | case comp .size of 496 | EQ -> 497 | case comp .angle of 498 | EQ -> 499 | case Maybe.maybe EQ (\fill -> Fill.compareAt fill d1 d2) encoding.fill of 500 | EQ -> 501 | case Maybe.maybe EQ (\stroke -> Stroke.compareAt stroke d1 d2) encoding.stroke of 502 | EQ -> 503 | comp .tooltip 504 | 505 | otherwise -> 506 | otherwise 507 | 508 | otherwise -> 509 | otherwise 510 | 511 | otherwise -> 512 | otherwise 513 | 514 | otherwise -> 515 | otherwise 516 | 517 | otherwise -> 518 | otherwise 519 | 520 | otherwise -> 521 | otherwise 522 | 523 | otherwise -> 524 | otherwise 525 | 526 | 527 | equalAt : Symbol data xdomain ydomain -> data -> data -> Bool 528 | equalAt encoding d1 d2 = 529 | let 530 | eq getter = 531 | Channel.equalMaybeAt (getter encoding) d1 d2 532 | in 533 | Channel.equalAt encoding.x d1 d2 534 | && Channel.equalAt encoding.y d1 d2 535 | && Maybe.maybe True (\fill -> Fill.equalAt fill d1 d2) encoding.fill 536 | && Maybe.maybe True (\stroke -> Stroke.equalAt stroke d1 d2) encoding.stroke 537 | && eq .shape 538 | && eq .size 539 | && eq .angle 540 | && eq .tooltip 541 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Text.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Text exposing (Text, text, scenegraph, legends) 2 | 3 | import Facet.Internal.Channel as Channel exposing (ShapeChannel, FloatChannel, PositionalChannel, TextChannel) 4 | import Facet.Internal.Encoding.Fill as Fill exposing (Fill) 5 | import Facet.Internal.Encoding.Stroke as Stroke exposing (Stroke) 6 | import Facet.Internal.Field as Field 7 | import Facet.Internal.Legend exposing (LegendSpec) 8 | import Facet.Maybe.Extra as Maybe 9 | import Facet.List.Extra as List 10 | import Facet.Scale as Scale exposing (Scale) 11 | import Facet.Scenegraph as Scenegraph exposing (Scenegraph) 12 | import Facet.Scenegraph.Cursor as Cursor exposing (Cursor) 13 | import Facet.Scenegraph.Fill as Scenegraph 14 | import Facet.Scenegraph.Stroke as Scenegraph 15 | import Facet.Scenegraph.Mark as Mark 16 | import Facet.Theme as Theme 17 | 18 | 19 | type alias Text data xdomain ydomain = 20 | { text : TextChannel data 21 | , dx : Maybe Float 22 | , dy : Maybe Float 23 | , size : Maybe (FloatChannel data) 24 | , angle : Maybe (FloatChannel data) 25 | , x : PositionalChannel data xdomain 26 | , y : PositionalChannel data ydomain 27 | , fill : Maybe (Fill data) 28 | , stroke : Maybe (Stroke data) 29 | , cursor : Maybe Cursor 30 | , href : Maybe String 31 | , tooltip : Maybe (TextChannel data) 32 | } 33 | 34 | 35 | 36 | -- Helpers --------------------------------------------------------------------- 37 | 38 | 39 | text : 40 | TextChannel data 41 | -> PositionalChannel data xdomain 42 | -> PositionalChannel data ydomain 43 | -> Text data xdomain ydomain 44 | text text x y = 45 | Text 46 | text 47 | Nothing 48 | Nothing 49 | Nothing 50 | Nothing 51 | x 52 | y 53 | Nothing 54 | Nothing 55 | Nothing 56 | Nothing 57 | Nothing 58 | 59 | 60 | 61 | -- LEGENDS --------------------------------------------------------------------- 62 | 63 | 64 | legends : Int -> Text data xdomain ydomain -> LegendSpec 65 | legends ticks encoding = 66 | { fillColor = encoding.fill |> Maybe.andThen .fill |> Maybe.andThen (Channel.legend ticks) 67 | , fillOpacity = encoding.fill |> Maybe.andThen .fillOpacity |> Maybe.andThen (Channel.legend ticks) 68 | , strokeColor = encoding.stroke |> Maybe.andThen .stroke |> Maybe.andThen (Channel.legend ticks) 69 | , strokeOpacity = encoding.stroke |> Maybe.andThen .strokeOpacity |> Maybe.andThen (Channel.legend ticks) 70 | , strokeWidth = encoding.stroke |> Maybe.andThen .strokeWidth |> Maybe.andThen (Channel.legend ticks) 71 | , strokeDash = encoding.stroke |> Maybe.andThen .strokeDash |> Maybe.andThen (Channel.legend ticks) 72 | , angle = encoding.angle |> Maybe.andThen (Channel.legend ticks) 73 | , shape = Nothing 74 | , cornerRadius = Nothing 75 | , size = encoding.size |> Maybe.andThen (Channel.legend ticks) 76 | , width = Nothing 77 | } 78 | 79 | 80 | 81 | -- SCENEGRAPH ------------------------------------------------------------------ 82 | 83 | 84 | {-| An intermediate representation of an encoding which allows for all 85 | fields to possibly be `Nothing` 86 | -} 87 | type alias TextInternal = 88 | { text : Maybe String 89 | , size : Maybe Float 90 | , angle : Maybe Float 91 | , x : Maybe Float 92 | , y : Maybe Float 93 | , fill : Maybe Scenegraph.Fill 94 | , stroke : Maybe Scenegraph.Stroke 95 | , cursor : Maybe Cursor 96 | , href : Maybe String 97 | , tooltip : Maybe String 98 | } 99 | 100 | 101 | {-| Combine `RectInternal`s with a preference for `mark2` (in practice, 102 | for each field in the two `RectInternal`s, only one _can_ have a `Just` 103 | value so the order of arguments does not matter.) 104 | -} 105 | combineIntermediate : TextInternal -> TextInternal -> TextInternal 106 | combineIntermediate mark1 mark2 = 107 | { x = mark2.x |> Maybe.orElse mark1.x 108 | , y = Maybe.orElse mark1.y mark2.y 109 | , text = Maybe.orElse mark1.text mark2.text 110 | , size = Maybe.orElse mark1.size mark2.size 111 | , angle = Maybe.orElse mark1.angle mark2.angle 112 | , fill = Maybe.orElse mark1.fill mark2.fill 113 | , stroke = Maybe.orElse mark1.stroke mark2.stroke 114 | , cursor = Maybe.orElse mark1.cursor mark2.cursor 115 | , href = Maybe.orElse mark1.href mark2.href 116 | , tooltip = Maybe.orElse mark1.tooltip mark2.tooltip 117 | } 118 | 119 | 120 | {-| Combine an `SymbolInternal` with a theme to populate any un-encoded fields. 121 | -} 122 | combineWithThemeDefaults : Theme.Text -> Maybe Float -> Maybe Float -> TextInternal -> Maybe Mark.Text 123 | combineWithThemeDefaults theme dx dy markInternal = 124 | Maybe.map3 125 | (\text x y -> 126 | let 127 | themeFont = 128 | theme.font 129 | 130 | font = 131 | Maybe.map (\size -> { themeFont | fontSize = size }) markInternal.size 132 | |> Maybe.withDefault themeFont 133 | in 134 | { text = text 135 | , align = theme.align 136 | , baseline = theme.baseline 137 | , direction = theme.direction 138 | , dx = Maybe.withDefault 0 dx 139 | , dy = Maybe.withDefault 0 dy 140 | , elipsis = theme.elipsis 141 | , font = font 142 | , angle = Maybe.withDefault theme.angle markInternal.angle 143 | , radius = 0 144 | , theta = 0 145 | , x = x 146 | , y = y 147 | , fill = Maybe.withDefault theme.fill markInternal.fill 148 | , stroke = Maybe.withDefault theme.stroke markInternal.stroke 149 | , cursor = Maybe.withDefault theme.cursor markInternal.cursor 150 | , href = markInternal.href 151 | , tooltip = markInternal.tooltip 152 | } 153 | ) 154 | markInternal.text 155 | markInternal.x 156 | markInternal.y 157 | 158 | 159 | {-| Test for an aggregate field in one of the channels 160 | -} 161 | containsAggregate : Text data xdomain ydomain -> Bool 162 | containsAggregate encoding = 163 | Channel.isAggregate encoding.text 164 | || Maybe.maybe False Channel.isAggregate encoding.size 165 | || Maybe.maybe False Channel.isAggregate encoding.angle 166 | || Channel.isAggregate encoding.x 167 | || Channel.isAggregate encoding.y 168 | || Maybe.maybe False Fill.containsAggregate encoding.fill 169 | || Maybe.maybe False Stroke.containsAggregate encoding.stroke 170 | || Maybe.maybe False Channel.isAggregate encoding.tooltip 171 | 172 | 173 | containsVector : Text data xdomain ydomain -> Bool 174 | containsVector encoding = 175 | Channel.isVector encoding.text 176 | || Maybe.maybe False Channel.isVector encoding.size 177 | || Maybe.maybe False Channel.isVector encoding.angle 178 | || Channel.isVector encoding.x 179 | || Channel.isVector encoding.y 180 | || Maybe.maybe False Fill.containsVector encoding.fill 181 | || Maybe.maybe False Stroke.containsVector encoding.stroke 182 | || Maybe.maybe False Channel.isVector encoding.tooltip 183 | 184 | 185 | scenegraph : 186 | Theme.Text 187 | -> Scale xdomain Float 188 | -> Scale ydomain Float 189 | -> List data 190 | -> Text data xdomain ydomain 191 | -> Scenegraph 192 | scenegraph theme xScale yScale data encoding = 193 | if containsAggregate encoding then 194 | data 195 | |> List.groupBy (compareAt encoding) 196 | (extract xScale yScale encoding) 197 | |> List.filterMap 198 | (\( mark, data ) -> 199 | let 200 | vector = 201 | extractVector xScale yScale encoding data 202 | 203 | agg = 204 | summarize xScale yScale encoding data 205 | in 206 | mark 207 | |> combineIntermediate vector 208 | |> combineIntermediate agg 209 | |> combineWithThemeDefaults theme encoding.dx encoding.dy 210 | ) 211 | |> Scenegraph.Text 212 | else if containsVector encoding then 213 | data 214 | |> List.groupBy (compareAt encoding) 215 | (extract xScale yScale encoding) 216 | |> List.filterMap 217 | (\( mark, data ) -> 218 | let 219 | vector = 220 | extractVector xScale yScale encoding data 221 | in 222 | mark 223 | |> combineIntermediate vector 224 | |> combineWithThemeDefaults theme encoding.dx encoding.dy 225 | ) 226 | |> Scenegraph.Text 227 | else 228 | List.filterMap 229 | (extract xScale yScale encoding 230 | >> combineWithThemeDefaults theme encoding.dx encoding.dy 231 | ) 232 | data 233 | |> Scenegraph.Text 234 | 235 | 236 | 237 | -- 238 | -- 239 | -- APPLY AGGREGATE, VECTOR AND SCALAR FIELDS ----------------------------------- 240 | 241 | 242 | {-| Apply each scalar channel of an encoding to a data point. 243 | Where a channel is not a scalar, the corresponding field in the returned 244 | `SymbolInternal` is `Nothing`. 245 | -} 246 | extract : 247 | Scale xdomain Float 248 | -> Scale ydomain Float 249 | -> Text data xdomain ydomain 250 | -> data 251 | -> TextInternal 252 | extract xScale yScale encoding datum = 253 | let 254 | extractMaybe getter = 255 | Maybe.andThen (\channel -> Channel.extract channel datum) <| 256 | getter encoding 257 | 258 | positionX = 259 | Field.extract encoding.x.field datum 260 | |> Maybe.andThen (Scale.scale xScale) 261 | 262 | positionY = 263 | Field.extract encoding.y.field datum 264 | |> Maybe.andThen (Scale.scale yScale) 265 | in 266 | { x = positionX 267 | , y = positionY 268 | , size = extractMaybe .size 269 | , angle = extractMaybe .angle 270 | , text = Channel.extract encoding.text datum 271 | , fill = Maybe.map (\fill -> Fill.extract fill datum) encoding.fill 272 | , stroke = Maybe.map (\stroke -> Stroke.extract stroke datum) encoding.stroke 273 | , cursor = encoding.cursor 274 | , href = encoding.href 275 | , tooltip = extractMaybe .tooltip 276 | } 277 | 278 | 279 | {-| Apply each vector channel of an encoding to a data point. 280 | Where a channel is not a vector, the corresponding field in the returned 281 | `SymbolInternal` is `Nothing`. 282 | -} 283 | extractVector : 284 | Scale xdomain Float 285 | -> Scale ydomain Float 286 | -> Text data xdomain ydomain 287 | -> List data 288 | -> TextInternal 289 | extractVector xScale yScale encoding data = 290 | let 291 | extractVectorMaybe getter = 292 | getter encoding 293 | |> Maybe.andThen 294 | (\channel -> 295 | Channel.extractVector channel data 296 | |> List.head 297 | |> Maybe.join 298 | ) 299 | 300 | extractVector channel = 301 | Channel.extractVector (channel encoding) data 302 | |> List.head 303 | |> Maybe.join 304 | 305 | positionX = 306 | Field.extractVector encoding.x.field data 307 | |> List.head 308 | |> Maybe.join 309 | |> Maybe.andThen 310 | (Scale.scale xScale) 311 | 312 | positionY = 313 | Field.extractVector encoding.y.field data 314 | |> List.head 315 | |> Maybe.join 316 | |> Maybe.andThen 317 | (Scale.scale yScale) 318 | 319 | fill = 320 | Maybe.andThen 321 | (\fill -> Fill.extractVector fill data |> List.head) 322 | encoding.fill 323 | 324 | stroke = 325 | Maybe.andThen 326 | (\stroke -> Stroke.extractVector stroke data |> List.head) 327 | encoding.stroke 328 | in 329 | { x = positionX 330 | , y = positionY 331 | , size = extractVectorMaybe .size 332 | , angle = extractVectorMaybe .angle 333 | , text = extractVector .text 334 | , fill = fill 335 | , stroke = stroke 336 | , cursor = encoding.cursor 337 | , href = encoding.href 338 | , tooltip = extractVectorMaybe .tooltip 339 | } 340 | 341 | 342 | {-| Apply each aggregate channel of an encoding to a data point. 343 | Where a channel is not an aggregate, the corresponding field in the returned 344 | `SymbolInternal` is `Nothing`. 345 | -} 346 | summarize : 347 | Scale xdomain Float 348 | -> Scale ydomain Float 349 | -> Text data xdomain ydomain 350 | -> List data 351 | -> TextInternal 352 | summarize xScale yScale encoding data = 353 | let 354 | summarizeMaybe getter = 355 | getter encoding 356 | |> Maybe.andThen (\channel -> Channel.summarize channel data) 357 | 358 | positionX = 359 | Field.summarize encoding.x.field data 360 | |> Maybe.andThen (Scale.scale xScale) 361 | 362 | positionY = 363 | Field.summarize encoding.y.field data 364 | |> Maybe.andThen (Scale.scale yScale) 365 | in 366 | { x = positionX 367 | , y = positionY 368 | , size = summarizeMaybe .size 369 | , angle = summarizeMaybe .angle 370 | , text = Channel.summarize encoding.text data 371 | , fill = Maybe.map (\fill -> Fill.summarize fill data) encoding.fill 372 | , stroke = Maybe.map (\stroke -> Stroke.summarize stroke data) encoding.stroke 373 | , cursor = encoding.cursor 374 | , href = encoding.href 375 | , tooltip = summarizeMaybe .tooltip 376 | } 377 | 378 | 379 | {-| Structural comparison of `Stroke` encoding domain evaluated at two data poinst 380 | -} 381 | compareAt : Text data xdomain ydomain -> data -> data -> Order 382 | compareAt encoding d1 d2 = 383 | let 384 | comp getter = 385 | Channel.compareMaybeAt (getter encoding) d1 d2 386 | in 387 | case Channel.compareAt encoding.x d1 d2 of 388 | EQ -> 389 | case Channel.compareAt encoding.y d1 d2 of 390 | EQ -> 391 | case Channel.compareAt encoding.text d1 d2 of 392 | EQ -> 393 | case comp .size of 394 | EQ -> 395 | case comp .angle of 396 | EQ -> 397 | case Maybe.maybe EQ (\fill -> Fill.compareAt fill d1 d2) encoding.fill of 398 | EQ -> 399 | case Maybe.maybe EQ (\stroke -> Stroke.compareAt stroke d1 d2) encoding.stroke of 400 | EQ -> 401 | comp .tooltip 402 | 403 | otherwise -> 404 | otherwise 405 | 406 | otherwise -> 407 | otherwise 408 | 409 | otherwise -> 410 | otherwise 411 | 412 | otherwise -> 413 | otherwise 414 | 415 | otherwise -> 416 | otherwise 417 | 418 | otherwise -> 419 | otherwise 420 | 421 | otherwise -> 422 | otherwise 423 | 424 | 425 | equalAt : Text data xdomain ydomain -> data -> data -> Bool 426 | equalAt encoding d1 d2 = 427 | let 428 | eq getter = 429 | Channel.equalMaybeAt (getter encoding) d1 d2 430 | in 431 | Channel.equalAt encoding.x d1 d2 432 | && Channel.equalAt encoding.y d1 d2 433 | && Channel.equalAt encoding.text d1 d2 434 | && Maybe.maybe True (\fill -> Fill.equalAt fill d1 d2) encoding.fill 435 | && Maybe.maybe True (\stroke -> Stroke.equalAt stroke d1 d2) encoding.stroke 436 | && eq .size 437 | && eq .angle 438 | && eq .tooltip 439 | -------------------------------------------------------------------------------- /src/Facet/Internal/Encoding/Trail.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Encoding.Trail exposing (Trail, trail, scenegraph, legends) 2 | 3 | import Facet.Internal.Encoding.Fill as Fill exposing (Fill) 4 | import Facet.Internal.Channel as Channel exposing (TextChannel, FloatChannel, PositionalChannel) 5 | import Facet.Internal.Field as Field 6 | import Facet.Internal.Legend exposing (LegendSpec) 7 | import Facet.List.Extra as List 8 | import Facet.Maybe.Extra as Maybe 9 | import Facet.Scale as Scale exposing (Scale) 10 | import Facet.Scenegraph as Scenegraph exposing (Scenegraph) 11 | import Facet.Scenegraph.Cursor as Cursor exposing (Cursor) 12 | import Facet.Scenegraph.Fill as Scenegraph 13 | import Facet.Scenegraph.Mark as Mark exposing (Behaviour) 14 | import Facet.Theme as Theme 15 | 16 | 17 | type alias Trail data xdomain ydomain = 18 | { x : PositionalChannel data xdomain 19 | , y : PositionalChannel data ydomain 20 | , width : FloatChannel data 21 | , behaviour : Behaviour 22 | , fill : Maybe (Fill data) 23 | , cursor : Maybe Cursor 24 | , href : Maybe String 25 | , tooltip : Maybe (TextChannel data) 26 | } 27 | 28 | 29 | 30 | -- Helpers --------------------------------------------------------------------- 31 | 32 | 33 | trail : 34 | PositionalChannel data xdomain 35 | -> PositionalChannel data ydomain 36 | -> FloatChannel data 37 | -> Behaviour 38 | -> Trail data xdomain ydomain 39 | trail x y width behaviour = 40 | Trail x y width behaviour Nothing Nothing Nothing Nothing 41 | 42 | 43 | 44 | -- LEGENDS --------------------------------------------------------------------- 45 | 46 | 47 | legends : Int -> Trail data xdomain ydomain -> LegendSpec 48 | legends ticks encoding = 49 | { fillColor = encoding.fill |> Maybe.andThen .fill |> Maybe.andThen (Channel.legend ticks) 50 | , fillOpacity = encoding.fill |> Maybe.andThen .fillOpacity |> Maybe.andThen (Channel.legend ticks) 51 | , strokeColor = Nothing 52 | , strokeOpacity = Nothing 53 | , strokeWidth = Nothing 54 | , strokeDash = Nothing 55 | , angle = Nothing 56 | , shape = Nothing 57 | , cornerRadius = Nothing 58 | , width = encoding.width |> Channel.legend ticks 59 | , size = Nothing 60 | } 61 | 62 | 63 | 64 | -- SCENEGRAPH ------------------------------------------------------------------ 65 | 66 | 67 | {-| An intermediate representation of an encoding which allows for all 68 | fields to possibly be `Nothing` 69 | -} 70 | type alias TrailInternal = 71 | { x : Maybe (List (Maybe Float)) 72 | , y : Maybe (List (Maybe Float)) 73 | , width : Maybe (List (Maybe Float)) 74 | , fill : Maybe Scenegraph.Fill 75 | , cursor : Maybe Cursor 76 | , href : Maybe String 77 | , tooltip : Maybe String 78 | } 79 | 80 | 81 | {-| Combine `LineInternal`s with a preference for `mark2` (in practice, 82 | for each field in the two `RectInternal`s, only one _can_ have a `Just` 83 | value so the order of arguments does not matter.) 84 | -} 85 | combineIntermediate : TrailInternal -> TrailInternal -> TrailInternal 86 | combineIntermediate mark1 mark2 = 87 | { x = Maybe.orElse mark1.x mark2.x 88 | , y = Maybe.orElse mark1.y mark2.y 89 | , width = Maybe.orElse mark1.width mark2.width 90 | , fill = Maybe.orElse mark1.fill mark2.fill 91 | , cursor = Maybe.orElse mark1.cursor mark2.cursor 92 | , href = Maybe.orElse mark1.href mark2.href 93 | , tooltip = Maybe.orElse mark1.tooltip mark2.tooltip 94 | } 95 | 96 | 97 | {-| Combine an `TrailInternal` with required fields from the encoding 98 | and a theme to populate any un-encoded fields. 99 | -} 100 | combineWithThemeDefaults : Theme.Trail -> Behaviour -> TrailInternal -> Maybe Mark.Trail 101 | combineWithThemeDefaults theme behaviour markInternal = 102 | Maybe.map3 103 | (\x y width -> 104 | { x = x 105 | , y = y 106 | , width = width 107 | , behaviour = behaviour 108 | , fill = Maybe.withDefault theme.fill markInternal.fill 109 | , cursor = Maybe.withDefault theme.cursor markInternal.cursor 110 | , href = markInternal.href 111 | , tooltip = markInternal.tooltip 112 | } 113 | ) 114 | markInternal.x 115 | markInternal.y 116 | markInternal.width 117 | 118 | 119 | {-| Test for an aggregate field in one of the channels 120 | -} 121 | containsAggregate : Trail data xdomain ydomain -> Bool 122 | containsAggregate encoding = 123 | Channel.isAggregate encoding.x 124 | || Channel.isAggregate encoding.y 125 | || Channel.isAggregate encoding.width 126 | || Maybe.maybe False Fill.containsAggregate encoding.fill 127 | || Maybe.maybe False Channel.isAggregate encoding.tooltip 128 | 129 | 130 | containsVector : Trail data xdomain ydomain -> Bool 131 | containsVector encoding = 132 | Channel.isVector encoding.x 133 | || Channel.isVector encoding.y 134 | || Channel.isVector encoding.width 135 | || Maybe.maybe False Fill.containsVector encoding.fill 136 | || Maybe.maybe False Channel.isVector encoding.tooltip 137 | 138 | 139 | 140 | {- Generate a scenegraph for the encoding -} 141 | 142 | 143 | scenegraph : 144 | Theme.Trail 145 | -> Scale xdomain Float 146 | -> Scale ydomain Float 147 | -> List data 148 | -> Trail data xdomain ydomain 149 | -> Scenegraph 150 | scenegraph theme xScale yScale data encoding = 151 | if containsAggregate encoding then 152 | data 153 | |> List.groupBy (compareAt encoding) 154 | (extract xScale yScale encoding) 155 | |> List.filterMap 156 | (\( mark, data ) -> 157 | let 158 | vector = 159 | extractVector xScale yScale encoding data 160 | 161 | agg = 162 | summarize xScale yScale encoding data 163 | in 164 | mark 165 | |> combineIntermediate vector 166 | |> combineIntermediate agg 167 | |> combineWithThemeDefaults theme encoding.behaviour 168 | ) 169 | |> Scenegraph.Trail 170 | else if containsVector encoding then 171 | data 172 | |> List.groupBy (compareAt encoding) 173 | (extract xScale yScale encoding) 174 | |> List.filterMap 175 | (\( mark, data ) -> 176 | let 177 | vector = 178 | extractVector xScale yScale encoding data 179 | in 180 | mark 181 | |> combineIntermediate vector 182 | |> combineWithThemeDefaults theme encoding.behaviour 183 | ) 184 | |> Scenegraph.Trail 185 | else 186 | List.filterMap 187 | (extract xScale yScale encoding 188 | >> combineWithThemeDefaults theme encoding.behaviour 189 | ) 190 | data 191 | |> Scenegraph.Trail 192 | 193 | 194 | 195 | -- APPLY AGGREGATE, VECTOR AND SCALAR FIELDS ----------------------------------- 196 | 197 | 198 | {-| Apply each scalar channel of an encoding to a data point. 199 | Where a channel is not a scalar, the corresponding field in the returned 200 | `TrailInternal` is `Nothing`. 201 | -} 202 | extract : 203 | Scale xdomain Float 204 | -> Scale ydomain Float 205 | -> Trail data xdomain ydomain 206 | -> data 207 | -> TrailInternal 208 | extract xScale yScale encoding datum = 209 | let 210 | extractMaybe getter = 211 | getter encoding 212 | |> Maybe.andThen (\channel -> Channel.extract channel datum) 213 | 214 | positionX = 215 | Field.extract encoding.x.field datum 216 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 217 | 218 | positionY = 219 | Field.extract encoding.y.field datum 220 | |> Maybe.andThen (Scale.scale yScale >> Maybe.map (Just >> List.singleton)) 221 | 222 | width = 223 | Channel.extract encoding.width datum 224 | |> Maybe.map (Just >> List.singleton) 225 | 226 | fill = 227 | Maybe.map (\fill -> Fill.extract fill datum) encoding.fill 228 | in 229 | { x = positionX 230 | , y = positionY 231 | , width = width 232 | , fill = fill 233 | , cursor = encoding.cursor 234 | , href = encoding.href 235 | , tooltip = extractMaybe .tooltip 236 | } 237 | 238 | 239 | {-| Apply each vector channel of an encoding to a data point. 240 | Where a channel is not a vector, the corresponding field in the returned 241 | `TrailInternal` is `Nothing`. 242 | -} 243 | extractVector : 244 | Scale xdomain Float 245 | -> Scale ydomain Float 246 | -> Trail data xdomain ydomain 247 | -> List data 248 | -> TrailInternal 249 | extractVector xScale yScale encoding data = 250 | let 251 | xs = 252 | case Field.extractVector encoding.x.field data |> List.map (Maybe.andThen (Scale.scale xScale)) of 253 | [] -> 254 | Nothing 255 | 256 | xs -> 257 | Just xs 258 | 259 | ys = 260 | case Field.extractVector encoding.y.field data |> List.map (Maybe.andThen (Scale.scale yScale)) of 261 | [] -> 262 | Nothing 263 | 264 | ys -> 265 | Just ys 266 | 267 | ws = 268 | case Channel.extractVector encoding.width data of 269 | [] -> 270 | Nothing 271 | 272 | ws -> 273 | Just ws 274 | 275 | fill = 276 | Maybe.andThen 277 | (\fill -> Fill.extractVector fill data |> List.head) 278 | encoding.fill 279 | 280 | tooltip = 281 | encoding.tooltip 282 | |> Maybe.andThen 283 | (\channel -> 284 | Channel.extractVector channel data 285 | |> List.head 286 | >> Maybe.join 287 | ) 288 | in 289 | { x = xs 290 | , y = ys 291 | , width = ws 292 | , fill = fill 293 | , cursor = encoding.cursor 294 | , href = encoding.href 295 | , tooltip = tooltip 296 | } 297 | 298 | 299 | {-| Apply each aggregate channel of an encoding to a data point. 300 | Where a channel is not an aggregate, the corresponding field in the returned 301 | `TrailInternal` is `Nothing`. 302 | -} 303 | summarize : 304 | Scale xdomain Float 305 | -> Scale ydomain Float 306 | -> Trail data xdomain ydomain 307 | -> List data 308 | -> TrailInternal 309 | summarize xScale yScale encoding data = 310 | let 311 | summarizeMaybe getter = 312 | getter encoding 313 | |> Maybe.andThen (\channel -> Channel.summarize channel data) 314 | 315 | x = 316 | Field.summarize encoding.x.field data 317 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 318 | 319 | y = 320 | Field.summarize encoding.x.field data 321 | |> Maybe.andThen (Scale.scale xScale >> Maybe.map (Just >> List.singleton)) 322 | 323 | width = 324 | Channel.summarize encoding.width data 325 | |> Maybe.map (Just >> List.singleton) 326 | 327 | fill = 328 | Maybe.map (\fill -> Fill.summarize fill data) encoding.fill 329 | in 330 | { x = x 331 | , y = y 332 | , width = width 333 | , fill = fill 334 | , cursor = encoding.cursor 335 | , href = encoding.href 336 | , tooltip = summarizeMaybe .tooltip 337 | } 338 | 339 | 340 | {-| Structural comparison of encoding domain evaluated at two data poinst 341 | -} 342 | compareAt : Trail data xdomain ydomain -> data -> data -> Order 343 | compareAt encoding d1 d2 = 344 | let 345 | comp getter = 346 | Channel.compareMaybeAt (getter encoding) d1 d2 347 | in 348 | case Channel.compareAt encoding.x d1 d2 of 349 | EQ -> 350 | case Channel.compareAt encoding.y d1 d2 of 351 | EQ -> 352 | case Channel.compareAt encoding.width d1 d2 of 353 | EQ -> 354 | case Maybe.maybe EQ (\fill -> Fill.compareAt fill d1 d2) encoding.fill of 355 | EQ -> 356 | comp .tooltip 357 | 358 | otherwise -> 359 | otherwise 360 | 361 | otherwise -> 362 | otherwise 363 | 364 | otherwise -> 365 | otherwise 366 | 367 | otherwise -> 368 | otherwise 369 | 370 | 371 | equalAt : Trail data xdomain ydomain -> data -> data -> Bool 372 | equalAt encoding d1 d2 = 373 | let 374 | eq getter = 375 | Channel.equalMaybeAt (getter encoding) d1 d2 376 | in 377 | Channel.equalAt encoding.x d1 d2 378 | && Channel.equalAt encoding.y d1 d2 379 | && Channel.equalAt encoding.width d1 d2 380 | && Maybe.maybe True (\fill -> Fill.equalAt fill d1 d2) encoding.fill 381 | && eq .tooltip 382 | -------------------------------------------------------------------------------- /src/Facet/Internal/Field.elm: -------------------------------------------------------------------------------- 1 | module Facet.Internal.Field 2 | exposing 3 | ( Field 4 | , constant 5 | , scalar 6 | , maybeScalar 7 | , aggregate 8 | , maybeAggregate 9 | , vector 10 | , maybeVector 11 | , extract 12 | , extractVector 13 | , summarize 14 | , fieldName 15 | , isAggregate 16 | , isConstant 17 | , isScalar 18 | , isVector 19 | , equalAt 20 | , compareAt 21 | ) 22 | 23 | 24 | type Field data domain 25 | = Constant domain 26 | | Scalar (Maybe String) (data -> Maybe domain) 27 | | Vector (Maybe String) (List data -> List (Maybe domain)) 28 | | Aggregate (Maybe String) (List data -> Maybe domain) 29 | 30 | 31 | equalAt : Field data domain -> data -> data -> Bool 32 | equalAt field d1 d2 = 33 | Maybe.withDefault True <| 34 | Maybe.map2 (==) 35 | (extract field d1) 36 | (extract field d2) 37 | 38 | 39 | compareAt : (domain -> domain -> Order) -> Field data domain -> data -> data -> Order 40 | compareAt compareWith field d1 d2 = 41 | Maybe.withDefault EQ <| 42 | Maybe.map2 (compareWith) 43 | (extract field d1) 44 | (extract field d2) 45 | 46 | 47 | scalar : Maybe String -> (data -> domain) -> Field data domain 48 | scalar name extract = 49 | Scalar name <| Just << extract 50 | 51 | 52 | maybeScalar : Maybe String -> (data -> Maybe domain) -> Field data domain 53 | maybeScalar name extract = 54 | Scalar name extract 55 | 56 | 57 | maybeVector : Maybe String -> (List data -> List (Maybe domain)) -> Field data domain 58 | maybeVector name extract = 59 | Vector name extract 60 | 61 | 62 | vector : Maybe String -> (List data -> List domain) -> Field data domain 63 | vector name extract = 64 | Vector name (List.map Just << extract) 65 | 66 | 67 | maybeAggregate : Maybe String -> (List data -> Maybe domain) -> Field data domain 68 | maybeAggregate name summarize = 69 | Aggregate name summarize 70 | 71 | 72 | aggregate : Maybe String -> (List data -> domain) -> Field data domain 73 | aggregate name summarize = 74 | Aggregate name <| Just << summarize 75 | 76 | 77 | constant : domain -> Field data domain 78 | constant value = 79 | Constant value 80 | 81 | 82 | extract : Field data domain -> data -> Maybe domain 83 | extract field datum = 84 | case field of 85 | Constant value -> 86 | Just value 87 | 88 | Scalar _ extract -> 89 | extract datum 90 | 91 | _ -> 92 | Nothing 93 | 94 | 95 | extractVector : Field data domain -> List data -> List (Maybe domain) 96 | extractVector field data = 97 | case field of 98 | Constant value -> 99 | [ Just value ] 100 | 101 | Vector _ extract -> 102 | extract data 103 | 104 | _ -> 105 | [] 106 | 107 | 108 | summarize : Field data domain -> List data -> Maybe domain 109 | summarize field data = 110 | case field of 111 | Constant value -> 112 | Just value 113 | 114 | Aggregate _ summarize -> 115 | summarize data 116 | 117 | _ -> 118 | Nothing 119 | 120 | 121 | fieldName : Field data domain -> Maybe String 122 | fieldName field = 123 | case field of 124 | Constant _ -> 125 | Nothing 126 | 127 | Scalar name _ -> 128 | name 129 | 130 | Vector name _ -> 131 | name 132 | 133 | Aggregate name _ -> 134 | name 135 | 136 | 137 | isScalar : Field data domain -> Bool 138 | isScalar field = 139 | case field of 140 | Scalar _ _ -> 141 | True 142 | 143 | _ -> 144 | False 145 | 146 | 147 | isVector : Field data domain -> Bool 148 | isVector field = 149 | case field of 150 | Vector _ _ -> 151 | True 152 | 153 | _ -> 154 | False 155 | 156 | 157 | isConstant : Field data domain -> Bool 158 | isConstant field = 159 | case field of 160 | Constant _ -> 161 | True 162 | 163 | _ -> 164 | False 165 | 166 | 167 | isAggregate : Field data domain -> Bool 168 | isAggregate field = 169 | case field of 170 | Aggregate _ _ -> 171 | True 172 | 173 | _ -> 174 | False 175 | -------------------------------------------------------------------------------- /src/Facet/List/Extra.elm: -------------------------------------------------------------------------------- 1 | module Facet.List.Extra exposing (find, groupBy, unique, chop, splitAt, fromMaybe, group, zipCycle, transpose) 2 | 3 | 4 | transpose : List (List a) -> List (List a) 5 | transpose ll = 6 | case ll of 7 | [] -> 8 | [] 9 | 10 | [] :: xss -> 11 | transpose xss 12 | 13 | (x :: xs) :: xss -> 14 | let 15 | heads = 16 | List.filterMap List.head xss 17 | 18 | tails = 19 | List.filterMap List.tail xss 20 | in 21 | (x :: heads) :: transpose (xs :: tails) 22 | 23 | 24 | fromMaybe : Maybe a -> List a 25 | fromMaybe maybeVal = 26 | case maybeVal of 27 | Just x -> 28 | [ x ] 29 | 30 | _ -> 31 | [] 32 | 33 | 34 | chop : Maybe Int -> List a -> List (List a) 35 | chop maybeLimit list = 36 | case maybeLimit of 37 | Nothing -> 38 | [ list ] 39 | 40 | Just limit -> 41 | case splitAt limit list of 42 | ( _, [] ) -> 43 | [ list ] 44 | 45 | ( xs, ys ) -> 46 | xs :: (chop maybeLimit ys) 47 | 48 | 49 | splitAt : Int -> List a -> ( List a, List a ) 50 | splitAt n xs = 51 | if n <= 0 then 52 | ( xs, [] ) 53 | else 54 | splitAtHelper n [] xs 55 | 56 | 57 | splitAtHelper : Int -> List a -> List a -> ( List a, List a ) 58 | splitAtHelper n accu xs = 59 | if n <= 0 then 60 | ( List.reverse accu, xs ) 61 | else 62 | case xs of 63 | next :: rest -> 64 | splitAtHelper (n - 1) (next :: accu) rest 65 | 66 | _ -> 67 | ( List.reverse accu, [] ) 68 | 69 | 70 | zipCycle : List a -> List b -> List ( a, b ) 71 | zipCycle xs ys = 72 | case ( xs, ys ) of 73 | ( [], _ ) -> 74 | [] 75 | 76 | ( _, [] ) -> 77 | [] 78 | 79 | _ -> 80 | zipCycleHelper xs ys [] xs ys 81 | 82 | 83 | zipCycleHelper : 84 | List a 85 | -> List b 86 | -> List ( a, b ) 87 | -> List a 88 | -> List b 89 | -> List ( a, b ) 90 | zipCycleHelper xs ys accu cx cy = 91 | case ( cx, cy ) of 92 | ( nextX :: restX, nextY :: restY ) -> 93 | zipCycleHelper xs ys (( nextX, nextY ) :: accu) restX restY 94 | 95 | ( _ :: _, _ ) -> 96 | zipCycleHelper xs ys accu cx ys 97 | 98 | ( _, _ :: _ ) -> 99 | zipCycleHelper xs ys accu xs cy 100 | 101 | _ -> 102 | List.reverse accu 103 | 104 | 105 | find : (a -> Bool) -> List a -> Maybe a 106 | find predicate list = 107 | case list of 108 | [] -> 109 | Nothing 110 | 111 | next :: rest -> 112 | if predicate next then 113 | Just next 114 | else 115 | find predicate rest 116 | 117 | 118 | groupWhile : (a -> a -> Bool) -> List a -> List (List a) 119 | groupWhile eq xs_ = 120 | case xs_ of 121 | [] -> 122 | [] 123 | 124 | x :: xs -> 125 | let 126 | ( ys, zs ) = 127 | span (eq x) xs 128 | in 129 | (x :: ys) :: groupWhile eq zs 130 | 131 | 132 | unique : (a -> a -> Order) -> List a -> List a 133 | unique compareWith list = 134 | list |> List.sortWith compareWith |> groupWhile (==) |> List.filterMap List.head 135 | 136 | 137 | group : (a -> a -> Order) -> List a -> List (List a) 138 | group compareWith list = 139 | list 140 | |> List.sortWith compareWith 141 | |> groupWhile (\a b -> compareWith a b == EQ) 142 | 143 | 144 | groupBy : (a -> a -> Order) -> (a -> b) -> List a -> List ( b, List a ) 145 | groupBy compareWith projection list = 146 | list 147 | |> List.sortWith compareWith 148 | |> groupWhile (\x y -> projection x == projection y) 149 | |> List.filterMap (\xs -> List.head xs |> Maybe.map (\x -> ( projection x, xs ))) 150 | 151 | 152 | span : (a -> Bool) -> List a -> ( List a, List a ) 153 | span p xs = 154 | ( takeWhile p xs, dropWhile p xs ) 155 | 156 | 157 | takeWhile : (a -> Bool) -> List a -> List a 158 | takeWhile predicate = 159 | let 160 | takeWhileMemo memo list = 161 | case list of 162 | [] -> 163 | List.reverse memo 164 | 165 | x :: xs -> 166 | if (predicate x) then 167 | takeWhileMemo (x :: memo) xs 168 | else 169 | List.reverse memo 170 | in 171 | takeWhileMemo [] 172 | 173 | 174 | dropWhile : (a -> Bool) -> List a -> List a 175 | dropWhile predicate list = 176 | case list of 177 | [] -> 178 | [] 179 | 180 | x :: xs -> 181 | if (predicate x) then 182 | dropWhile predicate xs 183 | else 184 | list 185 | -------------------------------------------------------------------------------- /src/Facet/Maybe/Extra.elm: -------------------------------------------------------------------------------- 1 | module Facet.Maybe.Extra exposing (orElse, maybe, maybeLazy, isNothing, isJust, join, toList) 2 | 3 | 4 | toList : Maybe a -> List a 5 | toList maybeVal = 6 | case maybeVal of 7 | Just x -> 8 | [ x ] 9 | 10 | _ -> 11 | [] 12 | 13 | 14 | join : Maybe (Maybe a) -> Maybe a 15 | join maybeMaybeVal = 16 | case maybeMaybeVal of 17 | Just (Just val) -> 18 | Just val 19 | 20 | _ -> 21 | Nothing 22 | 23 | 24 | isJust : Maybe a -> Bool 25 | isJust maybeVal = 26 | case maybeVal of 27 | Just _ -> 28 | True 29 | 30 | _ -> 31 | False 32 | 33 | 34 | isNothing : Maybe a -> Bool 35 | isNothing maybeVal = 36 | case maybeVal of 37 | Nothing -> 38 | True 39 | 40 | _ -> 41 | False 42 | 43 | 44 | orElse : Maybe a -> Maybe a -> Maybe a 45 | orElse ifNothing maybeVal = 46 | case maybeVal of 47 | Just _ -> 48 | maybeVal 49 | 50 | _ -> 51 | ifNothing 52 | 53 | 54 | maybe : a -> (b -> a) -> Maybe b -> a 55 | maybe ifNothing withJust maybeVal = 56 | case maybeVal of 57 | Just x -> 58 | withJust x 59 | 60 | _ -> 61 | ifNothing 62 | 63 | 64 | maybeLazy : (() -> a) -> (b -> a) -> Maybe b -> a 65 | maybeLazy ifNothing withJust maybeVal = 66 | case maybeVal of 67 | Just x -> 68 | withJust x 69 | 70 | _ -> 71 | ifNothing () 72 | -------------------------------------------------------------------------------- /src/Facet/Scale.elm: -------------------------------------------------------------------------------- 1 | module Facet.Scale 2 | exposing 3 | ( Scale 4 | , scale 5 | , unscale 6 | , ticks 7 | , legend 8 | , OutsideDomain 9 | , clamp 10 | , discard 11 | , allow 12 | , linear 13 | , linearNice 14 | , log10 15 | , naturalLog 16 | , sqrt 17 | , rgb 18 | , rgbBasis 19 | , hsl 20 | , tableau10 21 | , category10 22 | , band 23 | , customBand 24 | , continuous 25 | , sequential 26 | , ordinal 27 | , customOrdinal 28 | , constant 29 | ) 30 | 31 | {-| 32 | @docs Scale,scale, unscale, ticks, legend 33 | 34 | @docs constant, sequential, continuous, ordinal, customOrdinal 35 | 36 | @docs OutsideDomain, clamp, discard, allow 37 | 38 | @docs linear, linearNice , log10, naturalLog, sqrt 39 | 40 | @docs band, customBand 41 | 42 | @docs rgb, rgbBasis, hsl 43 | 44 | @docs tableau10, category10 45 | -} 46 | 47 | import Color exposing (Color) 48 | import Scale.Config as Config 49 | import Scale.Continuous as Continuous exposing (Continuous) 50 | import Scale.Continuous.Linear as Linear 51 | import Scale.Continuous.Log as Log 52 | import Scale.Continuous.Power as Power 53 | import Scale.Ordinal as Ordinal exposing (Ordinal) 54 | import Scale.Ordinal.Comparable as Ordinal 55 | import Scale.Ordinal.Custom as Ordinal 56 | import Scale.Ordinal.Band as Band 57 | import Scale.Sequential as Sequential exposing (Sequential) 58 | import Scale.Sequential.RGB as RGB 59 | import Scale.Sequential.RGBBasis as RGBBasis 60 | import Scale.Sequential.HSL as HSL 61 | 62 | 63 | {-| A scale provides a means of mapping between values of type _domain_ to 64 | values of type _range_. 65 | -} 66 | type Scale domain range 67 | = Continuous (Continuous domain range) 68 | | Ordinal (Ordinal domain range) 69 | | Sequential (Sequential domain range) 70 | | Constant range 71 | 72 | 73 | {-| -} 74 | scale : Scale domain range -> domain -> Maybe range 75 | scale scale datum = 76 | case scale of 77 | Continuous continuous -> 78 | Continuous.scale continuous datum 79 | 80 | Ordinal ordinal -> 81 | Ordinal.scale ordinal datum 82 | 83 | Sequential sequential -> 84 | Sequential.scale sequential datum 85 | 86 | Constant range -> 87 | Just range 88 | 89 | 90 | {-| -} 91 | unscale : Scale domain Float -> Float -> Maybe domain 92 | unscale scale datum = 93 | case scale of 94 | Continuous continuous -> 95 | Continuous.unscale continuous datum 96 | 97 | _ -> 98 | Nothing 99 | 100 | 101 | {-| -} 102 | ticks : Scale domain range -> Int -> List domain 103 | ticks scale count = 104 | case scale of 105 | Continuous continuous -> 106 | Continuous.ticks continuous count 107 | 108 | Ordinal ordinal -> 109 | Ordinal.ticks ordinal count 110 | 111 | Sequential sequential -> 112 | Sequential.ticks sequential count 113 | 114 | Constant _ -> 115 | [] 116 | 117 | 118 | {-| -} 119 | legend : Scale domain range -> Int -> List ( domain, range ) 120 | legend forScale count = 121 | ticks forScale count 122 | |> List.filterMap 123 | (\x -> 124 | scale forScale x 125 | |> Maybe.map (\y -> ( x, y )) 126 | ) 127 | 128 | 129 | 130 | -- constructors ---------------------------------------------------------------- 131 | 132 | 133 | {-| -} 134 | constant : a -> Scale domain a 135 | constant range = 136 | Constant range 137 | 138 | 139 | {-| -} 140 | continuous : 141 | { a 142 | | compareDomain : domain -> domain -> Order 143 | , deinterpolateDomain : ( domain, domain ) -> domain -> Float 144 | , domain : ( domain, domain ) 145 | , interpolateRange : ( range, range ) -> Float -> range 146 | , outsideDomain : OutsideDomain 147 | , range : ( range, range ) 148 | , reinterpolateDomain : ( domain, domain ) -> range -> domain 149 | , ticks : ( domain, domain ) -> Int -> List domain 150 | } 151 | -> Scale domain range 152 | continuous = 153 | Continuous << Continuous.continuous 154 | 155 | 156 | {-| -} 157 | ordinal : 158 | Ordinal.Comparable a comparableDomain range 159 | -> Scale comparableDomain range 160 | ordinal = 161 | Ordinal << Ordinal.comparable 162 | 163 | 164 | {-| -} 165 | customOrdinal : Ordinal.Custom a domain range -> Scale domain range 166 | customOrdinal = 167 | Ordinal << Ordinal.custom 168 | 169 | 170 | {-| -} 171 | sequential : 172 | { a 173 | | compareDomain : domain -> domain -> Order 174 | , deinterpolateDomain : ( domain, domain ) -> domain -> Float 175 | , domain : ( domain, domain ) 176 | , interpolator : Float -> range 177 | , outsideDomain : OutsideDomain 178 | , ticks : ( domain, domain ) -> Int -> List domain 179 | } 180 | -> Scale domain range 181 | sequential = 182 | Sequential << Sequential.sequential 183 | 184 | 185 | 186 | -- continuous scales ----------------------------------------------------------- 187 | 188 | 189 | {-| -} 190 | linear : { a | domain : ( Float, Float ), range : ( Float, Float ) } -> Scale Float Float 191 | linear { domain, range } = 192 | { domain = domain, range = range, outsideDomain = discard } 193 | -- |> Linear.niceDomain 10 194 | |> 195 | Linear.linear 196 | |> Continuous 197 | 198 | 199 | {-| -} 200 | linearNice : { a | numTicks : Int, domain : ( Float, Float ), range : ( Float, Float ) } -> Scale Float Float 201 | linearNice { numTicks, domain, range } = 202 | { domain = domain, range = range, outsideDomain = discard } 203 | |> Linear.niceDomain numTicks 204 | |> Linear.linear 205 | |> Continuous 206 | 207 | 208 | {-| -} 209 | log10 : { a | domain : ( Float, Float ), range : ( Float, Float ) } -> Scale Float Float 210 | log10 { domain, range } = 211 | { base = 10.0, domain = domain, range = range, outsideDomain = discard } 212 | |> Log.log 213 | |> Continuous 214 | 215 | 216 | {-| -} 217 | naturalLog : { a | domain : ( Float, Float ), range : ( Float, Float ) } -> Scale Float Float 218 | naturalLog { domain, range } = 219 | { base = Basics.e, domain = domain, range = range, outsideDomain = discard } 220 | |> Log.niceDomain 221 | |> Log.log 222 | |> Continuous 223 | 224 | 225 | {-| -} 226 | sqrt : { a | domain : ( Float, Float ), range : ( Float, Float ) } -> Scale Float Float 227 | sqrt { domain, range } = 228 | { exponent = 0.5, domain = domain, range = range, outsideDomain = discard } 229 | |> Power.niceDomain 10 230 | |> Power.power 231 | |> Continuous 232 | 233 | 234 | 235 | -- Band scales ----------------------------------------------------------------- 236 | 237 | 238 | {-| -} 239 | customBand : { a | domain : List domain, range : ( Float, Float ) } -> Scale domain Float 240 | customBand { domain, range } = 241 | { domain = domain, range = range, config = Nothing } 242 | |> Band.customBand 243 | |> Ordinal 244 | 245 | 246 | {-| -} 247 | band : { a | domain : List comparable, range : ( Float, Float ) } -> Scale comparable Float 248 | band { domain, range } = 249 | { domain = domain, range = range, config = Nothing } 250 | |> Band.band 251 | |> Ordinal 252 | 253 | 254 | 255 | -- Sequential color scales ----------------------------------------------------- 256 | 257 | 258 | {-| -} 259 | rgb : 260 | { a | domain : ( Float, Float ), range : ( Color, Color ) } 261 | -> Scale Float Color 262 | rgb { domain, range } = 263 | { domain = domain, range = range, gamma = Nothing, outsideDomain = discard } 264 | |> RGB.rgb 265 | |> Sequential 266 | 267 | 268 | {-| -} 269 | rgbBasis : { a | closed : Bool, domain : ( Float, Float ), range : List Color } -> Scale Float Color 270 | rgbBasis { closed, domain, range } = 271 | { domain = domain, range = range, closed = closed, outsideDomain = discard } 272 | |> RGBBasis.rgbBasis 273 | |> Sequential 274 | 275 | 276 | {-| -} 277 | hsl : { a | long : Bool, domain : ( Float, Float ), range : ( Color, Color ) } -> Scale Float Color 278 | hsl { long, domain, range } = 279 | { domain = domain, range = range, long = long, outsideDomain = discard } 280 | |> HSL.hsl 281 | |> Sequential 282 | 283 | 284 | 285 | -- Ordinal color scales -------------------------------------------------------- 286 | 287 | 288 | {-| -} 289 | tableau10 : List comparable -> Scale comparable Color 290 | tableau10 domain = 291 | { domain = domain 292 | , range = tableau10Colors 293 | } 294 | |> Ordinal.comparable 295 | |> Ordinal 296 | 297 | 298 | tableau10Colors : List Color 299 | tableau10Colors = 300 | [ Color.rgb 76 120 168 301 | , Color.rgb 245 133 24 302 | , Color.rgb 228 87 86 303 | , Color.rgb 114 183 178 304 | , Color.rgb 84 162 75 305 | , Color.rgb 238 202 59 306 | , Color.rgb 178 121 162 307 | , Color.rgb 255 157 166 308 | , Color.rgb 157 117 93 309 | , Color.rgb 186 176 172 310 | ] 311 | 312 | 313 | {-| -} 314 | category10 : List comparable -> Scale comparable Color 315 | category10 domain = 316 | { domain = domain 317 | , range = category10Colors 318 | } 319 | |> Ordinal.comparable 320 | |> Ordinal 321 | 322 | 323 | category10Colors : List Color 324 | category10Colors = 325 | [ Color.rgb 31 119 180 326 | , Color.rgb 255 127 14 327 | , Color.rgb 44 160 44 328 | , Color.rgb 214 39 40 329 | , Color.rgb 148 103 189 330 | , Color.rgb 140 86 75 331 | , Color.rgb 227 119 194 332 | , Color.rgb 17 127 127 333 | , Color.rgb 188 189 34 334 | , Color.rgb 23 190 207 335 | ] 336 | 337 | 338 | 339 | -- RE-EXPORTS ------------------------------------------------------------------ 340 | 341 | 342 | {-| -} 343 | type alias OutsideDomain = 344 | Config.OutsideDomain 345 | 346 | 347 | {-| -} 348 | clamp : Config.OutsideDomain 349 | clamp = 350 | Config.Clamp 351 | 352 | 353 | {-| -} 354 | discard : Config.OutsideDomain 355 | discard = 356 | Config.Discard 357 | 358 | 359 | {-| -} 360 | allow : Config.OutsideDomain 361 | allow = 362 | Config.Allow 363 | --------------------------------------------------------------------------------