Skip to content
Permalink
Browse files

Adding Heatmap trace implementation (#148)

  • Loading branch information
eric-czech authored and alexarchambault committed Jan 3, 2020
1 parent f7dd66d commit 1d82e3b81e34216dc8ccb8fb344c94aca485641d
@@ -7,6 +7,7 @@ sealed abstract class Sequence extends Product with Serializable
object Sequence {
final case class Doubles(seq: Seq[Double]) extends Sequence
final case class NestedDoubles(seq: Seq[Seq[Double]]) extends Sequence
final case class NestedInts(seq: Seq[Seq[Int]]) extends Sequence
final case class Strings(seq: Seq[String]) extends Sequence
final case class DateTimes(seq: Seq[LocalDateTime]) extends Sequence

@@ -20,6 +21,8 @@ object Sequence {
Doubles(s.map(_.toDouble))
implicit def fromNestedDoubleSeq(s: Seq[Seq[Double]]): Sequence =
NestedDoubles(s)
implicit def fromNestedIntSeq(s: Seq[Seq[Int]]): Sequence =
NestedInts(s)
implicit def fromStringSeq(s: Seq[String]): Sequence =
Strings(s)
implicit def fromDateTimes(seq: Seq[LocalDateTime]): Sequence =
@@ -260,3 +260,34 @@ object Surface {
Option(opacity) .map(d => d: Double)
)
}

@data class Heatmap(
y: Option[Sequence],
x: Option[Sequence],
z: Option[Sequence],
autocolorscale: Option[Boolean],
colorscale: Option[ColorScale],
showscale: Option[Boolean],
name: Option[String]
) extends Trace

object Heatmap {
def apply(
y: Sequence = null,
x: Sequence = null,
z: Sequence = null,
autocolorscale: JBoolean = null,
colorscale: ColorScale = null,
showscale: JBoolean = null,
name: String = null
): Heatmap =
Heatmap(
Option(y),
Option(x),
Option(z),
Option(autocolorscale).map(b => b: Boolean),
Option(colorscale),
Option(showscale).map(b => b: Boolean),
Option(name)
)
}
@@ -0,0 +1,9 @@
package plotly.element
import dataclass.data

sealed abstract class ColorScale extends Product with Serializable

object ColorScale {
@data class CustomScale(values: Seq[(Double, Color)]) extends ColorScale
@data class NamedScale(name: String) extends ColorScale
}
@@ -5,4 +5,6 @@ sealed abstract class Ticks(val label: String) extends Product with Serializable

object Ticks {
case object Outside extends Ticks("outside")
case object Inside extends Ticks("inside")
case object Empty extends Ticks("")
}
@@ -23,6 +23,8 @@ import plotly.element._
dtick: Option[Double],
ticklen: Option[Int],
tickfont: Option[Font],
tickprefix: Option[String],
ticksuffix: Option[String],
zeroline: Option[Boolean],
zerolinewidth: Option[Double],
zerolinecolor: Option[Color],
@@ -60,6 +62,8 @@ object Axis {
dtick: JDouble = null,
ticklen: JInt = null,
tickfont: Font = null,
tickprefix: String = null,
ticksuffix: String = null,
zeroline: JBoolean = null,
zerolinewidth: JDouble = null,
zerolinecolor: Color = null,
@@ -95,6 +99,8 @@ object Axis {
Option(dtick) .map(x => x: Double),
Option(ticklen) .map(x => x: Int),
Option(tickfont),
Option(tickprefix),
Option(ticksuffix),
Option(zeroline) .map(x => x: Boolean),
Option(zerolinewidth) .map(x => x: Double),
Option(zerolinecolor),
@@ -37,6 +37,12 @@ import scalatags.JsDom.all.{area => _, _}
),
"Filled Area Plots" -> Seq(
area.BasicOverlaidAreaChart
),
"Heatmaps" -> Seq(
heatmaps.BasicHeatmap,
heatmaps.CategoricalAxisHeatmap,
heatmaps.CustomColorScaleHeatmap,
heatmaps.AnnotatedHeatmap
)
)

@@ -0,0 +1,55 @@
package plotly.demo.heatmaps

import plotly._
import plotly.demo.DemoChart
import plotly.element._
import plotly.layout._

object AnnotatedHeatmap extends DemoChart {

def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#annotated-heatmap"
def id = "annotated-heatmap"
def source = AnnotatedHeatmapSource.source

// demo source start

val x = Seq("A", "B", "C", "D", "E");
val y = Seq("W", "X", "Y", "Z");
val z = Seq(
Seq(0.00, 0.00, 0.75, 0.75, 0.00),
Seq(0.00, 0.00, 0.75, 0.75, 0.00),
Seq(0.75, 0.75, 0.75, 0.75, 0.75),
Seq(0.00, 0.00, 0.00, 0.75, 0.00)
)

val data = Seq(
Heatmap(
z=z, x=x, y=y, showscale=false,
colorscale = ColorScale.CustomScale(Seq(
(0, Color.StringColor("#3D9970")),
(1, Color.StringColor("#001f3f"))
))
)
)

val layout = Layout(
title = "Annotated Heatmap",
xaxis = Axis(ticks=Ticks.Empty, side=Side.Top),
yaxis = Axis(ticks=Ticks.Empty, ticksuffix=" "),
annotations = for {
(xv, xi) <- x.zipWithIndex;
(yv, yi) <- y.zipWithIndex
} yield Annotation(
x=xv,
y=yv,
xref=Ref.Axis(AxisReference.X1),
yref=Ref.Axis(AxisReference.Y1),
showarrow=false,
text=z(yi)(xi).toString,
font=Font(color=Color.StringColor("white"))
)
)

// demo source end

}
@@ -0,0 +1,28 @@
package plotly.demo.heatmaps

import plotly._
import plotly.demo.NoLayoutDemoChart
import plotly.element._

object BasicHeatmap extends NoLayoutDemoChart {

def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#basic-heatmap"
def id = "basic-heatmap"
def source = BasicHeatmapSource.source

// demo source start

val data = Seq(
Heatmap(
z=Seq(
Seq(1, 20, 30),
Seq(20, 1, 60),
Seq(30, 60, 1)
),
colorscale=ColorScale.NamedScale("Portland")
)
)

// demo source end

}
@@ -0,0 +1,29 @@
package plotly.demo.heatmaps

import plotly._
import plotly.demo.NoLayoutDemoChart
import plotly.element._

object CategoricalAxisHeatmap extends NoLayoutDemoChart {

def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#heatmap-with-categorical-axis-labels"
def id = "categorical-axis-heatmap"
def source = CategoricalAxisHeatmapSource.source

// demo source start

val data = Seq(
Heatmap(
z=Seq(
Seq(1, null.asInstanceOf[Int], 30, 50, 1),
Seq(20, 1, 60, 80, 30),
Seq(30, 60, 1, -10, 20)
),
x=Seq("Monday", "Tuesday", "Wednesday", "Thursday", "Friday"),
y=Seq("Morning", "Afternoon", "Evening"),
)
)

// demo source end

}
@@ -0,0 +1,37 @@
package plotly.demo.heatmaps

import plotly._
import plotly.demo.NoLayoutDemoChart
import plotly.element._

object CustomColorScaleHeatmap extends NoLayoutDemoChart {

def plotlyDocUrl = "https://plot.ly/javascript/colorscales/#custom-colorscale-for-contour-plot"
def id = "custom-colorscale-heatmap"
def source = CustomColorScaleHeatmapSource.source

// demo source start

val data = Seq(
Heatmap(
z=Seq(
Seq(10.0, 10.625, 12.5, 15.625, 20.0),
Seq(5.625, 6.25, 8.125, 11.25, 15.625),
Seq(2.5, 3.125, 5.0, 8.125, 12.5),
Seq(0.625, 1.25, 3.125, 6.25, 10.625),
Seq(0.0, 0.625, 2.5, 5.625, 10.0)
),
colorscale=ColorScale.CustomScale(Seq(
(0, Color.RGB(166,206,227)),
(0.25, Color.RGB(31,120,180)),
(0.45, Color.RGB(178,223,138)),
(0.65, Color.RGB(51,160,44)),
(0.85, Color.RGB(251,154,153)),
(1, Color.RGB(227,26,28))
))
)
)

// demo source end

}
@@ -6,17 +6,17 @@ if [ -e gh-pages ]; then
exit 1
fi

sbt demo/fullOptJS
./sbt demo/fullOptJS
mkdir gh-pages

cp \
demo/target/scala-2.11/plotly-demo-opt.js \
demo/target/scala-2.11/plotly-demo-opt.js.map \
demo/target/scala-2.11/plotly-demo-jsdeps.js \
demo/target/scala-2.11/plotly-demo-jsdeps.min.js \
demo/target/scala-2.13/plotly-demo-opt.js \
demo/target/scala-2.13/plotly-demo-opt.js.map \
demo/target/scala-2.13/plotly-demo-jsdeps.js \
demo/target/scala-2.13/plotly-demo-jsdeps.min.js \
gh-pages

cat demo/target/scala-2.11/classes/index.html | \
cat demo/target/scala-2.13/classes/index.html | \
sed 's@\.\./plotly-demo-jsdeps\.js@plotly-demo-jsdeps.min.js@' | \
sed 's@\.\./plotly-demo-fastopt\.js@plotly-demo-opt.js@' | \
cat > gh-pages/index.html
@@ -46,6 +46,7 @@ object ArgonautCodecsInternals extends ArgonautCodecsExtra {
implicit val boxPointsBoolIsWrapper: IsWrapper[BoxPoints.Bool] = null
implicit val sequenceDoublesIsWrapper: IsWrapper[Sequence.Doubles] = null
implicit val sequenceNestedDoublesIsWrapper: IsWrapper[Sequence.NestedDoubles] = null
implicit val sequenceNestedIntsIsWrapper: IsWrapper[Sequence.NestedInts] = null
implicit val sequenceStringsIsWrapper: IsWrapper[Sequence.Strings] = null
implicit val sequenceDatetimesIsWrapper: IsWrapper[Sequence.DateTimes] = null
implicit val doubleElementIsWrapper: IsWrapper[Element.DoubleElement] = null
@@ -377,6 +378,30 @@ object ArgonautCodecsInternals extends ArgonautCodecsExtra {
}
}

implicit val encodeNamedColorScale: EncodeJson[ColorScale.NamedScale] =
EncodeJson.of[String].contramap(_.name)

implicit val decodeNamedColorScale: DecodeJson[ColorScale.NamedScale] =
DecodeJson { c =>
c.as[String].flatMap { s =>
// TODO: Add colorscale name enum?
DecodeResult.ok(ColorScale.NamedScale(s))
}
}

implicit val encodeCustomColorScale: EncodeJson[ColorScale.CustomScale] =
EncodeJson.of[Json].contramap(_.values.toList.asJson)

implicit val decodeCustomColorScale: DecodeJson[ColorScale.CustomScale] =
DecodeJson { c =>
c.as[Seq[(Double, Color)]].flatMap { s =>
DecodeResult.ok(ColorScale.CustomScale(s))
}
}

implicit val colorscaleJsonCodec: JsonSumCodecFor[ColorScale] =
JsonSumCodecFor(jsonSumDirectCodecFor("colorscale"))

implicit val elementJsonCodec: JsonSumCodecFor[Element] =
JsonSumCodecFor(jsonSumDirectCodecFor("element"))

@@ -109,7 +109,7 @@ object DocumentationTests {
// stub...
def getElementById(id: String): String = id
}

private object Numeric {
def linspace(from: Int, to: Int, count: Int) = {
val step = (to - from).toDouble / (count - 1)
@@ -243,6 +243,7 @@ class DocumentationTests extends FlatSpec with Matchers {
"statistical/box",
// TODO 2D Density plots
"statistical/histogram",
"scientific/heatmap",
// TODO 2D Histograms
// TODO Wind rose charts
// TODO Contour plots

0 comments on commit 1d82e3b

Please sign in to comment.
You can’t perform that action at this time.