|
|
@@ -24,6 +24,10 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
private static final String TEMPLATE_RESOURCE = "templates/outlook-exam-sprint-report-template.html";
|
|
|
private static final String DEFAULT_THEME_COLOR = "#448aff";
|
|
|
private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$");
|
|
|
+ private static final int CHART_AXIS_LEFT = 34;
|
|
|
+ private static final int CHART_AXIS_TOP = 50;
|
|
|
+ private static final int CHART_AXIS_BOTTOM = 180;
|
|
|
+ private static final int CHART_AXIS_HEIGHT = CHART_AXIS_BOTTOM - CHART_AXIS_TOP;
|
|
|
|
|
|
private final ObjectMapper objectMapper;
|
|
|
|
|
|
@@ -92,9 +96,9 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
}
|
|
|
|
|
|
private String renderPastPaperVocabularyChart(OutlookExamSprintReportPayload.PastPaperVocabularyChart chart) {
|
|
|
- int maxValue = Math.max(chart.totalWordCount(), chart.unknownWordCountBeforeSprint());
|
|
|
- int totalHeight = barHeight(chart.totalWordCount(), maxValue);
|
|
|
- int unknownHeight = barHeight(chart.unknownWordCountBeforeSprint(), maxValue);
|
|
|
+ int axisMax = roundUpToStep(Math.max(chart.totalWordCount(), chart.unknownWordCountBeforeSprint()), 250);
|
|
|
+ int totalHeight = barHeight(chart.totalWordCount(), axisMax);
|
|
|
+ int unknownHeight = barHeight(chart.unknownWordCountBeforeSprint(), axisMax);
|
|
|
double beforePercent = percentage(chart.unknownWordCountBeforeSprint(), chart.totalWordCount());
|
|
|
double afterPercent = percentage(chart.unknownWordCountAfterSprint(), chart.totalWordCount());
|
|
|
|
|
|
@@ -104,9 +108,11 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
.append("<div class='chart-box'>")
|
|
|
.append("<svg class='past-paper-column-chart' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 220' role='img' aria-label='真题试卷词汇掌握情况'>")
|
|
|
.append(renderChartAxes(320))
|
|
|
- .append("<rect class='chart-column total-column' x='84' y='").append(180 - totalHeight)
|
|
|
+ .append(renderYAxisTicks(axisMax, 250))
|
|
|
+ .append(renderXAxisTickMarks(112, 208))
|
|
|
+ .append("<rect class='chart-column total-column' x='84' y='").append(CHART_AXIS_BOTTOM - totalHeight)
|
|
|
.append("' width='56' height='").append(totalHeight).append("' rx='8' ry='8' fill='#448aff'/>")
|
|
|
- .append("<rect class='chart-column unknown-column' x='180' y='").append(180 - unknownHeight)
|
|
|
+ .append("<rect class='chart-column unknown-column' x='180' y='").append(CHART_AXIS_BOTTOM - unknownHeight)
|
|
|
.append("' width='56' height='").append(unknownHeight).append("' rx='8' ry='8' fill='#ff9800'/>")
|
|
|
.append("<text class='chart-caption' x='112' y='198' text-anchor='middle'>总词量</text>")
|
|
|
.append("<text class='chart-caption' x='208' y='198' text-anchor='middle'>生词量</text>")
|
|
|
@@ -126,8 +132,9 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
}
|
|
|
|
|
|
private String renderHighFrequencyVocabularyChart(OutlookExamSprintReportPayload.HighFrequencyVocabularyChart chart) {
|
|
|
- int basicHeight = barHeight(chart.basicCorePercent(), 100);
|
|
|
- int highScoreHeight = barHeight(chart.highScorePercent(), 100);
|
|
|
+ int axisMax = 100;
|
|
|
+ int basicHeight = barHeight(chart.basicCorePercent(), axisMax);
|
|
|
+ int highScoreHeight = barHeight(chart.highScorePercent(), axisMax);
|
|
|
|
|
|
return new StringBuilder()
|
|
|
.append("<div class='card'>")
|
|
|
@@ -135,9 +142,11 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
.append("<div class='chart-box'>")
|
|
|
.append("<svg class='high-frequency-column-chart' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 220' role='img' aria-label='常考词汇掌握情况'>")
|
|
|
.append(renderChartAxes(320))
|
|
|
- .append("<rect class='chart-column basic-core-column' x='84' y='").append(180 - basicHeight)
|
|
|
+ .append(renderYAxisTicks(axisMax, 20))
|
|
|
+ .append(renderXAxisTickMarks(112, 208))
|
|
|
+ .append("<rect class='chart-column basic-core-column' x='84' y='").append(CHART_AXIS_BOTTOM - basicHeight)
|
|
|
.append("' width='56' height='").append(basicHeight).append("' rx='8' ry='8' fill='#42a5f5'/>")
|
|
|
- .append("<rect class='chart-column high-score-column' x='180' y='").append(180 - highScoreHeight)
|
|
|
+ .append("<rect class='chart-column high-score-column' x='180' y='").append(CHART_AXIS_BOTTOM - highScoreHeight)
|
|
|
.append("' width='56' height='").append(highScoreHeight).append("' rx='8' ry='8' fill='#66bb6a'/>")
|
|
|
.append("<text class='chart-caption' x='112' y='198' text-anchor='middle'>基础必会词</text>")
|
|
|
.append("<text class='chart-caption' x='208' y='198' text-anchor='middle'>高分拉分词</text>")
|
|
|
@@ -158,14 +167,17 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
.mapToDouble(OutlookExamSprintReportPayload.VocabularyFrequencyBar::currentValue)
|
|
|
.max()
|
|
|
.orElse(1d);
|
|
|
- int maxScaled = (int) Math.ceil(highest * 10d);
|
|
|
+ int axisMax = roundUpToStep((int) Math.ceil(highest), 50);
|
|
|
+ int maxScaled = axisMax * 10;
|
|
|
|
|
|
StringBuilder builder = new StringBuilder();
|
|
|
builder.append("<div class='card'>")
|
|
|
.append("<h3 class='card-title'>词频区间掌握度</h3>")
|
|
|
.append("<div class='chart-box'>")
|
|
|
.append("<svg class='frequency-band-column-chart' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 360 220' role='img' aria-label='词频区间掌握度'>")
|
|
|
- .append(renderChartAxes(360));
|
|
|
+ .append(renderChartAxes(360))
|
|
|
+ .append(renderYAxisTicks(axisMax, 50))
|
|
|
+ .append(renderXAxisTickMarks(97, 187, 277));
|
|
|
|
|
|
int[] xPositions = {70, 160, 250};
|
|
|
String[] columnClasses = {"high-band-column", "mid-band-column", "low-band-column"};
|
|
|
@@ -175,7 +187,7 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
int x = xPositions[index];
|
|
|
|
|
|
builder.append("<rect class='chart-column ").append(columnClasses[index]).append("' x='")
|
|
|
- .append(x).append("' y='").append(180 - height).append("' width='54' height='")
|
|
|
+ .append(x).append("' y='").append(CHART_AXIS_BOTTOM - height).append("' width='54' height='")
|
|
|
.append(height).append("' rx='8' ry='8' fill='").append(safeColor(bar.themeColor())).append("'/>")
|
|
|
.append("<text class='donut-label-text' x='").append(x + 27).append("' y='198' text-anchor='middle'>")
|
|
|
.append(escape(bar.bandLabel())).append("</text>")
|
|
|
@@ -305,12 +317,52 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
|
|
|
private String renderChartAxes(int width) {
|
|
|
return new StringBuilder()
|
|
|
- .append("<line class='chart-axis' x1='34' y1='18' x2='34' y2='180' stroke='#9aa6b2' stroke-width='1'/>")
|
|
|
- .append("<line class='chart-axis' x1='34' y1='180' x2='").append(width - 24)
|
|
|
- .append("' y2='180' stroke='#9aa6b2' stroke-width='1'/>")
|
|
|
+ .append("<line class='chart-axis' x1='").append(CHART_AXIS_LEFT).append("' y1='18' x2='")
|
|
|
+ .append(CHART_AXIS_LEFT).append("' y2='").append(CHART_AXIS_BOTTOM)
|
|
|
+ .append("' stroke='#9aa6b2' stroke-width='1'/>")
|
|
|
+ .append("<line class='chart-axis' x1='").append(CHART_AXIS_LEFT).append("' y1='")
|
|
|
+ .append(CHART_AXIS_BOTTOM).append("' x2='").append(width - 24)
|
|
|
+ .append("' y2='").append(CHART_AXIS_BOTTOM).append("' stroke='#9aa6b2' stroke-width='1'/>")
|
|
|
.toString();
|
|
|
}
|
|
|
|
|
|
+ private String renderYAxisTicks(int axisMax, int tickStep) {
|
|
|
+ if (axisMax <= 0 || tickStep <= 0) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ StringBuilder builder = new StringBuilder();
|
|
|
+ for (int tickValue = 0; tickValue <= axisMax; tickValue += tickStep) {
|
|
|
+ int y = CHART_AXIS_BOTTOM - (int) Math.round((tickValue / (double) axisMax) * CHART_AXIS_HEIGHT);
|
|
|
+ builder.append("<line class='chart-axis-tick chart-axis-tick-y' x1='")
|
|
|
+ .append(CHART_AXIS_LEFT - 4).append("' y1='").append(y)
|
|
|
+ .append("' x2='").append(CHART_AXIS_LEFT).append("' y2='").append(y)
|
|
|
+ .append("' stroke='#9aa6b2' stroke-width='1'/>")
|
|
|
+ .append("<text class='chart-axis-tick-label chart-axis-tick-label-y' x='")
|
|
|
+ .append(CHART_AXIS_LEFT - 8).append("' y='").append(y + 4)
|
|
|
+ .append("' text-anchor='end' fill='#7f8b97' font-size='11'>")
|
|
|
+ .append(tickValue)
|
|
|
+ .append("</text>");
|
|
|
+ }
|
|
|
+ return builder.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String renderXAxisTickMarks(int... xCenters) {
|
|
|
+ if (xCenters == null || xCenters.length == 0) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ StringBuilder builder = new StringBuilder();
|
|
|
+ for (int xCenter : xCenters) {
|
|
|
+ builder.append("<line class='chart-axis-tick chart-axis-tick-x' x1='")
|
|
|
+ .append(xCenter).append("' y1='").append(CHART_AXIS_BOTTOM)
|
|
|
+ .append("' x2='").append(xCenter).append("' y2='")
|
|
|
+ .append(CHART_AXIS_BOTTOM + 5)
|
|
|
+ .append("' stroke='#9aa6b2' stroke-width='1'/>");
|
|
|
+ }
|
|
|
+ return builder.toString();
|
|
|
+ }
|
|
|
+
|
|
|
private int barHeight(int value, int maxValue) {
|
|
|
if (maxValue <= 0) {
|
|
|
return 0;
|
|
|
@@ -319,6 +371,16 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
|
|
|
return Math.max(16, (int) Math.round(ratio * 130));
|
|
|
}
|
|
|
|
|
|
+ private int roundUpToStep(int value, int step) {
|
|
|
+ if (value <= 0) {
|
|
|
+ return step;
|
|
|
+ }
|
|
|
+ if (step <= 0) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ return ((value + step - 1) / step) * step;
|
|
|
+ }
|
|
|
+
|
|
|
private double percentage(int numerator, int denominator) {
|
|
|
if (denominator <= 0) {
|
|
|
return 0d;
|