diff --git a/app/javascript/controllers/bulk_select_controller.js b/app/javascript/controllers/bulk_select_controller.js index f7050b1a..c8ae14f2 100644 --- a/app/javascript/controllers/bulk_select_controller.js +++ b/app/javascript/controllers/bulk_select_controller.js @@ -19,19 +19,19 @@ export default class extends Controller { } bulkEditDrawerTitleTargetConnected(element) { - element.innerText = `Edit ${this.selectedIdsValue.length} ${this.#pluralizedResourceName()}` + element.innerText = `Edit ${this.selectedIdsValue.length} ${this._pluralizedResourceName()}` } submitBulkRequest(e) { const form = e.target.closest("form"); const scope = e.params.scope - this.#addHiddenFormInputsForSelectedIds(form, `${scope}[entry_ids][]`, this.selectedIdsValue) + this._addHiddenFormInputsForSelectedIds(form, `${scope}[entry_ids][]`, this.selectedIdsValue) form.requestSubmit() } togglePageSelection(e) { if (e.target.checked) { - this.#selectAll() + this._selectAll() } else { this.deselectAll() } @@ -40,20 +40,20 @@ export default class extends Controller { toggleGroupSelection(e) { const group = this.groupTargets.find(group => group.contains(e.target)) - this.#rowsForGroup(group).forEach(row => { + this._rowsForGroup(group).forEach(row => { if (e.target.checked) { - this.#addToSelection(row.dataset.id) + this._addToSelection(row.dataset.id) } else { - this.#removeFromSelection(row.dataset.id) + this._removeFromSelection(row.dataset.id) } }) } toggleRowSelection(e) { if (e.target.checked) { - this.#addToSelection(e.target.dataset.id) + this._addToSelection(e.target.dataset.id) } else { - this.#removeFromSelection(e.target.dataset.id) + this._removeFromSelection(e.target.dataset.id) } } @@ -66,8 +66,8 @@ export default class extends Controller { this._updateView() } - #addHiddenFormInputsForSelectedIds(form, paramName, transactionIds) { - this.#resetFormInputs(form, paramName); + _addHiddenFormInputsForSelectedIds(form, paramName, transactionIds) { + this._resetFormInputs(form, paramName); transactionIds.forEach(id => { const input = document.createElement("input"); @@ -78,47 +78,47 @@ export default class extends Controller { }) } - #resetFormInputs(form, paramName) { + _resetFormInputs(form, paramName) { const existingInputs = form.querySelectorAll(`input[name='${paramName}']`); existingInputs.forEach((input) => input.remove()); } - #rowsForGroup(group) { + _rowsForGroup(group) { return this.rowTargets.filter(row => group.contains(row)) } - #addToSelection(idToAdd) { + _addToSelection(idToAdd) { this.selectedIdsValue = Array.from( new Set([...this.selectedIdsValue, idToAdd]) ) } - #removeFromSelection(idToRemove) { + _removeFromSelection(idToRemove) { this.selectedIdsValue = this.selectedIdsValue.filter(id => id !== idToRemove) } - #selectAll() { + _selectAll() { this.selectedIdsValue = this.rowTargets.map(t => t.dataset.id) } _updateView = () => { - this.#updateSelectionBar() - this.#updateGroups() - this.#updateRows() + this._updateSelectionBar() + this._updateGroups() + this._updateRows() } - #updateSelectionBar() { + _updateSelectionBar() { const count = this.selectedIdsValue.length - this.selectionBarTextTarget.innerText = `${count} ${this.#pluralizedResourceName()} selected` + this.selectionBarTextTarget.innerText = `${count} ${this._pluralizedResourceName()} selected` this.selectionBarTarget.hidden = count === 0 this.selectionBarTarget.querySelector("input[type='checkbox']").checked = count > 0 } - #pluralizedResourceName() { + _pluralizedResourceName() { return `${this.resourceValue}${this.selectedIdsValue.length === 1 ? "" : "s"}` } - #updateGroups() { + _updateGroups() { this.groupTargets.forEach(group => { const rows = this.rowTargets.filter(row => group.contains(row)) const groupSelected = rows.length > 0 && rows.every(row => this.selectedIdsValue.includes(row.dataset.id)) @@ -126,7 +126,7 @@ export default class extends Controller { }) } - #updateRows() { + _updateRows() { this.rowTargets.forEach(row => { row.checked = this.selectedIdsValue.includes(row.dataset.id) }) diff --git a/app/javascript/controllers/time_series_chart_controller.js b/app/javascript/controllers/time_series_chart_controller.js index b094f41c..e9d3b6c2 100644 --- a/app/javascript/controllers/time_series_chart_controller.js +++ b/app/javascript/controllers/time_series_chart_controller.js @@ -11,47 +11,47 @@ export default class extends Controller { usePercentSign: Boolean } - #d3SvgMemo = null; - #d3GroupMemo = null; - #d3Tooltip = null; - #d3InitialContainerWidth = 0; - #d3InitialContainerHeight = 0; - #normalDataPoints = []; + _d3SvgMemo = null; + _d3GroupMemo = null; + _d3Tooltip = null; + _d3InitialContainerWidth = 0; + _d3InitialContainerHeight = 0; + _normalDataPoints = []; connect() { - this.#install() - document.addEventListener("turbo:load", this.#reinstall) + this._install() + document.addEventListener("turbo:load", this._reinstall) } disconnect() { - this.#teardown() - document.removeEventListener("turbo:load", this.#reinstall) + this._teardown() + document.removeEventListener("turbo:load", this._reinstall) } - #reinstall = () => { - this.#teardown() - this.#install() + _reinstall = () => { + this._teardown() + this._install() } - #teardown() { - this.#d3SvgMemo = null - this.#d3GroupMemo = null - this.#d3Tooltip = null - this.#normalDataPoints = [] + _teardown() { + this._d3SvgMemo = null + this._d3GroupMemo = null + this._d3Tooltip = null + this._normalDataPoints = [] - this.#d3Container.selectAll("*").remove() + this._d3Container.selectAll("*").remove() } - #install() { - this.#normalizeDataPoints() - this.#rememberInitialContainerSize() - this.#draw() + _install() { + this._normalizeDataPoints() + this._rememberInitialContainerSize() + this._draw() } - #normalizeDataPoints() { - this.#normalDataPoints = (this.dataValue.values || []).map((d) => ({ + _normalizeDataPoints() { + this._normalDataPoints = (this.dataValue.values || []).map((d) => ({ ...d, date: new Date(d.date), value: d.value.amount ? +d.value.amount : +d.value, @@ -60,96 +60,96 @@ export default class extends Controller { } - #rememberInitialContainerSize() { - this.#d3InitialContainerWidth = this.#d3Container.node().clientWidth - this.#d3InitialContainerHeight = this.#d3Container.node().clientHeight + _rememberInitialContainerSize() { + this._d3InitialContainerWidth = this._d3Container.node().clientWidth + this._d3InitialContainerHeight = this._d3Container.node().clientHeight } - #draw() { - if (this.#normalDataPoints.length < 2) { - this.#drawEmpty() + _draw() { + if (this._normalDataPoints.length < 2) { + this._drawEmpty() } else { - this.#drawChart() + this._drawChart() } } - #drawEmpty() { - this.#d3Svg.selectAll(".tick").remove() - this.#d3Svg.selectAll(".domain").remove() + _drawEmpty() { + this._d3Svg.selectAll(".tick").remove() + this._d3Svg.selectAll(".domain").remove() - this.#drawDashedLineEmptyState() - this.#drawCenteredCircleEmptyState() + this._drawDashedLineEmptyState() + this._drawCenteredCircleEmptyState() } - #drawDashedLineEmptyState() { - this.#d3Svg + _drawDashedLineEmptyState() { + this._d3Svg .append("line") - .attr("x1", this.#d3InitialContainerWidth / 2) + .attr("x1", this._d3InitialContainerWidth / 2) .attr("y1", 0) - .attr("x2", this.#d3InitialContainerWidth / 2) - .attr("y2", this.#d3InitialContainerHeight) + .attr("x2", this._d3InitialContainerWidth / 2) + .attr("y2", this._d3InitialContainerHeight) .attr("stroke", tailwindColors.gray[300]) .attr("stroke-dasharray", "4, 4") } - #drawCenteredCircleEmptyState() { - this.#d3Svg + _drawCenteredCircleEmptyState() { + this._d3Svg .append("circle") - .attr("cx", this.#d3InitialContainerWidth / 2) - .attr("cy", this.#d3InitialContainerHeight / 2) + .attr("cx", this._d3InitialContainerWidth / 2) + .attr("cy", this._d3InitialContainerHeight / 2) .attr("r", 4) .style("fill", tailwindColors.gray[400]) } - #drawChart() { - this.#drawTrendline() + _drawChart() { + this._drawTrendline() if (this.useLabelsValue) { - this.#drawXAxisLabels() - this.#drawGradientBelowTrendline() + this._drawXAxisLabels() + this._drawGradientBelowTrendline() } if (this.useTooltipValue) { - this.#drawTooltip() - this.#trackMouseForShowingTooltip() + this._drawTooltip() + this._trackMouseForShowingTooltip() } } - #drawTrendline() { - this.#installTrendlineSplit() + _drawTrendline() { + this._installTrendlineSplit() - this.#d3Group + this._d3Group .append("path") - .datum(this.#normalDataPoints) + .datum(this._normalDataPoints) .attr("fill", "none") .attr("stroke", `url(#${this.element.id}-split-gradient)`) - .attr("d", this.#d3Line) + .attr("d", this._d3Line) .attr("stroke-linejoin", "round") .attr("stroke-linecap", "round") .attr("stroke-width", this.strokeWidthValue) } - #installTrendlineSplit() { - const gradient = this.#d3Svg + _installTrendlineSplit() { + const gradient = this._d3Svg .append("defs") .append("linearGradient") .attr("id", `${this.element.id}-split-gradient`) .attr("gradientUnits", "userSpaceOnUse") - .attr("x1", this.#d3XScale.range()[0]) - .attr("x2", this.#d3XScale.range()[1]) + .attr("x1", this._d3XScale.range()[0]) + .attr("x2", this._d3XScale.range()[1]) gradient.append("stop") .attr("class", "start-color") .attr("offset", "0%") - .attr("stop-color", this.#trendColor) + .attr("stop-color", this._trendColor) gradient.append("stop") .attr("class", "middle-color") .attr("offset", "100%") - .attr("stop-color", this.#trendColor) + .attr("stop-color", this._trendColor) gradient.append("stop") .attr("class", "end-color") @@ -157,31 +157,31 @@ export default class extends Controller { .attr("stop-color", tailwindColors.gray[300]) } - #setTrendlineSplitAt(percent) { - this.#d3Svg + _setTrendlineSplitAt(percent) { + this._d3Svg .select(`#${this.element.id}-split-gradient`) .select(".middle-color") .attr("offset", `${percent * 100}%`) - this.#d3Svg + this._d3Svg .select(`#${this.element.id}-split-gradient`) .select(".end-color") .attr("offset", `${percent * 100}%`) - this.#d3Svg + this._d3Svg .select(`#${this.element.id}-trendline-gradient-rect`) - .attr("width", this.#d3ContainerWidth * percent) + .attr("width", this._d3ContainerWidth * percent) } - #drawXAxisLabels() { + _drawXAxisLabels() { // Add ticks - this.#d3Group + this._d3Group .append("g") - .attr("transform", `translate(0,${this.#d3ContainerHeight})`) + .attr("transform", `translate(0,${this._d3ContainerHeight})`) .call( d3 - .axisBottom(this.#d3XScale) - .tickValues([this.#normalDataPoints[0].date, this.#normalDataPoints[this.#normalDataPoints.length - 1].date]) + .axisBottom(this._d3XScale) + .tickValues([this._normalDataPoints[0].date, this._normalDataPoints[this._normalDataPoints.length - 1].date]) .tickSize(0) .tickFormat(d3.timeFormat("%d %b %Y")) ) @@ -189,7 +189,7 @@ export default class extends Controller { .remove() // Style ticks - this.#d3Group.selectAll(".tick text") + this._d3Group.selectAll(".tick text") .style("fill", tailwindColors.gray[500]) .style("font-size", "12px") .style("font-weight", "500") @@ -201,54 +201,54 @@ export default class extends Controller { .attr("dy", "0em") } - #drawGradientBelowTrendline() { + _drawGradientBelowTrendline() { // Define gradient - const gradient = this.#d3Group + const gradient = this._d3Group .append("defs") .append("linearGradient") .attr("id", `${this.element.id}-trendline-gradient`) .attr("gradientUnits", "userSpaceOnUse") .attr("x1", 0) .attr("x2", 0) - .attr("y1", this.#d3YScale(d3.max(this.#normalDataPoints, d => d.value))) - .attr("y2", this.#d3ContainerHeight) + .attr("y1", this._d3YScale(d3.max(this._normalDataPoints, d => d.value))) + .attr("y2", this._d3ContainerHeight) gradient .append("stop") .attr("offset", 0) - .attr("stop-color", this.#trendColor) + .attr("stop-color", this._trendColor) .attr("stop-opacity", 0.06) gradient .append("stop") .attr("offset", 0.5) - .attr("stop-color", this.#trendColor) + .attr("stop-color", this._trendColor) .attr("stop-opacity", 0) // Clip path makes gradient start at the trendline - this.#d3Group + this._d3Group .append("clipPath") .attr("id", `${this.element.id}-clip-below-trendline`) .append("path") - .datum(this.#normalDataPoints) + .datum(this._normalDataPoints) .attr("d", d3.area() - .x(d => this.#d3XScale(d.date)) - .y0(this.#d3ContainerHeight) - .y1(d => this.#d3YScale(d.value)) + .x(d => this._d3XScale(d.date)) + .y0(this._d3ContainerHeight) + .y1(d => this._d3YScale(d.value)) ) // Apply the gradient + clip path - this.#d3Group + this._d3Group .append("rect") .attr("id", `${this.element.id}-trendline-gradient-rect`) - .attr("width", this.#d3ContainerWidth) - .attr("height", this.#d3ContainerHeight) + .attr("width", this._d3ContainerWidth) + .attr("height", this._d3ContainerHeight) .attr("clip-path", `url(#${this.element.id}-clip-below-trendline)`) .style("fill", `url(#${this.element.id}-trendline-gradient)`) } - #drawTooltip() { - this.#d3Tooltip = d3 + _drawTooltip() { + this._d3Tooltip = d3 .select(`#${this.element.id}`) .append("div") .style("position", "absolute") @@ -261,13 +261,13 @@ export default class extends Controller { .style("opacity", 0) // Starts as hidden } - #trackMouseForShowingTooltip() { + _trackMouseForShowingTooltip() { const bisectDate = d3.bisector(d => d.date).left - this.#d3Group + this._d3Group .append("rect") - .attr("width", this.#d3ContainerWidth) - .attr("height", this.#d3ContainerHeight) + .attr("width", this._d3ContainerWidth) + .attr("height", this._d3ContainerHeight) .attr("fill", "none") .attr("pointer-events", "all") .on("mousemove", (event) => { @@ -278,53 +278,53 @@ export default class extends Controller { const adjustedX = overflowX > 0 ? event.pageX - overflowX - 20 : tooltipX const [xPos] = d3.pointer(event) - const x0 = bisectDate(this.#normalDataPoints, this.#d3XScale.invert(xPos), 1) - const d0 = this.#normalDataPoints[x0 - 1] - const d1 = this.#normalDataPoints[x0] - const d = xPos - this.#d3XScale(d0.date) > this.#d3XScale(d1.date) - xPos ? d1 : d0 - const xPercent = this.#d3XScale(d.date) / this.#d3ContainerWidth + const x0 = bisectDate(this._normalDataPoints, this._d3XScale.invert(xPos), 1) + const d0 = this._normalDataPoints[x0 - 1] + const d1 = this._normalDataPoints[x0] + const d = xPos - this._d3XScale(d0.date) > this._d3XScale(d1.date) - xPos ? d1 : d0 + const xPercent = this._d3XScale(d.date) / this._d3ContainerWidth - this.#setTrendlineSplitAt(xPercent) + this._setTrendlineSplitAt(xPercent) // Reset - this.#d3Group.selectAll(".data-point-circle").remove() - this.#d3Group.selectAll(".guideline").remove() + this._d3Group.selectAll(".data-point-circle").remove() + this._d3Group.selectAll(".guideline").remove() // Guideline - this.#d3Group + this._d3Group .append("line") .attr("class", "guideline") - .attr("x1", this.#d3XScale(d.date)) + .attr("x1", this._d3XScale(d.date)) .attr("y1", 0) - .attr("x2", this.#d3XScale(d.date)) - .attr("y2", this.#d3ContainerHeight) + .attr("x2", this._d3XScale(d.date)) + .attr("y2", this._d3ContainerHeight) .attr("stroke", tailwindColors.gray[300]) .attr("stroke-dasharray", "4, 4") // Big circle - this.#d3Group + this._d3Group .append("circle") .attr("class", "data-point-circle") - .attr("cx", this.#d3XScale(d.date)) - .attr("cy", this.#d3YScale(d.value)) + .attr("cx", this._d3XScale(d.date)) + .attr("cy", this._d3YScale(d.value)) .attr("r", 8) - .attr("fill", this.#trendColor) + .attr("fill", this._trendColor) .attr("fill-opacity", "0.1") .attr("pointer-events", "none") // Small circle - this.#d3Group + this._d3Group .append("circle") .attr("class", "data-point-circle") - .attr("cx", this.#d3XScale(d.date)) - .attr("cy", this.#d3YScale(d.value)) + .attr("cx", this._d3XScale(d.date)) + .attr("cy", this._d3YScale(d.value)) .attr("r", 3) - .attr("fill", this.#trendColor) + .attr("fill", this._trendColor) .attr("pointer-events", "none") // Render tooltip - this.#d3Tooltip - .html(this.#tooltipTemplate(d)) + this._d3Tooltip + .html(this._tooltipTemplate(d)) .style("opacity", 1) .style("z-index", 999) .style("left", adjustedX + "px") @@ -334,16 +334,16 @@ export default class extends Controller { const hoveringOnGuideline = event.toElement?.classList.contains("guideline") if (!hoveringOnGuideline) { - this.#d3Group.selectAll(".guideline").remove() - this.#d3Group.selectAll(".data-point-circle").remove() - this.#d3Tooltip.style("opacity", 0) + this._d3Group.selectAll(".guideline").remove() + this._d3Group.selectAll(".data-point-circle").remove() + this._d3Tooltip.style("opacity", 0) - this.#setTrendlineSplitAt(1) + this._setTrendlineSplitAt(1) } }) } - #tooltipTemplate(datum) { + _tooltipTemplate(datum) { return (`
${d3.timeFormat("%b %d, %Y")(datum.date)} @@ -356,26 +356,26 @@ export default class extends Controller { cx="5" cy="5" r="4" - stroke="${this.#tooltipTrendColor(datum)}" + stroke="${this._tooltipTrendColor(datum)}" fill="transparent" stroke-width="1"> - ${this.#tooltipValue(datum)}${this.usePercentSignValue ? "%" : ""} + ${this._tooltipValue(datum)}${this.usePercentSignValue ? "%" : ""}
${this.usePercentSignValue || datum.trend.value === 0 || datum.trend.value.amount === 0 ? ` ` : ` - - ${this.#tooltipChange(datum)} (${datum.trend.percent}%) + + ${this._tooltipChange(datum)} (${datum.trend.percent}%) `} `) } - #tooltipTrendColor(datum) { + _tooltipTrendColor(datum) { return { up: tailwindColors.success, down: tailwindColors.error, @@ -383,30 +383,30 @@ export default class extends Controller { }[datum.trend.direction] } - #tooltipValue(datum) { + _tooltipValue(datum) { if (datum.currency) { - return this.#currencyValue(datum) + return this._currencyValue(datum) } else { return datum.value } } - #tooltipChange(datum) { + _tooltipChange(datum) { if (datum.currency) { - return this.#currencyChange(datum) + return this._currencyChange(datum) } else { - return this.#decimalChange(datum) + return this._decimalChange(datum) } } - #currencyValue(datum) { + _currencyValue(datum) { return Intl.NumberFormat(undefined, { style: "currency", currency: datum.currency, }).format(datum.value) } - #currencyChange(datum) { + _currencyChange(datum) { return Intl.NumberFormat(undefined, { style: "currency", currency: datum.currency, @@ -414,7 +414,7 @@ export default class extends Controller { }).format(datum.trend.value.amount) } - #decimalChange(datum) { + _decimalChange(datum) { return Intl.NumberFormat(undefined, { style: "decimal", signDisplay: "always", @@ -422,38 +422,38 @@ export default class extends Controller { } - #createMainSvg() { - return this.#d3Container + _createMainSvg() { + return this._d3Container .append("svg") - .attr("width", this.#d3InitialContainerWidth) - .attr("height", this.#d3InitialContainerHeight) - .attr("viewBox", [0, 0, this.#d3InitialContainerWidth, this.#d3InitialContainerHeight]) + .attr("width", this._d3InitialContainerWidth) + .attr("height", this._d3InitialContainerHeight) + .attr("viewBox", [0, 0, this._d3InitialContainerWidth, this._d3InitialContainerHeight]) } - #createMainGroup() { - return this.#d3Svg + _createMainGroup() { + return this._d3Svg .append("g") - .attr("transform", `translate(${this.#margin.left},${this.#margin.top})`) + .attr("transform", `translate(${this._margin.left},${this._margin.top})`) } - get #d3Svg() { - if (this.#d3SvgMemo) { - return this.#d3SvgMemo + get _d3Svg() { + if (this._d3SvgMemo) { + return this._d3SvgMemo } else { - return this.#d3SvgMemo = this.#createMainSvg() + return this._d3SvgMemo = this._createMainSvg() } } - get #d3Group() { - if (this.#d3GroupMemo) { - return this.#d3GroupMemo + get _d3Group() { + if (this._d3GroupMemo) { + return this._d3GroupMemo } else { - return this.#d3GroupMemo = this.#createMainGroup() + return this._d3GroupMemo = this._createMainGroup() } } - get #margin() { + get _margin() { if (this.useLabelsValue) { return { top: 20, right: 0, bottom: 30, left: 0 } } else { @@ -461,59 +461,59 @@ export default class extends Controller { } } - get #d3ContainerWidth() { - return this.#d3InitialContainerWidth - this.#margin.left - this.#margin.right + get _d3ContainerWidth() { + return this._d3InitialContainerWidth - this._margin.left - this._margin.right } - get #d3ContainerHeight() { - return this.#d3InitialContainerHeight - this.#margin.top - this.#margin.bottom + get _d3ContainerHeight() { + return this._d3InitialContainerHeight - this._margin.top - this._margin.bottom } - get #d3Container() { + get _d3Container() { return d3.select(this.element) } - get #trendColor() { - if (this.#trendDirection === "flat") { + get _trendColor() { + if (this._trendDirection === "flat") { return tailwindColors.gray[500] - } else if (this.#trendDirection === this.#favorableDirection) { + } else if (this._trendDirection === this._favorableDirection) { return tailwindColors.green[500] } else { return tailwindColors.error } } - get #trendDirection() { + get _trendDirection() { return this.dataValue.trend.direction } - get #favorableDirection() { + get _favorableDirection() { return this.dataValue.trend.favorable_direction } - get #d3Line() { + get _d3Line() { return d3 .line() - .x(d => this.#d3XScale(d.date)) - .y(d => this.#d3YScale(d.value)) + .x(d => this._d3XScale(d.date)) + .y(d => this._d3YScale(d.value)) } - get #d3XScale() { + get _d3XScale() { return d3 .scaleTime() - .rangeRound([0, this.#d3ContainerWidth]) - .domain(d3.extent(this.#normalDataPoints, d => d.date)) + .rangeRound([0, this._d3ContainerWidth]) + .domain(d3.extent(this._normalDataPoints, d => d.date)) } - get #d3YScale() { + get _d3YScale() { const reductionPercent = this.useLabelsValue ? 0.15 : 0.05 - const dataMin = d3.min(this.#normalDataPoints, d => d.value) - const dataMax = d3.max(this.#normalDataPoints, d => d.value) + const dataMin = d3.min(this._normalDataPoints, d => d.value) + const dataMax = d3.max(this._normalDataPoints, d => d.value) const padding = (dataMax - dataMin) * reductionPercent return d3 .scaleLinear() - .rangeRound([this.#d3ContainerHeight, 0]) + .rangeRound([this._d3ContainerHeight, 0]) .domain([dataMin - padding, dataMax + padding]) } -} +} \ No newline at end of file