Skip to content

Implement CubeHelix sampling function #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 31, 2017

Conversation

safareli
Copy link
Contributor

@safareli safareli commented Aug 26, 2017

fix #30

Copy link
Contributor Author

@safareli safareli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not particularly happy with name ColorScaleBuilder, we might move it with corresponding functions in separate module too.

src/Color.purs Outdated
radians = pi / 180.0

cubehelixMix :: Number -> Mixer -- TODO fix alpha
cubehelixMix gama (HSLA ah' as' al' _) (HSLA bh' bs' bl' _) =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe add this as a comment in the code?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should be gamma instead of gama.

src/Color.purs Outdated
al = al'
bl = bl' - al

-- NOTE not sure why isNan check is needed so I have not ported it
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove this comments, as still not sure why it's needed 🗡

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's remove the NaN checks. I think it's only needed if the Color could possibly have NaN values internally..

src/Color.purs Outdated
-- NOTE not sure why isNan check is needed so I have not ported it
-- if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as;
-- if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah;
in \t ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be much better in performance, what's your thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly. Good idea.

@@ -53,19 +64,26 @@ stopColor :: ColorStop -> Color
stopColor (ColorStop c _) = c

-- | A color scale.
data ColorScale = ColorScale ColorSpace Color (List ColorStop) Color
data ColorScaleBuilder = ColorScaleBuilder Color (List ColorStop) Color
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exporting this is very useful so users could write their own helper functions, without waiting for their PR to be merged. Before I needed to define my own type for it see this

Also if we have separate type which does not contain ColorSpace then we can implement combineScale :: Number → Scale → Scale → Scale which i did in the link above. If the Scale contained colorSpace then we would have to discard one of them, which I think will not be a good thing to do.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about just naming this ColorStops or ColorStopList?

uniformScale mode b middle e = colorScale mode b stops e
uniformScale mode b middle e = ColorScale mode $ uniformScale' b middle e

uniformScale' :: forall f. Foldable f => Color -> f Color -> Color -> ColorScaleBuilder
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this ' functions might be useful when someone is working on ColorScaleBuilder and might not care about ColorSpace. Like I did here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Can we please add a docstring?

@safareli
Copy link
Contributor Author

Stack overflows on HSV -> HSL -> HSV, HSL -> HSV -> HSL test :/

@sharkdp
Copy link
Collaborator

sharkdp commented Aug 27, 2017

Thank you for working on this. Let me know when it's ready to review.

@safareli
Copy link
Contributor Author

It's ready for review

Copy link
Collaborator

@sharkdp sharkdp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this! There's a few things I'd like to discuss first, before merging this - please see the inline-comments.

src/Color.purs Outdated
where h' = if h == 360.0 then h else h `modPos` 360.0
s' = clamp 0.0 1.0 s
hsla h s l a = HSLA h s' l' a'
where s' = clamp 0.0 1.0 s
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like that we have to change this:

  • It forces us to insert clipHSLHue calls in multiple places (see below)
  • Worse, it forces us to remember that we have to call clipHSLHue when using hue values (in the future)

Why is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's needed for coubehelix mix of colors.
for example if we want gradient from volor eith hue 350 to color with hue 370.
without this change we will get gradient from 350 down to 10 where wi will have length of 340 in hue, not up to 370 where length will be 20. and the distinction between 370 and 10 might be useful for other sampling functions too

to not forget cliping we could use toHSLA and do not patern match on constructor, or sormthing else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can revert this part and see that coubhelix gradient will look like

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, okay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just pushed a refacto of this issue.
let me know if you like the approach, or if you can think of different approach to issue?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I rather prefer the previous approach, to be honest. With both hue and hueOriginal as fields, we have to keep track of always keeping both in sync. Sorry 😄

Thinking about this a bit more, I guess it's okay to allow for angles outside of [0°, 360°].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reverted most of the changes but made this change, so we are not forced to remember clipping Hue:

data Color = HSLA Hue Number Number Number

newtype Hue = UnclippedHue Number

clipHue :: Hue -> Number
clipHue (UnclippedHue x) = x `modPos` 360.0

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

src/Color.purs Outdated
@@ -500,10 +505,13 @@ interpolateAngle fraction a b = interpolate fraction shortest.from shortest.to
dist { from, to } = abs (to - from)
shortest = unsafePartial (fromJust (minimumBy (comparing dist) paths))


type Mixer = Color -> Color -> Number -> Color
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with the type synonym, but I'd like to add a short doc-string (.. a function that interpolates between two colors..).

src/Color.purs Outdated
@@ -538,6 +546,32 @@ mix Lab c1 c2 frac = lab
f = toLab c1
t = toLab c2

radians :: Radians
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this definition inside cubehelixMix, if it is not needed anywhere else.

src/Color.purs Outdated
radians = pi / 180.0

cubehelixMix :: Number -> Mixer -- TODO fix alpha
cubehelixMix gama (HSLA ah' as' al' _) (HSLA bh' bs' bl' _) =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe add this as a comment in the code?

src/Color.purs Outdated
radians :: Radians
radians = pi / 180.0

cubehelixMix :: Number -> Mixer -- TODO fix alpha
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment: I'd prefer mixCubehelix.

uniformScale mode b middle e = colorScale mode b stops e
uniformScale mode b middle e = ColorScale mode $ uniformScale' b middle e

uniformScale' :: forall f. Foldable f => Color -> f Color -> Color -> ColorScaleBuilder
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Can we please add a docstring?

sample :: Sampler ColorScale
sample (ColorScale mode scale) = mkSimpleSampler (mix mode) scale

type Sampler a = a -> Number -> Color
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO: this type synonym just adds mental overhead when reading the type definitions below.

-- | Get the color at a specific point on the color scale (number between 0 and
-- | 1). If the number is smaller than 0, the color at 0 is returned. If the
-- | number is larger than 1, the color at 1 is returned.
sample :: ColorScale -> Number -> Color
sample scale@(ColorScale mode b middle e) x
mkSimpleSampler mixer (ColorScaleBuilder b middle e) x
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a docstring between type declaration and function implementation?

colors scale = colors' (sample scale)

-- | Takes sampling function and returns A list of colors that is sampled from
-- | a color scale. The number of colors can be specified.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does colors' have to be a top-level function?


cssColorStops scale@(ColorScale _ b middle e) = cssColorStops csRGB
cssColorStopsSample :: Sampler ColorScaleBuilder -> Int -> ColorScaleBuilder -> String
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these for? Can you please add some comments/documentation?

mix HSL c1 c2 frac = hsla
(interpolateAngle frac f.h t.h)
(interpolateAngle frac f.h t.h) -- NOTE here we might benefit from using uncliped Hue?
Copy link
Contributor Author

@safareli safareli Aug 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see use of UnclippedHue here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea!


-- | Mix two colors by [Dave Green's `cubehelix'](http://www.mrao.cam.ac.uk/~dag/CUBEHELIX/) interpolation between them.
-- | Takes gamma correction value as an argument and retunrs Interpolator.
-- | For more details see: [d3-plugins/cubehelix](https://github.com/d3/d3-plugins/tree/40f8b3b91e67719f58408732d7ddae94cafa559a/cubehelix#interpolateCubehelix)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do links like this work in pursuit?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so.

@safareli
Copy link
Contributor Author

I have added comments to new functions, let me know if you spot some typos or would like different wording.

@sharkdp
Copy link
Collaborator

sharkdp commented Aug 28, 2017

I don't have any time today but I'll try to review it in full tomorrow. Thank you very much.

Do you know why the tests started failing now? Why did the stackoverflow only appear now?

@safareli
Copy link
Contributor Author

Do you know why the tests started failing now? Why did the stackoverflow only appear now?

Not sure, when I was adding the test which is now failing it was raising stackoverflow at first and because of that I used smaller numbers.

@safareli safareli changed the title implement CubeHelix staff Implement CubeHelix sampling function Aug 29, 2017
@safareli
Copy link
Contributor Author

I just found out that it's allowed to have hue be out of range [0,360]

https://www.w3.org/TR/css3-color/#hsl-color

HSL colors are encoding as a triple (hue, saturation, lightness). Hue is represented as an angle of the color circle (i.e. the rainbow represented in a circle). This angle is so typically measured in degrees that the unit is implicit in CSS; syntactically, only a is given. By definition red=0=360, and the other colors are spread around the circle, so green=120, blue=240, etc. As an angle, it implicitly wraps around such that -120=240 and 480=120. One way an implementation could normalize such an angle x to the range [0,360) (i.e. zero degrees, inclusive, to 360 degrees, exclusive) is to compute (((x mod 360) + 360) mod 360). Saturation and lightness are represented as percentages. 100% is full saturation, and 0% is a shade of gray. 0% lightness is black, 100% lightness is white, and 50% lightness is “normal”.

This makes me think that we should remove clipping from cssStringHSLA. what you think?

Besides cssStringHSLA we call clip in to{RGB,HSL,HSV}A.
Also There is no way for user to get unclipped Hue. I think we should add toHSLA' or similar, which returns { h :: Hue, s :: Number, l :: Number, a :: Number } and export Hue + clipHue. thoughts?

@safareli
Copy link
Contributor Author

I came to d3/d3-color here cubehelix is implemented as color space, not as just an interpolation.

If we try same thing I see it as creating Color.cobehelix which takes h s l a which converts CobeHelix color to HSLA to construct Color object. Then adding CobeHelix to Color.ColorSpace and we will add case for this colorspace in Color.mix, where Colors a b will be converted to CobeHelix mixed and then back to HSLA.

But I don't quite see how to implement, will need to dig into implementation
Will I do not get is what's the range of saturation, so I think I will stop on this until i hear back on d3/d3-color#34


here is a:

A two-dimensional cross section of the Cubehelix color space: from top to bottom, hue ranges from 0° to 360°; from left to right, lightness ranges from 1 to 0. Saturation is a constant 1.
Raw

if you try to look at that image with grayscale enabled with on our PC/mobile you will see that lightness is monotonic.

This is Color picker in CobeHelix space.

@sharkdp
Copy link
Collaborator

sharkdp commented Aug 29, 2017

Not sure, when I was adding the test which is now failing it was raising stackoverflow at first and because of that I used smaller numbers.

Thanks for fixing those

This makes me think that we should remove clipping from cssStringHSLA. what you think?

Yeah, good point. Thanks for the reference.

Besides cssStringHSLA we call clip in to{RGB,HSL,HSV}A.
Also There is no way for user to get unclipped Hue. I think we should add toHSLA' or similar, which returns { h :: Hue, s :: Number, l :: Number, a :: Number } and export Hue + clipHue. thoughts?

Maybe it's actually best if we return unclipped values everywhere. Do we need the clipped values at some point? 😄

But I don't quite see how to implement, will need to dig into implementation
Will I do not get is what's the range of saturation, so I think I will stop on this until i hear back on d3/d3-color#34

Cool. Thanks for looking into this. Sounds like a good option to me (adding it as a ColorSpace).

@safareli
Copy link
Contributor Author

Do we need the clipped values at some point?

We need to clip for toRGBA, on the other side for some applications clipped hue might be needed. So what you are suggesting is to just store unclipped hue as Number and expose clipHue :: Number -> Number which will be used only in toRGBAand in client code ? or what

About ColorSpace I don't yet understand how we can do that I have some open questions in the issue will take a look into it in future maybe.

So to merge this we just need to decide what should we don on clipping.

@sharkdp
Copy link
Collaborator

sharkdp commented Aug 31, 2017

We need to clip for toRGBA, on the other side for some applications clipped hue might be needed. So what you are suggesting is to just store unclipped hue as Number and expose clipHue :: Number -> Number which will be used only in toRGBAand in client code ? or what

I think it's perfectly fine the way you did it right now, thank you very much.

@sharkdp sharkdp merged commit bcf09c0 into purescript-contrib:master Aug 31, 2017
@sharkdp
Copy link
Collaborator

sharkdp commented Aug 31, 2017

@safareli

I have added cubehelix :: ColorScale. Thought you might find this useful.

I've also added the mixCubehelix function and the cubehelix colorscale to the interactive documentation: http://sharkdp.github.io/purescript-colors/

@safareli
Copy link
Contributor Author

nice!

@safareli
Copy link
Contributor Author

Let me know when you release new version :P

@sharkdp
Copy link
Collaborator

sharkdp commented Aug 31, 2017

Let me know when you release new version :P

Done. Thanks again!

@sharkdp
Copy link
Collaborator

sharkdp commented Aug 31, 2017

I've also added a small interactive cubehelix experiment at the bottom of the page:
http://sharkdp.github.io/purescript-colors/

(similar to https://www.mrao.cam.ac.uk/~dag/CUBEHELIX/cubetry.html)

@safareli
Copy link
Contributor Author

sweet 🍓

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

add 'cubehelix' colour scheme
2 participants