pict-section-activitygraph 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-activitygraph",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Pict section that renders a stacked-bar activity timeline: one bar per period, stacked by type or actor, across parallel domains. Themed SVG, no chart dependency.",
|
|
5
5
|
"main": "source/Pict-Section-ActivityGraph.js",
|
|
6
6
|
"scripts": {
|
|
@@ -73,6 +73,7 @@ function _colorFor(pStackBy, pKey, pIndex)
|
|
|
73
73
|
/**
|
|
74
74
|
* pModel:
|
|
75
75
|
* { Period, StackBy ('ActivityType'|'ActorKind'), Measure ('EventCount'|'Volume'),
|
|
76
|
+
* LegendPosition ('top-left'|'top-right'|'bottom-left'|'bottom-right'|'none', default 'bottom-right'),
|
|
76
77
|
* Columns: [ { Key, Title, Accent } ], Buckets: [ { PeriodStart, PeriodKey, Domain, ActivityType, ActorKind, EventCount, Volume } ] }
|
|
77
78
|
*/
|
|
78
79
|
function buildSVG(pModel)
|
|
@@ -83,6 +84,8 @@ function buildSVG(pModel)
|
|
|
83
84
|
let tmpMeasure = (tmpModel.Measure === 'Volume') ? 'Volume' : 'EventCount';
|
|
84
85
|
let tmpColumns = Array.isArray(tmpModel.Columns) && tmpModel.Columns.length ? tmpModel.Columns : [{ Key: 'WorkItems', Title: 'Work Items', Accent: '#e0891e' }, { Key: 'Knowledge', Title: 'Knowledge', Accent: '#3f7fd0' }];
|
|
85
86
|
let tmpBuckets = Array.isArray(tmpModel.Buckets) ? tmpModel.Buckets : [];
|
|
87
|
+
let tmpLegendOptions = { 'top-left': 1, 'top-right': 1, 'bottom-left': 1, 'bottom-right': 1, 'none': 1 };
|
|
88
|
+
let tmpLegendPosition = tmpLegendOptions[tmpModel.LegendPosition] ? tmpModel.LegendPosition : 'bottom-right';
|
|
86
89
|
|
|
87
90
|
// --- geometry ---
|
|
88
91
|
const WIDTH = 940;
|
|
@@ -139,40 +142,67 @@ function buildSVG(pModel)
|
|
|
139
142
|
+ '<text x="' + (WIDTH / 2) + '" y="' + (tmpH / 2) + '" text-anchor="middle" font-family="sans-serif" font-size="14" fill="' + _C.muted + '">No activity in this range yet.</text></svg>';
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
// Legend placement is configurable (default bottom-right, out of the title's way). When it is not at
|
|
146
|
+
// the top, the chart body starts higher and grows a band at the bottom to hold it.
|
|
147
|
+
let tmpLegendTop = tmpLegendPosition.indexOf('top') === 0;
|
|
148
|
+
let tmpLegendRight = tmpLegendPosition.indexOf('right') >= 0;
|
|
149
|
+
let tmpShowLegend = (tmpLegendPosition !== 'none') && (tmpStackKeys.length > 0);
|
|
150
|
+
|
|
151
|
+
let tmpChartTop = TOP + ((tmpShowLegend && tmpLegendTop) ? LEGEND_H : 10);
|
|
152
|
+
const GROUP_H = 22; // a super-group (quarter/year) gets its own header band, so its label
|
|
153
|
+
// never shares a line with the per-row date labels.
|
|
154
|
+
// Count the super-group bands up front so the SVG height accounts for them.
|
|
155
|
+
let tmpGroupCount = 0;
|
|
156
|
+
let tmpScanGroup = null;
|
|
157
|
+
tmpPeriodOrder.forEach((pRow) => { let tmpG = _superGroup(pRow.PeriodStart, tmpPeriod); if (tmpG && tmpG !== tmpScanGroup) { tmpGroupCount += 1; tmpScanGroup = tmpG; } });
|
|
158
|
+
let tmpBodyBottom = tmpChartTop + tmpPeriodOrder.length * ROW_H + tmpGroupCount * GROUP_H;
|
|
159
|
+
let tmpHeight = tmpBodyBottom + ((tmpShowLegend && !tmpLegendTop) ? LEGEND_H : 0) + 16;
|
|
144
160
|
let tmpSVG = [];
|
|
145
|
-
tmpSVG.push('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + WIDTH + ' ' + tmpHeight + '" width="100%" role="img" aria-label="System activity, one bar per ' + _esc(tmpPeriod) + ' stacked by ' + _esc(tmpStackBy) + '" font-family="-apple-system, Segoe UI, Roboto, sans-serif">');
|
|
161
|
+
tmpSVG.push('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + WIDTH + ' ' + tmpHeight + '" width="100%" role="img" aria-label="System activity, one bar per ' + _esc(tmpPeriod) + ' stacked by ' + _esc(tmpStackBy) + ', each column scaled 0 to ' + tmpMaxTotal + '" font-family="-apple-system, Segoe UI, Roboto, sans-serif">');
|
|
146
162
|
|
|
147
|
-
//
|
|
148
|
-
let tmpLegendX = PAD_L;
|
|
163
|
+
// title (always top-left)
|
|
149
164
|
tmpSVG.push('<text x="0" y="' + (TOP - 22) + '" font-size="15" font-weight="600" fill="' + _C.text + '">System Activity</text>');
|
|
150
|
-
|
|
165
|
+
|
|
166
|
+
// legend: a swatch + label per stack key, placed per LegendPosition. The total width is measured
|
|
167
|
+
// first so a right-aligned legend ends flush with the chart's right edge.
|
|
168
|
+
if (tmpShowLegend)
|
|
151
169
|
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
170
|
+
let tmpItems = tmpStackKeys.map((pKey) => ({ Key: pKey, Color: tmpColorByKey[pKey], W: 16 + _esc(pKey).length * 6.8 }));
|
|
171
|
+
let tmpTotalW = tmpItems.reduce((pSum, pItem) => pSum + pItem.W + 16, 0) - 16;
|
|
172
|
+
let tmpLX = tmpLegendRight ? (WIDTH - PAD_R - tmpTotalW) : PAD_L;
|
|
173
|
+
let tmpSwatchY = tmpLegendTop ? (TOP - 4) : (tmpBodyBottom + 14);
|
|
174
|
+
let tmpTextY = tmpLegendTop ? (TOP + 5) : (tmpBodyBottom + 23);
|
|
175
|
+
tmpItems.forEach((pItem) =>
|
|
176
|
+
{
|
|
177
|
+
tmpSVG.push('<rect x="' + tmpLX.toFixed(1) + '" y="' + tmpSwatchY + '" width="11" height="11" rx="2" fill="' + pItem.Color + '"/>');
|
|
178
|
+
tmpSVG.push('<text x="' + (tmpLX + 16).toFixed(1) + '" y="' + tmpTextY + '" font-size="12" fill="' + _C.muted + '">' + _esc(pItem.Key) + '</text>');
|
|
179
|
+
tmpLX += pItem.W + 16;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
156
182
|
|
|
157
|
-
// column headers
|
|
183
|
+
// column headers: accent-colored title (left) + the axis scale (right). Both columns share one global
|
|
184
|
+
// max, so labeling each track's full-width value with the same number makes the shared, comparable
|
|
185
|
+
// 0-to-max scale explicit -- a track that fills the whole width equals that max.
|
|
158
186
|
tmpColumns.forEach((pCol, pIndex) =>
|
|
159
187
|
{
|
|
160
188
|
let tmpX = tmpColX[pIndex];
|
|
161
189
|
tmpSVG.push('<text x="' + tmpX + '" y="' + (tmpChartTop - 8) + '" font-size="13" font-weight="600" fill="' + (pCol.Accent || _C.text) + '">' + _esc(pCol.Title || pCol.Key) + '</text>');
|
|
162
190
|
tmpSVG.push('<rect x="' + tmpX + '" y="' + (tmpChartTop - 4) + '" width="' + Math.max(40, tmpColWidth * 0.4) + '" height="2" fill="' + (pCol.Accent || _C.border) + '"/>');
|
|
191
|
+
tmpSVG.push('<text x="' + (tmpX + tmpBarMax).toFixed(1) + '" y="' + (tmpChartTop - 8) + '" text-anchor="end" font-size="10" fill="' + _C.muted + '">0 to ' + tmpMaxTotal + '</text>');
|
|
163
192
|
});
|
|
164
193
|
|
|
165
|
-
// rows
|
|
194
|
+
// rows (running y, so a super-group header band pushes the rows below it down instead of overlapping)
|
|
166
195
|
let tmpPrevGroup = null;
|
|
167
|
-
|
|
196
|
+
let tmpY = tmpChartTop;
|
|
197
|
+
tmpPeriodOrder.forEach((pPeriodRow) =>
|
|
168
198
|
{
|
|
169
|
-
let tmpY = tmpChartTop + pRowIndex * ROW_H;
|
|
170
199
|
let tmpGroup = _superGroup(pPeriodRow.PeriodStart, tmpPeriod);
|
|
171
200
|
if (tmpGroup && tmpGroup !== tmpPrevGroup)
|
|
172
201
|
{
|
|
173
|
-
//
|
|
174
|
-
tmpSVG.push('<line x1="0" y1="' + (tmpY
|
|
175
|
-
tmpSVG.push('<text x="0" y="' + (tmpY +
|
|
202
|
+
// A band of its own: a boundary line, then the group label on its own line above the rows.
|
|
203
|
+
tmpSVG.push('<line x1="0" y1="' + (tmpY + 7) + '" x2="' + (WIDTH - PAD_R) + '" y2="' + (tmpY + 7) + '" stroke="' + _C.border + '" stroke-width="1"/>');
|
|
204
|
+
tmpSVG.push('<text x="0" y="' + (tmpY + 19) + '" font-size="11" font-weight="600" fill="' + _C.muted + '">' + _esc(tmpGroup) + '</text>');
|
|
205
|
+
tmpY += GROUP_H;
|
|
176
206
|
tmpPrevGroup = tmpGroup;
|
|
177
207
|
}
|
|
178
208
|
// period label
|
|
@@ -198,6 +228,7 @@ function buildSVG(pModel)
|
|
|
198
228
|
tmpSVG.push('<text x="' + (tmpX + 6).toFixed(1) + '" y="' + (tmpY + BAR_H) + '" font-size="11" font-weight="600" fill="' + _C.text + '">' + tmpCell.Total + '</text>');
|
|
199
229
|
}
|
|
200
230
|
});
|
|
231
|
+
tmpY += ROW_H;
|
|
201
232
|
});
|
|
202
233
|
|
|
203
234
|
tmpSVG.push('</svg>');
|
|
@@ -25,6 +25,7 @@ const _DEFAULTS =
|
|
|
25
25
|
Periods: ['Day', 'Week', 'Month', 'Quarter', 'Year'],
|
|
26
26
|
StackBy: 'ActivityType',
|
|
27
27
|
Measure: 'EventCount',
|
|
28
|
+
LegendPosition: 'bottom-right',
|
|
28
29
|
Columns: [
|
|
29
30
|
{ Key: 'WorkItems', Title: 'Work Items', Accent: '#e0891e' },
|
|
30
31
|
{ Key: 'Knowledge', Title: 'Knowledge', Accent: '#3f7fd0' }
|
|
@@ -71,6 +72,7 @@ class PictViewActivityGraph extends libPictView
|
|
|
71
72
|
Periods: Array.isArray(tmpConfig.Periods) ? tmpConfig.Periods : _DEFAULTS.Periods,
|
|
72
73
|
StackBy: tmpConfig.StackBy || _DEFAULTS.StackBy,
|
|
73
74
|
Measure: tmpConfig.Measure || _DEFAULTS.Measure,
|
|
75
|
+
LegendPosition: tmpConfig.LegendPosition || _DEFAULTS.LegendPosition,
|
|
74
76
|
Columns: Array.isArray(tmpConfig.Columns) ? tmpConfig.Columns : _DEFAULTS.Columns,
|
|
75
77
|
Buckets: []
|
|
76
78
|
};
|
|
@@ -146,6 +148,7 @@ class PictViewActivityGraph extends libPictView
|
|
|
146
148
|
Period: this._State.Period,
|
|
147
149
|
StackBy: this._State.StackBy,
|
|
148
150
|
Measure: this._State.Measure,
|
|
151
|
+
LegendPosition: this._State.LegendPosition,
|
|
149
152
|
Columns: this._State.Columns,
|
|
150
153
|
Buckets: this._State.Buckets
|
|
151
154
|
});
|