Skip to content

Commit

Permalink
优化图例色彩关联性 (#13)
Browse files Browse the repository at this point in the history
* Compress images.

* Improve plots.

* Restyle intervals plot.

* Update image.

* Clean code.

* Identify yaxis color.

* Clean code.
  • Loading branch information
zhongl authored Sep 4, 2024
1 parent 43d4089 commit 36ecfa5
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 80 deletions.
Binary file modified mpb-history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified mpb-intervals.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 5 additions & 11 deletions src/garmin/Activities.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package garmin

import java.util as ju
import java.util.NoSuchElementException as Complain

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

import org.scalajs.dom.HTMLElement
import org.scalajs.dom.document
Expand All @@ -21,14 +19,10 @@ object Activities:
Inject[History]
): Page[Activities] =
case (URL("/modern/activities", `activityType`("running")), `a.inline-edit-target`(_)) =>
val es = `a.inline-edit-target`.all(Seq(document)).toList
val fs = for case `href`(s"/modern/activity/$id") <- es yield ActivityId(id).request
if !fs.isEmpty then
for history <- Future.sequence(fs) do
inline def complain[A]: A = throw ju.NoSuchElementException("div.advanced-filtering")
val e = `div.advanced-filtering`(document).getOrElse(complain)
history.filter(_.nonEmpty).inject(`mpb`.elementAt(e.before(_)))
end if
val es = `a.inline-edit-target`.all(Seq(document)).toList
val e = `div.advanced-filtering`(document).getOrElse(throw Complain("anchor"))
val ids = for case `href`(s"/modern/activity/$id") <- es yield ActivityId(id)
ids.inject(`mpb`.elementAt(e.before(_)))
None

case (URL("/modern/activities", `activityType`("running")), _) =>
Expand Down
6 changes: 6 additions & 0 deletions src/garmin/ActivityId.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package garmin

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.scalajs.js

import org.scalajs.dom.Request

import core.*
import core.metrics.*
import core.service.Fetch

Expand All @@ -13,6 +15,9 @@ opaque type ActivityId = String
object ActivityId:
def apply(s: String): ActivityId = s

given inject(using Fetch[ActivityId, Intervals], Inject[History]): Inject[List[ActivityId]] = (e, ids) =>
for history <- Future.sequence(ids.map(_.request)) if history.nonEmpty do history.filter(_.nonEmpty).inject(e)

given fetch(using Fetch[Request, js.Dynamic], Conversion[Get, Request]): Fetch[ActivityId, Intervals] = id =>
inline def url = s"https://connect.garmin.cn/activity-service/activity/$id/splits"
inline def referrer = s"https://connect.garmin.cn/modern/activity/$id"
Expand All @@ -25,6 +30,7 @@ object ActivityId:
laps = d.asInstanceOf[Splits].lapDTOs.toList
yield for l <- laps if isActive(l.intensityType)
yield l.asInstanceOf[Interval]
end for
end fetch

private trait Lap extends js.Object:
Expand Down
22 changes: 6 additions & 16 deletions src/garmin/Profile.scala
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
package garmin

import java.util as ju

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import java.util.NoSuchElementException as Complain

import org.scalajs.dom.Element
import org.scalajs.dom.HTMLElement
import org.scalajs.dom.document

import core.*
import core.metrics.*
import core.service.Fetch
import sourcecode.Name

sealed trait Profile
object Profile:
given page(
using Initialize[HTMLElement],
Fetch[ActivityId, Intervals],
Inject[History]
Inject[List[ActivityId]]
): Page[Profile] =
case (URL(s"/modern/profile/$_", _), `a[data-activityid]`(_)) =>
val es = `a[data-activityid]`.all(Seq(document)).filter(isRunning).toList
val fs = for case `data-activityid`(id) <- es yield ActivityId(id).request
if !fs.isEmpty then
for history <- Future.sequence(fs) do
inline def complain[A]: A = throw ju.NoSuchElementException("anchor")
val e = `div[class^="PageContent"]`(document).getOrElse(complain)
history.filter(_.nonEmpty).inject("mpb".elementAt(e.before(_)))
end if
val es = `a[data-activityid]`.all(Seq(document)).filter(isRunning).toList
val ids = for case `data-activityid`(id) <- es yield ActivityId(id)
val e = `div[class^="PageContent"]`(document).getOrElse(throw Complain("anchor"))
ids.inject("mpb".elementAt(e.before(_)))
None

case m =>
Expand Down
2 changes: 2 additions & 0 deletions src/garmin/package.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package garmin


import org.scalajs.dom.Element
import org.scalajs.dom.URL
import org.scalajs.dom.URLSearchParams


private[garmin] object URL:
def unapply(u: URL): Option[(String, URLSearchParams)] =
Some(u.pathname, u.searchParams)
Expand Down
29 changes: 29 additions & 0 deletions src/plotly/ColorPalette.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package plotly

import scala.scalajs.js
import scala.scalajs.js.JSConverters.*

import typings.plotlyJs.anon.PartialLayout

trait ColorPalette[A]:
def list: List[String]
extension (i: Int) inline def color = list(i % list.length)
extension (a: PartialLayout) inline def setColorPalette = a.setColorway(list.toJSArray)
end ColorPalette

object ColorPalette:
given ColorPalette[Common] with
val list: List[String] = List(
"#1f77b4",
"#ff7f0e",
"#2ca02c",
"#d62728",
"#9467bd",
"#8c564b",
"#e377c2",
"#7f7f7f",
"#bcbd22",
"#17becf"
)
end given
end ColorPalette
5 changes: 4 additions & 1 deletion src/plotly/DataArrayFrom.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import typings.plotlyJs.mod.PlotType
import typings.plotlyJs.plotlyJsBooleans.`false`
import typings.plotlyJs.plotlyJsStrings.legendonly
import typings.plotlyJs.plotlyJsStrings.y
import typings.plotlyJs.plotlyJsStrings.yPlussignname

import core.DateFormat
import core.metrics.*
Expand All @@ -23,11 +24,13 @@ object DataArrayFrom:
Data
.PartialPlotDataAutobinx()
.setName(name)
.setLine(PartialScatterLine().setWidth(1))
.setY(v.map(fy).toJSArray)
.setX(v.indices.map(_ + 1.0).toJSArray)
.setHoverinfo(yPlussignname)

js.Array(
scatterLine("mpb", _.mpb),
scatterLine("mpb", _.mpb).setShowlegend(false),
scatterLine("bpm", _.averageHR.round).setVisible(true).setYaxis("y2"),
scatterLine("spm", _.averageRunCadence.round).setVisible(legendonly).setYaxis("y2")
// scatterLine("/km", _.pace).setVisible(legendonly).setYaxis("y2")
Expand Down
28 changes: 22 additions & 6 deletions src/plotly/Layout.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package plotly

import scala.scalajs.js
import scala.scalajs.js.JSConverters.*

import typings.plotlyJs.anon.PartialLayout
import typings.plotlyJs.anon.PartialLayoutAxis
Expand All @@ -21,24 +22,39 @@ object Layout:
.setHeight(200)
.setMargin(PartialMargin().setPad(4).setL(50).setR(50).setT(50).setB(50))

given intervals(using Layout[Common], Title[Intervals]): Layout[Intervals] = is =>
given intervals(using Layout[Common], Title[Intervals], ColorPalette[Common]): Layout[Intervals] = is =>
inline def inside = PartialLegendBgcolor()
.setX(1.05)
.setX(1.1)
.setY(0.5)
.setItemclick(`false`)
.setItemdoubleclick(`false`)

inline def yAxis = PartialLayoutAxis()
.setColor(0.color)
.setTickformat(".2f")
.setOverlaying(y2)
.setTickmodeSync

inline def yAxis2 = PartialLayoutAxis()
.setColor(1.color)
.setSide(right)

Common.layout
.setTitle(is.title)
.setShowlegend(true)
.setColorPalette
.setLegend(inside)
.setXaxis(PartialLayoutAxis().setDtick(1.0).setTitle("圈数"))
.setYaxis(PartialLayoutAxis().setOverlaying(y2).setTickmodeSync)
.setYaxis2(PartialLayoutAxis().setSide(right))
.setYaxis(yAxis)
.setYaxis2(yAxis2)
end intervals

given history(using Layout[Common], Title[History]): Layout[History] = h =>
Common.layout.setTitle(h.title).setShowlegend(false)
given history(using Layout[Common], Title[History], ColorPalette[Common]): Layout[History] = h =>
Common.layout
.setTitle(h.title)
.setShowlegend(false)
.setColorPalette
.setYaxis(PartialLayoutAxis().setTickformat(".2f").setColor(0.color))

extension (a: PartialLayoutAxis)
private inline def setTickmodeSync: PartialLayoutAxis =
Expand Down
38 changes: 22 additions & 16 deletions src/plotly/Listen.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
package plotly

import scala.scalajs.js
import scala.scalajs.js.JSConverters.*

import org.scalajs.dom.HTMLElement

import typings.plotlyJs.anon.PartialLayout
import typings.plotlyJs.anon.PartialLayoutAxis
import typings.plotlyJs.anon.PartialPlotDataAutobinx
import typings.plotlyJs.mod.LegendClickEvent
import typings.plotlyJs.mod.PlotlyHTMLElement
import typings.plotlyJs.mod.PlotMouseEvent
import typings.plotlyJs.plotlyJsStrings.legendonly
import typings.plotlyJs.plotlyJsStrings.plotly_hover
import typings.plotlyJs.plotlyJsStrings.plotly_legendclick
import typings.plotlyJs.plotlyJsStrings.plotly_unhover
import typings.std.stdStrings.highlight
import typings.std.stdStrings.normal
import typings.std.stdStrings.visible
import typings.plotlyJs.plotlyJsStrings.right
import typings.plotlyJsDistMin.mod.relayout
import typings.plotlyJsDistMin.mod.restyle

import core.metrics.*
import typings.plotlyJs.anon.PartialScatterLine

type Context[A] = (A, PlotlyHTMLElement)
trait Listen[A, B] extends (Context[A] => Unit)
Expand All @@ -25,36 +30,37 @@ object Listen:
given tuple[A, H, T <: Tuple](using h: Listen[A, H], t: Listen[A, T]): Listen[A, H *: T] = (a, p) =>
h(a, p); t(a, p)

given legendclick(
using
show: Restyle[visible],
hidden: Restyle[legendonly]
): Listen[Intervals, plotly_legendclick] = (_, p) =>
given legendclick(using ColorPalette[Common]): Listen[Intervals, plotly_legendclick] = (_, p) =>
p.on(
plotly_legendclick,
accept[LegendClickEvent]: e =>
e.curveNumber.toInt match
case 0 =>
case i =>
val (l, r) = Range(1, e.data.length).partition(_ == i)
show(p, l)
hidden(p, r)
restyle(p, PartialPlotDataAutobinx().setVisible(true), l.map(_.doubleValue).toJSArray)
restyle(p, PartialPlotDataAutobinx().setVisible(legendonly), r.map(_.doubleValue).toJSArray)
val yAxis2 = PartialLayoutAxis().setColor(i.color).setSide(right)
relayout(p, PartialLayout().setYaxis2(yAxis2))
end match
)
end legendclick

given hover(using h: Restyle[highlight], n: Restyle[normal]): Listen[History, plotly_hover] = (_, p) =>
given hover: Listen[History, plotly_hover] = (_, p) =>
val area: HTMLElement = p.querySelector(".nsewdrag")
p.on_plotlyhover(
plotly_hover,
e =>
area.style.cursor = "pointer"
h(p, Seq(e.points(0).curveNumber.intValue))
val data = PartialPlotDataAutobinx().setLine(PartialScatterLine().setWidth(2))
restyle(p, data, e.points(0).curveNumber)
)
p.on(
plotly_unhover,
accept[PlotMouseEvent]: e =>
area.style.cursor = ""
n(p, Seq(e.points(0).curveNumber.intValue))
plotly_unhover,
accept[PlotMouseEvent]: e =>
area.style.cursor = ""
val data = PartialPlotDataAutobinx().setLine(PartialScatterLine().setWidth(1))
restyle(p, data, e.points(0).curveNumber)
)
end hover

Expand Down
30 changes: 0 additions & 30 deletions src/plotly/Restyle.scala

This file was deleted.

0 comments on commit 36ecfa5

Please sign in to comment.