diff --git a/app/assets/javascripts/kanaui/kiddo/charts/line_chart.js b/app/assets/javascripts/kanaui/kiddo/charts/line_chart.js index 42231e8..c03dbe5 100644 --- a/app/assets/javascripts/kanaui/kiddo/charts/line_chart.js +++ b/app/assets/javascripts/kanaui/kiddo/charts/line_chart.js @@ -68,26 +68,24 @@ return dataset.name; })); - // Create legend container at the top + // Create legend container to the right of the chart area var legendContainer = svg .append("g") .attr("class", "chart-legend") - .attr("transform", "translate(" + (self.width - 100) + ", -25)"); + .attr("transform", "translate(" + (self.width + self.margin_left + 15) + ", 10)"); // Calculate total values for legend var legendData = datasets.map(function (dataset, index) { var latestValue = dataset.values[dataset.values.length - 1]; - var totalCount = dataset.values.length; return { name: dataset.name, value: latestValue ? latestValue.y : 0, - count: totalCount, color: self.color(dataset.name), index: index, }; }); - // Create legend items + // Create legend items as a vertical column var legendItems = legendContainer .selectAll(".legend-item") .data(legendData) @@ -95,34 +93,30 @@ .append("g") .attr("class", "legend-item"); - var xOffset = 0; - legendItems.each(function (d, i) { + var yOffset = 0; + legendItems.each(function (d) { var legendItem = d3.select(this); - // Add colored circle legendItem .append("circle") - .attr("cx", xOffset + 6) - .attr("cy", 0) + .attr("cx", 6) + .attr("cy", yOffset) .attr("r", 6) .style("fill", d.color); - // Add text label var labelText = - helper.formatSeriesName(d.name) + " (" + d.count + "): " + d3.format(",.2f")(d.value); + helper.formatSeriesName(d.name, self.reportName) + ": " + helper.formatValue(d.value); legendItem .append("text") - .attr("x", xOffset + 18) - .attr("y", 0) + .attr("x", 18) + .attr("y", yOffset) .attr("dy", "0.35em") .style("font-size", "0.875rem") .style("font-weight", "500") .style("fill", "#6B7280") .text(labelText); - // Calculate width for next item - var textWidth = this.getBBox().width; - xOffset += textWidth + 40; // Add spacing between items + yOffset += 22; }); // Render data lines diff --git a/app/assets/javascripts/kanaui/kiddo/charts/utils/mouse_over.js b/app/assets/javascripts/kanaui/kiddo/charts/utils/mouse_over.js index 30df0e4..e9b770c 100644 --- a/app/assets/javascripts/kanaui/kiddo/charts/utils/mouse_over.js +++ b/app/assets/javascripts/kanaui/kiddo/charts/utils/mouse_over.js @@ -59,15 +59,16 @@ var infoBox = info.node().getBBox(); var infoTitleBox = infoTitle.node().getBBox(); var margin = 40; + var minWidth = Math.max(box.width, infoTitleBox.width) + margin; info.attr("height", infoBox.height + box.height + 7); - if (infoBox.width < box.width) { - info.attr("width", box.width + margin); - infoTitleBg.attr("width", box.width + margin); + if (infoBox.width < minWidth) { + info.attr("width", minWidth); + infoTitleBg.attr("width", minWidth); $("#mouseover_canvas #info-title").attr( "dx", - (box.width + margin) / 2 - infoTitleBox.width / 2 + minWidth / 2 - infoTitleBox.width / 2 ); } }; @@ -81,7 +82,9 @@ }); function mousemove(event) { - var mouseX = d3.pointer(event, this)[0]; + var pointer = d3.pointer(event, this); + var mouseX = pointer[0]; + var mouseY = pointer[1]; $("#mouseover_canvas .chart_values").detach().remove(); $("#mouseover_canvas .chart_circles").detach().remove(); @@ -115,11 +118,7 @@ .attr("cy", y(d.y)) .style("fill", self.color(name)); - var canvasPosition = x(x0) > self.width / 2 ? 50 : self.width / 2; - - canvas.attr("transform", "translate(" + canvasPosition + ",0)"); - - canvas.select("#info-title").text(d.date); + canvas.select("#info-title").text(helper.formatDate(d.date)); elementsForLegend.push({ element: element, d: d }); }); @@ -155,6 +154,17 @@ addInfoDimensions(text); }); + + // Position tooltip to follow the cursor; flip left when near the right edge. + var tooltipOffset = 15; + var tooltipWidth = info.node().getBBox().width; + var tooltipHeight = info.node().getBBox().height; + var localX = (mouseX + tooltipOffset + tooltipWidth > self.width) + ? mouseX - tooltipWidth - tooltipOffset + : mouseX + tooltipOffset; + var canvasX = localX + self.margin_left; + var canvasY = Math.max(0, mouseY - 150); + canvas.attr("transform", "translate(" + canvasX + "," + canvasY + ")"); } }, }; diff --git a/app/assets/javascripts/kanaui/kiddo/helper.js b/app/assets/javascripts/kanaui/kiddo/helper.js index 8a38332..3d62d90 100644 --- a/app/assets/javascripts/kanaui/kiddo/helper.js +++ b/app/assets/javascripts/kanaui/kiddo/helper.js @@ -1,9 +1,11 @@ (function (Kiddo, d3) { Kiddo.Helper = function () { + var formatValue = function (d) { + return d % 1 === 0 ? d3.format(",d")(d) : d3.format(",.2f")(d); + }; var formatCurrency = function (d) { return "$" + formatValue(d); }; - var formatValue = d3.format(",.2f"); var humanizeSegment = function (segment) { segment = String(segment || "") @@ -30,33 +32,64 @@ .join(" "); }; - var formatSeriesName = function (name) { + var formatSeriesName = function (name, reportName) { return String(name || "") .split(/\s*::\s*/) + .filter(function (segment) { + // Remove numeric-only segments (tenant record id) + return !/^\d+$/.test(segment.trim()); + }) .map(function (segment) { var parts = segment.split(/\s*:\s*/); if (parts.length > 1) { - return humanizeSegment(parts[0]) + " (" + humanizeSegment(parts.slice(1).join(": ")) + ")"; + var label = (reportName && parts[0].trim().toLowerCase() === "count") + ? reportName + : humanizeSegment(parts[0]); + return label + " (" + humanizeSegment(parts.slice(1).join(": ")) + ")"; } - return humanizeSegment(segment); }) .filter(function (segment) { return segment.length > 0; }) - .join(" / "); + .join(" : "); + }; + + // Extracts just the qualifier part (e.g. "EUR" from "count: EUR :: 1") for compact tooltip labels + var formatSeriesLabel = function (name) { + var segments = String(name || "") + .split(/\s*::\s*/) + .filter(function (segment) { + return !/^\d+$/.test(segment.trim()); + }); + + if (segments.length === 0) return humanizeSegment(name); + + var parts = segments[0].split(/\s*:\s*/); + if (parts.length > 1) { + return humanizeSegment(parts.slice(1).join(": ")); + } + return humanizeSegment(segments[0]); + }; + + var parseDateFn = d3.timeParse("%Y-%m-%d"); + var formatDateFn = d3.timeFormat("%b %d, %Y"); + var formatDate = function (dateStr) { + var parsed = parseDateFn(dateStr); + return parsed ? formatDateFn(parsed) : dateStr; }; return { - parseDate: d3.timeParse("%Y-%m-%d"), + parseDate: parseDateFn, bisectDate: d3.bisector(function (d) { return d.x; }).left, formatCurrency: formatCurrency, formatValue: formatValue, formatSeriesName: formatSeriesName, + formatDate: formatDate, formatValueDisplay: function (name, d) { - return formatSeriesName(name) + ": " + formatValue(d.y); // Add currency boolean on backend later -- formatCurrency(d.y); } + return formatSeriesLabel(name) + ": " + formatValue(d.y); }, }; }; diff --git a/app/assets/javascripts/kanaui/kiddo/settings.js b/app/assets/javascripts/kanaui/kiddo/settings.js index c061ee1..15eddd6 100644 --- a/app/assets/javascripts/kanaui/kiddo/settings.js +++ b/app/assets/javascripts/kanaui/kiddo/settings.js @@ -19,6 +19,7 @@ raw_height: raw_height, width: raw_width - margin_left - margin_right, height: raw_height - margin_top - margin_bottom, + reportName: $("#chartAnchor").data("report-name") || "", }; }; })((window.Kiddo = window.Kiddo || {}), d3); diff --git a/app/assets/stylesheets/kanaui/kanaui.css b/app/assets/stylesheets/kanaui/kanaui.css index daf1344..65a6b2c 100644 --- a/app/assets/stylesheets/kanaui/kanaui.css +++ b/app/assets/stylesheets/kanaui/kanaui.css @@ -103,11 +103,40 @@ .kenui-analytics-dashboard-index .well ul { list-style-type: none; - margin-top: 0.625rem; + margin: 0.625rem 0 0; background: #fafafa; - width: fit-content; - padding: 0.625rem; + width: 100%; + padding: 1rem 1.25rem; border-radius: 0.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem 1.25rem; + align-items: flex-start; +} + +/* Smoothing option links (Weekly/Monthly average/sum) render as inline pills */ +.kenui-analytics-dashboard-index .well > ul > li.smoothing-option { + display: inline-flex; +} + +.kenui-analytics-dashboard-index .well > ul > li.smoothing-option > a { + display: inline-flex; + align-items: center; + background: #ffffff; + border: 0.0625rem solid #d5d7da; + border-radius: 999px; + padding: 0.375rem 0.75rem; + text-decoration: none; + color: #414651; + font-weight: 500; + font-size: 0.8125rem; + line-height: 1.25rem; + transition: all 0.15s ease-in-out; +} + +.kenui-analytics-dashboard-index .well > ul > li.smoothing-option > a:hover { + border-color: #1570ef; + color: #1570ef; } .kenui-analytics-dashboard-index .well ul li a { @@ -168,6 +197,26 @@ background: #ffffff; } +.kenui-analytics-dashboard-index .chart-title-container { + width: auto; + height: auto; + display: block; + justify-content: unset; + align-items: unset; + position: static; + margin-top: 15px; +} + +.kenui-analytics-dashboard-index .chart-title { + font-weight: 600; + font-size: 1.25rem; + line-height: 1.75rem; + color: #111827; + white-space: normal; + transform: none; + text-align: left; +} + .kenui-analytics-dashboard-index #chartAnchor svg { background: #ffffff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; @@ -619,20 +668,32 @@ .kenui-analytics-dashboard-index .advanced-controls { margin: 0.625rem 0; padding: 0; + flex: 1 1 100%; } .kenui-analytics-dashboard-index .advanced-controls .form-horizontal { - background: #fafafa; + background: #ffffff; border: 0.0625rem solid #e9eaeb; border-radius: 0.5rem; - padding: 0.625rem; + padding: 1rem 1.25rem; margin-top: 0.625rem; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem 1.5rem; + align-items: start; +} + +.kenui-analytics-dashboard-index .advanced-controls .form-horizontal > .form-group:last-of-type { + grid-column: 1 / -1; + justify-self: end; + margin-top: 0; } .kenui-analytics-dashboard-index .advanced-controls fieldset { border: none; margin: 0; padding: 0; + min-width: 0; } .kenui-analytics-dashboard-index .advanced-controls legend { @@ -647,10 +708,10 @@ } .kenui-analytics-dashboard-index .advanced-controls .form-group { - margin-bottom: 1rem; + margin-bottom: 0.75rem; display: flex; flex-direction: column; - align-items: center; + align-items: stretch; } .kenui-analytics-dashboard-index .advanced-controls .control-label { @@ -675,7 +736,7 @@ .kenui-analytics-dashboard-index .advanced-controls select.form-control { height: 6rem; - min-width: 16rem; + min-width: 0; width: 100%; } @@ -715,18 +776,19 @@ } /* Current Analytics Query Section */ -.kenui-analytics-dashboard-index .well ul li:has(.query-label) { - background: #f8f9fa; +.kenui-analytics-dashboard-index .well ul li.query-block { + background: #ffffff; border: 0.0625rem solid #e9eaeb; border-radius: 0.5rem; - padding: 0.625rem; - margin: 0.625rem 0; + padding: 0.75rem 1rem; + margin: 0; display: flex; flex-direction: column; - gap: 0.75rem; + gap: 0.5rem; + flex: 1 1 100%; } -.kenui-analytics-dashboard-index .well ul li:has(.query-label) a { +.kenui-analytics-dashboard-index .well ul li.query-block a { color: #1570ef; text-decoration: none; font-weight: 500; @@ -739,12 +801,12 @@ transition: color 0.15s ease-in-out; } -.kenui-analytics-dashboard-index .well ul li:has(.query-label) a:hover { +.kenui-analytics-dashboard-index .well ul li.query-block a:hover { color: #0d5bb8; text-decoration: underline; } -.kenui-analytics-dashboard-index .well ul li:has(.query-label) pre { +.kenui-analytics-dashboard-index .well ul li.query-block pre { background: #ffffff; border: 0.0625rem solid #d5d7da; border-radius: 0.375rem; @@ -760,7 +822,7 @@ box-shadow: 0 0.0625rem 0.1875rem rgba(0, 0, 0, 0.1); } -.kenui-analytics-dashboard-index .well ul li:has(.query-label) .query-label { +.kenui-analytics-dashboard-index .well ul li.query-block .query-label { font-weight: 500; font-size: 0.875rem; line-height: 1.25rem; @@ -770,6 +832,10 @@ /* Responsive adjustments for smaller screens */ @media (max-width: 768px) { + .kenui-analytics-dashboard-index .advanced-controls .form-horizontal { + grid-template-columns: 1fr; + } + .kenui-analytics-dashboard-index .advanced-controls .form-group { flex-direction: column; align-items: flex-start; @@ -785,7 +851,7 @@ width: 100%; } - .kenui-analytics-dashboard-index .well ul li:has(.query-label) pre { + .kenui-analytics-dashboard-index .well ul li.query-block pre { font-size: 0.75rem; padding: 0.5rem 0.75rem; } diff --git a/app/controllers/kanaui/dashboard_controller.rb b/app/controllers/kanaui/dashboard_controller.rb index 06b53da..74c13d5 100644 --- a/app/controllers/kanaui/dashboard_controller.rb +++ b/app/controllers/kanaui/dashboard_controller.rb @@ -146,7 +146,7 @@ def build_slice_and_dice_query groups[field_name] = params["group_#{field_name}"] end - filter_query = '' + filter_query = +'' filters.each do |k, v| next if v.blank? @@ -158,8 +158,7 @@ def build_slice_and_dice_query groups.each do |k, v| next if v.blank? - # TODO: Make "no other" configurable - query << "^dimension:#{k}(#{v.join('|')}|-)" + query << "^dimension:#{k}(#{v.join('|')})" end # Template variables diff --git a/app/helpers/kanaui/dashboard_helper.rb b/app/helpers/kanaui/dashboard_helper.rb index 38136b4..9c576d3 100644 --- a/app/helpers/kanaui/dashboard_helper.rb +++ b/app/helpers/kanaui/dashboard_helper.rb @@ -18,9 +18,15 @@ def available_reports(options = {}) end def reports(start_date, end_date, name, smooth, sql_only, format, options = {}) - path = "#{KILLBILL_ANALYTICS_PREFIX}/reports?format=#{format}&startDate=#{start_date}&endDate=#{end_date}&name=#{name}" - path = "#{path}&smooth=#{smooth}" if smooth - path = "#{path}&sqlOnly=true" if sql_only.present? + query = { + 'format' => format, + 'startDate' => start_date, + 'endDate' => end_date, + 'name' => name + } + query['smooth'] = smooth if smooth + query['sqlOnly'] = 'true' if sql_only.present? + path = "#{KILLBILL_ANALYTICS_PREFIX}/reports?#{query.to_query}" response = KillBillClient::API.get path, {}, options response.body end diff --git a/app/views/kanaui/dashboard/index.html.erb b/app/views/kanaui/dashboard/index.html.erb index 31ff95e..b257c06 100644 --- a/app/views/kanaui/dashboard/index.html.erb +++ b/app/views/kanaui/dashboard/index.html.erb @@ -150,6 +150,11 @@
<%= params[:name] -%>