mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-28 17:49:38 +02:00
Refactor trendline chart controller into a time series chart controller
This commit is contained in:
parent
ee920f359c
commit
237cfc1f45
3 changed files with 139 additions and 6 deletions
135
app/javascript/controllers/time_series_chart_controller.js
Normal file
135
app/javascript/controllers/time_series_chart_controller.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import tailwindColors from "@maybe/tailwindcolors"
|
||||||
|
import * as d3 from "d3"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = { series: Object }
|
||||||
|
|
||||||
|
#_dataPoints = []
|
||||||
|
#_d3Svg = null
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.#install()
|
||||||
|
document.addEventListener("turbo:load", this.#reinstall)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
document.removeEventListener("turbo:load", this.#reinstall)
|
||||||
|
}
|
||||||
|
|
||||||
|
#reinstall = () => {
|
||||||
|
this.#teardown()
|
||||||
|
this.#install()
|
||||||
|
}
|
||||||
|
|
||||||
|
#teardown() {
|
||||||
|
this.#_d3Svg = null
|
||||||
|
this.#_dataPoints = []
|
||||||
|
this.#d3Container.selectAll("*").remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
#install() {
|
||||||
|
this.#normalizeDataPoints()
|
||||||
|
this.#draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
#normalizeDataPoints() {
|
||||||
|
this.#dataPoints = this.seriesValue.values.map((d) => ({
|
||||||
|
date: new Date(d.date),
|
||||||
|
value: d.value.amount ? +d.value.amount : +d.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#draw() {
|
||||||
|
this.#d3Svg
|
||||||
|
.append("path")
|
||||||
|
.datum(this.#dataPoints)
|
||||||
|
.attr("fill", "none")
|
||||||
|
.attr("stroke", this.#trendColor)
|
||||||
|
.attr("stroke-width", 2)
|
||||||
|
.attr("d", this.#d3Line);
|
||||||
|
}
|
||||||
|
|
||||||
|
#createD3Svg() {
|
||||||
|
const height = this.#d3ContainerHeight
|
||||||
|
const width = this.#d3ContainerWidth
|
||||||
|
|
||||||
|
return this.#d3Container
|
||||||
|
.append("svg")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height)
|
||||||
|
.attr("viewBox", [ 0, 0, width, height ])
|
||||||
|
}
|
||||||
|
|
||||||
|
get #dataPoints() {
|
||||||
|
return this.#_dataPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
set #dataPoints(dataPoints) {
|
||||||
|
this.#_dataPoints = dataPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
get #d3Svg() {
|
||||||
|
if (this.#_d3Svg) {
|
||||||
|
return this.#_d3Svg
|
||||||
|
} else {
|
||||||
|
return this.#_d3Svg = this.#createD3Svg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get #d3ContainerWidth() {
|
||||||
|
return this.#d3Container.node().clientWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
get #d3ContainerHeight() {
|
||||||
|
return this.#d3Container.node().clientHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
get #d3Container() {
|
||||||
|
return d3.select(this.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
get #trendColor() {
|
||||||
|
if (this.#trendDirection === "flat") {
|
||||||
|
return tailwindColors.gray[500]
|
||||||
|
} else if (this.#trendDirection === this.#favorableDirection) {
|
||||||
|
return tailwindColors.green[500]
|
||||||
|
} else {
|
||||||
|
return tailwindColors.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get #trendDirection() {
|
||||||
|
return this.seriesValue.trend.direction
|
||||||
|
}
|
||||||
|
|
||||||
|
get #favorableDirection() {
|
||||||
|
return this.seriesValue.trend.favorableDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
get #d3Line() {
|
||||||
|
return d3
|
||||||
|
.line()
|
||||||
|
.x(d => this.#d3XScale(d.date))
|
||||||
|
.y(d => this.#d3YScale(d.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
get #d3XScale() {
|
||||||
|
return d3
|
||||||
|
.scaleTime()
|
||||||
|
.rangeRound([0, this.#d3ContainerWidth])
|
||||||
|
.domain(d3.extent(this.#dataPoints, d => d.date))
|
||||||
|
}
|
||||||
|
|
||||||
|
get #d3YScale() {
|
||||||
|
const PADDING = 0.05
|
||||||
|
const dataMin = d3.min(this.#dataPoints, d => d.value)
|
||||||
|
const dataMax = d3.max(this.#dataPoints, d => d.value)
|
||||||
|
const padding = (dataMax - dataMin) * PADDING
|
||||||
|
|
||||||
|
return d3
|
||||||
|
.scaleLinear()
|
||||||
|
.rangeRound([this.#d3ContainerHeight, 0])
|
||||||
|
.domain([dataMin - padding, dataMax + padding])
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ class TimeSeries::Value
|
||||||
|
|
||||||
def as_json
|
def as_json
|
||||||
{
|
{
|
||||||
date: date,
|
date: date.iso8601,
|
||||||
value: value.as_json,
|
value: value.as_json,
|
||||||
trend: trend.as_json
|
trend: trend.as_json
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,9 @@
|
||||||
} %>
|
} %>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-controller="trendline"
|
data-controller="time-series-chart"
|
||||||
id="assetsTrendline"
|
|
||||||
class="h-full w-2/5"
|
class="h-full w-2/5"
|
||||||
data-trendline-series-value="<%= @asset_series.to_json %>"
|
data-time-series-chart-series-value="<%= @asset_series.to_json %>"></div>
|
||||||
data-trendline-classification-value="asset"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 p-4 flex items-stretch justify-between">
|
<div class="w-1/2 p-4 flex items-stretch justify-between">
|
||||||
<div class="space-y-2 grow">
|
<div class="space-y-2 grow">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue