@rowsncolumns/charts 28.1.11 → 28.1.13
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/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/basic-chart/basic-chart.js +462 -286
- package/dist/cjs/basic-chart/helpers.d.ts.map +1 -1
- package/dist/cjs/basic-chart/helpers.js +2 -0
- package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/plotly/basic-chart/basic-chart.js +198 -149
- package/dist/cjs/plotly/basic-chart/helpers.d.ts.map +1 -1
- package/dist/cjs/plotly/basic-chart/helpers.js +2 -0
- package/dist/cjs/utils/hierarchy.d.ts +6 -0
- package/dist/cjs/utils/hierarchy.d.ts.map +1 -1
- package/dist/cjs/utils/hierarchy.js +23 -1
- package/dist/cjs/utils/histogram.d.ts +42 -3
- package/dist/cjs/utils/histogram.d.ts.map +1 -1
- package/dist/cjs/utils/histogram.js +193 -5
- package/dist/cjs/utils/overrides.d.ts +1 -1
- package/dist/cjs/utils/overrides.d.ts.map +1 -1
- package/dist/cjs/utils/overrides.js +44 -39
- package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/basic-chart/basic-chart.js +297 -109
- package/dist/esm/basic-chart/helpers.d.ts.map +1 -1
- package/dist/esm/basic-chart/helpers.js +2 -0
- package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/plotly/basic-chart/basic-chart.js +132 -70
- package/dist/esm/plotly/basic-chart/helpers.d.ts.map +1 -1
- package/dist/esm/plotly/basic-chart/helpers.js +2 -0
- package/dist/esm/utils/hierarchy.d.ts +6 -0
- package/dist/esm/utils/hierarchy.d.ts.map +1 -1
- package/dist/esm/utils/hierarchy.js +21 -0
- package/dist/esm/utils/histogram.d.ts +42 -3
- package/dist/esm/utils/histogram.d.ts.map +1 -1
- package/dist/esm/utils/histogram.js +171 -4
- package/dist/esm/utils/overrides.d.ts +1 -1
- package/dist/esm/utils/overrides.d.ts.map +1 -1
- package/dist/esm/utils/overrides.js +4 -2
- package/package.json +2 -2
|
@@ -23,12 +23,12 @@ import { applyEmptyCellsPolicy, connectsAcrossEmptyCells, isolatedPointIndexes,
|
|
|
23
23
|
import { getEchartsGlState, loadEchartsGl, } from "../utils/echarts-gl-loader";
|
|
24
24
|
import { echartsGroupedCategoryAxes, resolveCategoryLevels, } from "../utils/category-levels";
|
|
25
25
|
import { gradientToEcharts } from "../utils/gradient";
|
|
26
|
-
import { buildHierarchyTree, extractHierarchyRows, getHierarchyHeaderTitle, } from "../utils/hierarchy";
|
|
27
|
-
import { binHistogram, computePareto, isParetoSpec, } from "../utils/histogram";
|
|
26
|
+
import { buildHierarchyTree, contrastTextColor, extractHierarchyRows, getHierarchyHeaderTitle, } from "../utils/hierarchy";
|
|
27
|
+
import { aggregateByCategory, binHistogram, computePareto, isParetoSpec, } from "../utils/histogram";
|
|
28
28
|
import { categoryAxisNameGap, legendLabelsFrom, reserveEchartsLegendBand, } from "../utils/legend-layout";
|
|
29
29
|
import { clampSecondPieSize, isOfPieChartSpec, otherSliceHalfAngle, resolveOfPieType, splitOfPie, } from "../utils/of-pie";
|
|
30
30
|
import { clampDoughnutHole, doughnutRings } from "../utils/doughnut";
|
|
31
|
-
import { applyAxisOverrides, applyLegendOverrides, buildSeriesLabelOption, resolveSeriesDataLabels, } from "../utils/overrides";
|
|
31
|
+
import { applyAxisOverrides, dataLabelFormatter, applyLegendOverrides, buildSeriesLabelOption, resolveSeriesDataLabels, } from "../utils/overrides";
|
|
32
32
|
import { buildRadarIndicators, buildRadarSeriesData } from "../utils/radar";
|
|
33
33
|
import { buildRegionMapData, buildSequentialColorRamp, isRegionMapSpec, resolveGeoScope, warnOnceForUnmatchedRegions, } from "../utils/region-map";
|
|
34
34
|
import { loadRegionMapGeo } from "../utils/region-map-loader";
|
|
@@ -116,7 +116,14 @@ const HIERARCHY_LEVEL_STYLES = [
|
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
itemStyle: {
|
|
119
|
-
gapWidth:
|
|
119
|
+
gapWidth: 2,
|
|
120
|
+
borderColor: "#ffffff",
|
|
121
|
+
borderWidth: 1,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
itemStyle: {
|
|
126
|
+
gapWidth: 0,
|
|
120
127
|
borderColor: "#ffffff",
|
|
121
128
|
borderWidth: 1,
|
|
122
129
|
},
|
|
@@ -139,14 +146,22 @@ const treemapBannerShown = (layout) => layout !== "none";
|
|
|
139
146
|
* Colour a hierarchy like Excel: each top-level node takes the next
|
|
140
147
|
* palette colour and every descendant inherits it (#1072).
|
|
141
148
|
*/
|
|
142
|
-
const colorHierarchyNodes = (nodes, palette) => nodes.map((node, index) => {
|
|
149
|
+
const colorHierarchyNodes = (nodes, palette, options = {}) => nodes.map((node, index) => {
|
|
143
150
|
const color = chartPaletteColorAt(palette, index);
|
|
144
|
-
const paint = (n) => ({
|
|
151
|
+
const paint = (n, depth) => ({
|
|
145
152
|
...n,
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
// A top-level node's "border" is what echarts paints behind its
|
|
154
|
+
// banner strip: fill it with the category colour and write the name
|
|
155
|
+
// in a contrasting colour, like Excel's banner (#1086).
|
|
156
|
+
itemStyle: depth === 0 && options.banner
|
|
157
|
+
? { color, borderColor: color }
|
|
158
|
+
: { color },
|
|
159
|
+
...(depth === 0 && options.banner
|
|
160
|
+
? { upperLabel: { color: contrastTextColor(color) } }
|
|
161
|
+
: {}),
|
|
162
|
+
...(n.children ? { children: n.children.map((c) => paint(c, depth + 1)) } : {}),
|
|
148
163
|
});
|
|
149
|
-
return paint(node);
|
|
164
|
+
return paint(node, 0);
|
|
150
165
|
});
|
|
151
166
|
/** Sunburst rings: Excel leaves the centre empty, so even the first level is a ring (#1073). */
|
|
152
167
|
const SUNBURST_RADIUS = ["15%", "80%"];
|
|
@@ -759,6 +774,27 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
759
774
|
resolveRange: (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy),
|
|
760
775
|
});
|
|
761
776
|
});
|
|
777
|
+
const bubbleLegend = applyLegendOverrides({
|
|
778
|
+
...legend,
|
|
779
|
+
show: preparedSeries.length > 1,
|
|
780
|
+
data: preparedSeries.map((entry) => entry.name),
|
|
781
|
+
}, spec.legend);
|
|
782
|
+
const bubbleBand = reserveEchartsLegendBand({
|
|
783
|
+
position: spec.legend?.position,
|
|
784
|
+
visible: bubbleLegend.show !== false,
|
|
785
|
+
labels: preparedSeries.map((entry) => entry.name),
|
|
786
|
+
legendFontSize: bubbleLegend.textStyle?.fontSize ??
|
|
787
|
+
spec.legend?.textFormat?.fontSize ??
|
|
788
|
+
12,
|
|
789
|
+
legendItemWidth: defaultLegendConfig.itemWidth,
|
|
790
|
+
width: width ?? 0,
|
|
791
|
+
height: height ?? 0,
|
|
792
|
+
hasTitle: Boolean(explicitChartTitle),
|
|
793
|
+
titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
|
|
794
|
+
hasBottomAxisTitle: Boolean(spec.horizontalAxisTitle),
|
|
795
|
+
bottomAxisNameGap: 30,
|
|
796
|
+
hasLeftAxisTitle: Boolean(spec.verticalAxisTitle),
|
|
797
|
+
});
|
|
762
798
|
chartRef.current?.setOption({
|
|
763
799
|
xAxis: [
|
|
764
800
|
applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
|
|
@@ -784,17 +820,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
784
820
|
]
|
|
785
821
|
: []),
|
|
786
822
|
],
|
|
787
|
-
legend:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
}, spec.legend),
|
|
792
|
-
grid: {
|
|
793
|
-
left: "3%",
|
|
794
|
-
right: "4%",
|
|
795
|
-
bottom: "3%",
|
|
796
|
-
containLabel: true,
|
|
797
|
-
},
|
|
823
|
+
legend: { ...bubbleLegend, ...bubbleBand.legend },
|
|
824
|
+
// The plot shrinks to leave the legend its own band, as the
|
|
825
|
+
// category charts do (#1089).
|
|
826
|
+
grid: bubbleBand.grid,
|
|
798
827
|
tooltip: {
|
|
799
828
|
trigger: "item",
|
|
800
829
|
formatter: tooltipFormatter,
|
|
@@ -834,13 +863,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
834
863
|
theme.themeColors.accent5,
|
|
835
864
|
theme.themeColors.accent6,
|
|
836
865
|
]);
|
|
837
|
-
const
|
|
866
|
+
const isTreemap = spec.chartType === "treemap";
|
|
867
|
+
const banner = isTreemap && treemapBannerShown(spec.parentLabelLayout);
|
|
868
|
+
const hierarchyData = colorHierarchyNodes(buildHierarchyTree(hierarchyRows), hierarchyPalette, { banner });
|
|
838
869
|
const rootName = hierarchyData.length === 1 ? hierarchyData[0]?.name ?? "" : "";
|
|
839
870
|
const drillDown = Boolean(spec.drillDown);
|
|
840
871
|
const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
|
|
841
872
|
const hierarchyDepth = getHierarchyDepth(hierarchyData);
|
|
842
|
-
const isTreemap = spec.chartType === "treemap";
|
|
843
|
-
const banner = isTreemap && treemapBannerShown(spec.parentLabelLayout);
|
|
844
873
|
const levels = buildHierarchyLevels(hierarchyDepth, {
|
|
845
874
|
treemap: isTreemap,
|
|
846
875
|
banner,
|
|
@@ -873,12 +902,45 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
873
902
|
: params.value ?? "";
|
|
874
903
|
return `${params.name}: ${formatAxisValue(sizeNumberFormat, value, locale)}`;
|
|
875
904
|
};
|
|
876
|
-
//
|
|
877
|
-
//
|
|
905
|
+
// Data labels: a treemap block's label always sits in its bottom-left
|
|
906
|
+
// corner (the bar placements mean nothing on a block) with each part
|
|
907
|
+
// — name, value — on its own line; `{ show: false }` turns the labels
|
|
908
|
+
// off entirely (#1086). Without `dataLabels` the block shows its name.
|
|
878
909
|
const hierarchyDataLabels = spec.dataLabels;
|
|
879
|
-
const
|
|
910
|
+
const hierarchyLabelOpts = hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
|
|
880
911
|
? { ...hierarchyDataLabels, numberFormatPattern: sizeNumberFormat }
|
|
881
|
-
: hierarchyDataLabels
|
|
912
|
+
: hierarchyDataLabels;
|
|
913
|
+
const hierarchyLabel = hierarchyLabelOpts?.show === false
|
|
914
|
+
? { show: false }
|
|
915
|
+
: hierarchyLabelOpts?.show
|
|
916
|
+
? {
|
|
917
|
+
show: true,
|
|
918
|
+
formatter: dataLabelFormatter(hierarchyLabelOpts, locale, isTreemap ? "\n" : ": "),
|
|
919
|
+
...(isTreemap ? { position: "insideBottomLeft" } : {}),
|
|
920
|
+
}
|
|
921
|
+
: {
|
|
922
|
+
show: true,
|
|
923
|
+
formatter: "{b}",
|
|
924
|
+
...(isTreemap ? { position: "insideBottomLeft" } : {}),
|
|
925
|
+
};
|
|
926
|
+
// Reserve a band for the legend (and the title) so neither paints
|
|
927
|
+
// over the blocks (#1086), as the cartesian layout does.
|
|
928
|
+
const hierarchyBand = reserveEchartsLegendBand({
|
|
929
|
+
position: spec.legend?.position,
|
|
930
|
+
visible: legendShown && hierarchyData.length > 0,
|
|
931
|
+
labels: hierarchyData.map((node) => node.name),
|
|
932
|
+
legendFontSize: hierarchyLegend.textStyle?.fontSize ??
|
|
933
|
+
spec.legend?.textFormat?.fontSize ??
|
|
934
|
+
12,
|
|
935
|
+
legendItemWidth: defaultLegendConfig.itemWidth,
|
|
936
|
+
width: width ?? 0,
|
|
937
|
+
height: height ?? 0,
|
|
938
|
+
hasTitle: Boolean(explicitChartTitle ?? headerTitle),
|
|
939
|
+
titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
|
|
940
|
+
hasBottomAxisTitle: false,
|
|
941
|
+
bottomAxisNameGap: 0,
|
|
942
|
+
hasLeftAxisTitle: false,
|
|
943
|
+
});
|
|
882
944
|
chartRef.current?.setOption({
|
|
883
945
|
xAxis: [
|
|
884
946
|
{
|
|
@@ -894,6 +956,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
894
956
|
],
|
|
895
957
|
legend: {
|
|
896
958
|
...hierarchyLegend,
|
|
959
|
+
...hierarchyBand.legend,
|
|
897
960
|
data: hierarchyData.map((node) => node.name),
|
|
898
961
|
selectedMode: false,
|
|
899
962
|
},
|
|
@@ -913,16 +976,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
913
976
|
name: seriesDisplayName,
|
|
914
977
|
rootName: rootName || seriesDisplayName || "",
|
|
915
978
|
data: hierarchyData,
|
|
916
|
-
label:
|
|
917
|
-
show: true,
|
|
918
|
-
formatter: "{b}",
|
|
919
|
-
...(isTreemap ? { position: "insideBottomLeft" } : {}),
|
|
920
|
-
...(hierarchySeriesLabel ?? {}),
|
|
921
|
-
},
|
|
979
|
+
label: hierarchyLabel,
|
|
922
980
|
// Only `levels[1]` (the categories) may carry a banner; see
|
|
923
981
|
// buildHierarchyLevels.
|
|
924
982
|
upperLabel: isTreemap ? { show: false } : undefined,
|
|
925
983
|
levels,
|
|
984
|
+
...(isTreemap
|
|
985
|
+
? {
|
|
986
|
+
// Excel has no wheel zoom / drag pan on a treemap; the
|
|
987
|
+
// sheet keeps scrolling under the pointer (#1086).
|
|
988
|
+
roam: drillDown,
|
|
989
|
+
left: hierarchyBand.grid.left,
|
|
990
|
+
right: hierarchyBand.grid.right,
|
|
991
|
+
top: hierarchyBand.grid.top,
|
|
992
|
+
bottom: hierarchyBand.grid.bottom,
|
|
993
|
+
}
|
|
994
|
+
: {}),
|
|
926
995
|
// Excel has no drill-down on either chart: zooming into a
|
|
927
996
|
// branch on click (and the treemap breadcrumb) is opt-in.
|
|
928
997
|
nodeClick: drillDown
|
|
@@ -1330,8 +1399,44 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1330
1399
|
const lines = indicators.map((indicator, i) => `${indicator.name}: ${formatAxisValue(pattern, values[i], locale)}`);
|
|
1331
1400
|
return [params?.name ?? "", ...lines].filter(Boolean).join("<br/>");
|
|
1332
1401
|
};
|
|
1402
|
+
const radarLegend = applyLegendOverrides({
|
|
1403
|
+
...legend,
|
|
1404
|
+
show: radarData.length > 1,
|
|
1405
|
+
data: radarData.map((d) => d.name),
|
|
1406
|
+
}, spec.legend);
|
|
1407
|
+
// The circle moves and shrinks to leave the legend (and the title)
|
|
1408
|
+
// their own band instead of being drawn over (#1089).
|
|
1409
|
+
const radarBand = reserveEchartsLegendBand({
|
|
1410
|
+
position: spec.legend?.position,
|
|
1411
|
+
visible: radarLegend.show !== false,
|
|
1412
|
+
labels: radarData.map((d) => d.name),
|
|
1413
|
+
legendFontSize: radarLegend.textStyle?.fontSize ??
|
|
1414
|
+
spec.legend?.textFormat?.fontSize ??
|
|
1415
|
+
12,
|
|
1416
|
+
legendItemWidth: defaultLegendConfig.itemWidth,
|
|
1417
|
+
width: width ?? 0,
|
|
1418
|
+
height: height ?? 0,
|
|
1419
|
+
hasTitle: Boolean(explicitChartTitle),
|
|
1420
|
+
titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
|
|
1421
|
+
hasBottomAxisTitle: false,
|
|
1422
|
+
bottomAxisNameGap: 0,
|
|
1423
|
+
hasLeftAxisTitle: false,
|
|
1424
|
+
});
|
|
1425
|
+
const radarPlotWidth = Math.max(0, (width ?? 0) - radarBand.grid.left - radarBand.grid.right);
|
|
1426
|
+
const radarPlotHeight = Math.max(0, (height ?? 0) - radarBand.grid.top - radarBand.grid.bottom);
|
|
1427
|
+
// Leave ~30px around the circle for the axis names.
|
|
1428
|
+
const radarGeometry = radarPlotWidth > 0 && radarPlotHeight > 0
|
|
1429
|
+
? {
|
|
1430
|
+
center: [
|
|
1431
|
+
radarBand.grid.left + radarPlotWidth / 2,
|
|
1432
|
+
radarBand.grid.top + radarPlotHeight / 2,
|
|
1433
|
+
],
|
|
1434
|
+
radius: Math.max(10, Math.min(radarPlotWidth, radarPlotHeight) / 2 - 30),
|
|
1435
|
+
}
|
|
1436
|
+
: {};
|
|
1333
1437
|
chartRef.current?.setOption({
|
|
1334
1438
|
radar: {
|
|
1439
|
+
...radarGeometry,
|
|
1335
1440
|
// The common scale's tick labels along ONE axis, as Excel draws
|
|
1336
1441
|
// them: echarts labels every indicator axis, so each indicator
|
|
1337
1442
|
// carries its own `axisLabel.show` (an indicator's own options win
|
|
@@ -1350,11 +1455,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1350
1455
|
formatter: (value) => formatAxisValue(radarFormats[0], value, locale),
|
|
1351
1456
|
},
|
|
1352
1457
|
},
|
|
1353
|
-
legend:
|
|
1354
|
-
...legend,
|
|
1355
|
-
show: radarData.length > 1,
|
|
1356
|
-
data: radarData.map((d) => d.name),
|
|
1357
|
-
}, spec.legend),
|
|
1458
|
+
legend: { ...radarLegend, ...radarBand.legend },
|
|
1358
1459
|
tooltip: { trigger: "item", formatter: radarTooltipFormatter },
|
|
1359
1460
|
series: [
|
|
1360
1461
|
{
|
|
@@ -2301,6 +2402,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2301
2402
|
top: 60,
|
|
2302
2403
|
bottom: 20,
|
|
2303
2404
|
};
|
|
2405
|
+
// `dataLabels: { show: false }` hides the values inside the bars; the
|
|
2406
|
+
// stage names on the left stay (#1091).
|
|
2407
|
+
const funnelLabelsOff = resolveSeriesDataLabels(spec.dataLabels, seriesEntry)?.show ===
|
|
2408
|
+
false;
|
|
2304
2409
|
chartSeries.push({
|
|
2305
2410
|
type: "funnel",
|
|
2306
2411
|
id: chartSeriesId(0, 0),
|
|
@@ -2310,11 +2415,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2310
2415
|
itemStyle: {
|
|
2311
2416
|
color: (params) => stageColors.get(params.dataIndex) ?? funnelColor,
|
|
2312
2417
|
},
|
|
2313
|
-
label:
|
|
2314
|
-
show:
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2418
|
+
label: funnelLabelsOff
|
|
2419
|
+
? { show: false }
|
|
2420
|
+
: {
|
|
2421
|
+
show: true,
|
|
2422
|
+
position: "inside",
|
|
2423
|
+
formatter: (params) => formatAxisValue(undefined, params.value, locale),
|
|
2424
|
+
},
|
|
2318
2425
|
});
|
|
2319
2426
|
// The stage names sit to the left of the bars, where Excel's
|
|
2320
2427
|
// category axis puts them: an invisible twin funnel carries them.
|
|
@@ -2331,49 +2438,49 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2331
2438
|
labelLine: { show: false },
|
|
2332
2439
|
z: 1,
|
|
2333
2440
|
});
|
|
2334
|
-
legend.
|
|
2441
|
+
// The legend lists the series (shown when `legend.position` says so).
|
|
2442
|
+
legend.data = seriesEntry?.dataLabel ? [seriesEntry.dataLabel] : [];
|
|
2335
2443
|
}
|
|
2336
2444
|
else if (spec.chartType === "boxplot") {
|
|
2337
2445
|
const quartileMethod = spec.quartileMethod;
|
|
2338
|
-
const showOutliers = spec.showOutliers;
|
|
2446
|
+
const showOutliers = spec.showOutliers !== false;
|
|
2339
2447
|
const showMeanMarker = spec.showMeanMarker === true;
|
|
2340
|
-
const
|
|
2341
|
-
const
|
|
2342
|
-
//
|
|
2343
|
-
//
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
// the points beyond them (Excel's "show outlier points").
|
|
2357
|
-
// Otherwise the whiskers span the full min/max — today's
|
|
2358
|
-
// rendering.
|
|
2359
|
-
const lo = showOutliers === true ? summary.lowerWhisker : summary.min;
|
|
2360
|
-
const hi = showOutliers === true ? summary.upperWhisker : summary.max;
|
|
2361
|
-
boxStats.push([lo, summary.q1, summary.median, summary.q3, hi]);
|
|
2362
|
-
boxLabels.push(label);
|
|
2363
|
-
if (showOutliers === true) {
|
|
2364
|
-
for (const outlier of summary.outliers) {
|
|
2365
|
-
outlierPoints.push([boxIndex, outlier]);
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
if (showMeanMarker) {
|
|
2369
|
-
meanPoints.push([boxIndex, summary.mean]);
|
|
2370
|
-
}
|
|
2448
|
+
const showInnerPoints = spec.showInnerPoints === true;
|
|
2449
|
+
const showMeanLine = spec.showMeanLine === true;
|
|
2450
|
+
// A category domain groups every series by its distinct values (in
|
|
2451
|
+
// order of first appearance): one box per category per series, as
|
|
2452
|
+
// Excel draws it. Without a domain each series is one box on its own
|
|
2453
|
+
// slot (#1092).
|
|
2454
|
+
const boxDomainSource = spec.domains?.[0]?.sources?.[0];
|
|
2455
|
+
const boxCategories = boxDomainSource
|
|
2456
|
+
? (getDomainValuesFromRange(boxDomainSource, spec.hiddenDimensionStrategy) ?? []).map((v) => (v === null || v === undefined ? "" : `${v}`.trim()))
|
|
2457
|
+
: [];
|
|
2458
|
+
const grouped = boxCategories.some((c) => c !== "");
|
|
2459
|
+
const groups = [];
|
|
2460
|
+
if (grouped) {
|
|
2461
|
+
for (const c of boxCategories) {
|
|
2462
|
+
if (c !== "" && !groups.includes(c))
|
|
2463
|
+
groups.push(c);
|
|
2371
2464
|
}
|
|
2372
2465
|
}
|
|
2466
|
+
const boxSeriesSpecs = spec.series ?? [];
|
|
2467
|
+
const slots = grouped ? groups : boxSeriesSpecs.map((s, i) => s.dataLabel && !isAFormula(s.dataLabel) ? s.dataLabel : `Series ${i + 1}`);
|
|
2468
|
+
// Excel sizes the boxes like columns: gapWidth is a percentage of the
|
|
2469
|
+
// box width (default 150). echarts' boxplot only clamps its width
|
|
2470
|
+
// (`boxWidth`), so the clamp is the box width that gap allows in the
|
|
2471
|
+
// plot's category band.
|
|
2472
|
+
const boxGap = typeof spec.gapWidth === "number" ? Math.max(0, spec.gapWidth) : 150;
|
|
2473
|
+
const boxPlotWidth = Math.max(0, (width ?? 0) * 0.85 - 40);
|
|
2474
|
+
const boxBand = slots.length ? boxPlotWidth / slots.length : boxPlotWidth;
|
|
2475
|
+
const boxWidthMax = Math.max(2, Math.round(boxBand / (Math.max(1, boxSeriesSpecs.length) + boxGap / 100)));
|
|
2476
|
+
const boxCount = Math.max(1, boxSeriesSpecs.length);
|
|
2477
|
+
// Points of series i sit at that series' sub-slot inside the band
|
|
2478
|
+
// (echarts spreads a category's boxes over ~80% of it).
|
|
2479
|
+
const slotX = (slotIndex, seriesIndex) => slotIndex + ((seriesIndex + 0.5) / boxCount - 0.5) * 0.8;
|
|
2373
2480
|
chartDomains.length = 0;
|
|
2374
2481
|
chartDomains.push({
|
|
2375
2482
|
type: "category",
|
|
2376
|
-
data:
|
|
2483
|
+
data: slots,
|
|
2377
2484
|
show: showAxis,
|
|
2378
2485
|
axisLine: { show: true },
|
|
2379
2486
|
axisTick: { show: true },
|
|
@@ -2383,42 +2490,113 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2383
2490
|
nameGap: 30,
|
|
2384
2491
|
});
|
|
2385
2492
|
chartSeries.length = 0;
|
|
2386
|
-
const
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2493
|
+
const boxLegend = [];
|
|
2494
|
+
boxSeriesSpecs.forEach((seriesEntry, seriesIndex) => {
|
|
2495
|
+
const label = seriesEntry.dataLabel && !isAFormula(seriesEntry.dataLabel)
|
|
2496
|
+
? seriesEntry.dataLabel
|
|
2497
|
+
: "";
|
|
2498
|
+
const rawValues = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
|
|
2499
|
+
const color = seriesEntry.color ?? paletteColorAt(seriesIndex);
|
|
2500
|
+
const boxData = [];
|
|
2501
|
+
const outlierPoints = [];
|
|
2502
|
+
const innerPoints = [];
|
|
2503
|
+
const meanPoints = [];
|
|
2504
|
+
const meanLine = [];
|
|
2505
|
+
const slotValues = grouped
|
|
2506
|
+
? groups.map((g) => rawValues.filter((_, i) => boxCategories[i] === g))
|
|
2507
|
+
: slots.map((_, i) => (i === seriesIndex ? rawValues : []));
|
|
2508
|
+
slotValues.forEach((values, slotIndex) => {
|
|
2509
|
+
const summary = computeBoxplotSummary(values, { quartileMethod });
|
|
2510
|
+
if (!summary) {
|
|
2511
|
+
boxData.push([]);
|
|
2512
|
+
meanLine.push(null);
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
// Whiskers stop at the most extreme points inside the 1.5×IQR
|
|
2516
|
+
// fences; hiding the outliers hides the points only (Excel).
|
|
2517
|
+
boxData.push([
|
|
2518
|
+
summary.lowerWhisker,
|
|
2519
|
+
summary.q1,
|
|
2520
|
+
summary.median,
|
|
2521
|
+
summary.q3,
|
|
2522
|
+
summary.upperWhisker,
|
|
2523
|
+
]);
|
|
2524
|
+
meanLine.push(summary.mean);
|
|
2525
|
+
const x = slotX(slotIndex, seriesIndex);
|
|
2526
|
+
if (showOutliers) {
|
|
2527
|
+
for (const outlier of summary.outliers)
|
|
2528
|
+
outlierPoints.push([x, outlier]);
|
|
2529
|
+
}
|
|
2530
|
+
if (showInnerPoints) {
|
|
2531
|
+
for (const v of values) {
|
|
2532
|
+
if (v >= summary.lowerWhisker && v <= summary.upperWhisker) {
|
|
2533
|
+
innerPoints.push([x, v]);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
if (showMeanMarker)
|
|
2538
|
+
meanPoints.push([x, summary.mean]);
|
|
2404
2539
|
});
|
|
2405
|
-
|
|
2406
|
-
|
|
2540
|
+
if (label)
|
|
2541
|
+
boxLegend.push(label);
|
|
2407
2542
|
chartSeries.push({
|
|
2543
|
+
type: "boxplot",
|
|
2544
|
+
id: chartSeriesId(seriesIndex, 0),
|
|
2545
|
+
name: label,
|
|
2546
|
+
data: boxData,
|
|
2547
|
+
boxWidth: [2, boxWidthMax],
|
|
2548
|
+
itemStyle: { color, borderColor: color },
|
|
2549
|
+
});
|
|
2550
|
+
const pointSeries = (name, points, extra) => ({
|
|
2408
2551
|
type: "scatter",
|
|
2409
|
-
name
|
|
2410
|
-
data:
|
|
2411
|
-
|
|
2412
|
-
symbolSize: 9,
|
|
2413
|
-
...(boxColor ? { itemStyle: { color: boxColor } } : {}),
|
|
2552
|
+
name,
|
|
2553
|
+
data: points,
|
|
2554
|
+
itemStyle: { color },
|
|
2414
2555
|
tooltip: { show: false },
|
|
2556
|
+
...extra,
|
|
2415
2557
|
});
|
|
2416
|
-
|
|
2417
|
-
|
|
2558
|
+
if (outlierPoints.length > 0) {
|
|
2559
|
+
chartSeries.push(pointSeries("", outlierPoints, { symbolSize: 6 }));
|
|
2560
|
+
}
|
|
2561
|
+
if (innerPoints.length > 0) {
|
|
2562
|
+
chartSeries.push(pointSeries("", innerPoints, { symbolSize: 4, itemStyle: { color, opacity: 0.6 } }));
|
|
2563
|
+
}
|
|
2564
|
+
if (meanPoints.length > 0) {
|
|
2565
|
+
chartSeries.push(pointSeries("", meanPoints, { symbol: "diamond", symbolSize: 9 }));
|
|
2566
|
+
}
|
|
2567
|
+
if (showMeanLine && meanLine.some((m) => m !== null)) {
|
|
2568
|
+
chartSeries.push({
|
|
2569
|
+
type: "line",
|
|
2570
|
+
name: "",
|
|
2571
|
+
data: meanLine.map((m, slotIndex) => m === null ? null : [slotX(slotIndex, seriesIndex), m]),
|
|
2572
|
+
showSymbol: false,
|
|
2573
|
+
lineStyle: { color, type: "dashed", width: 1 },
|
|
2574
|
+
itemStyle: { color },
|
|
2575
|
+
tooltip: { show: false },
|
|
2576
|
+
z: 3,
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
// The legend lists the series (shown when `legend.position` says so).
|
|
2581
|
+
legend.data = boxLegend;
|
|
2418
2582
|
}
|
|
2419
2583
|
else if (spec.chartType === "histogram") {
|
|
2420
2584
|
const rawValues = flattenChartDataValues(spec.series, getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
|
|
2421
|
-
|
|
2585
|
+
// A domain names a category column: Excel's "By Category" histogram
|
|
2586
|
+
// / Pareto over categories aggregates the values per category (sum
|
|
2587
|
+
// unless the series says otherwise) instead of binning them (#1087).
|
|
2588
|
+
const histogramDomainSource = spec.domains?.[0]?.sources?.[0];
|
|
2589
|
+
const histogramCategories = histogramDomainSource
|
|
2590
|
+
? (getDomainValuesFromRange(histogramDomainSource, spec.hiddenDimensionStrategy) ?? [])
|
|
2591
|
+
: [];
|
|
2592
|
+
let { labels, counts } = histogramCategories.length
|
|
2593
|
+
? aggregateByCategory(histogramCategories, rawValues, spec.series?.[0]?.aggregateType ?? "SUM")
|
|
2594
|
+
: binHistogram(rawValues, spec.binCount, {
|
|
2595
|
+
binSize: spec.binSize,
|
|
2596
|
+
intervalClosed: spec.intervalClosed,
|
|
2597
|
+
overflow: spec.overflow,
|
|
2598
|
+
underflow: spec.underflow,
|
|
2599
|
+
});
|
|
2422
2600
|
// Pareto — a histogram whose bins are sorted descending with a
|
|
2423
2601
|
// cumulative-% line overlaid on a secondary 0-100% axis. The
|
|
2424
2602
|
// authored type arrives as `sourceChartType: "PARETO"` on an
|
|
@@ -2448,7 +2626,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2448
2626
|
type: "bar",
|
|
2449
2627
|
name: spec.series?.[0]?.dataLabel ?? "",
|
|
2450
2628
|
data: counts,
|
|
2451
|
-
|
|
2629
|
+
// Excel's histogram columns touch unless `gapWidth` says otherwise
|
|
2630
|
+
// (#1087); the gap maps like a column chart's.
|
|
2631
|
+
barCategoryGap: gapWidthToBarCategoryGap(spec.gapWidth ?? 0, { seriesCount: 1 }) ??
|
|
2632
|
+
"0%",
|
|
2452
2633
|
};
|
|
2453
2634
|
const histogramColor = spec.series?.[0]?.color;
|
|
2454
2635
|
if (histogramColor) {
|
|
@@ -2458,14 +2639,21 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2458
2639
|
if (isPareto) {
|
|
2459
2640
|
// Cumulative-% overlay on the secondary axis (yAxisIndex 1);
|
|
2460
2641
|
// the matching 0-100% axis element is appended with the other
|
|
2461
|
-
// value axes below.
|
|
2642
|
+
// value axes below. Excel draws the Pareto line without markers.
|
|
2643
|
+
const paretoLineColor = spec.paretoLineColor;
|
|
2462
2644
|
chartSeries.push({
|
|
2463
2645
|
type: "line",
|
|
2464
2646
|
name: "Cumulative %",
|
|
2465
2647
|
data: cumulativePercents,
|
|
2466
2648
|
yAxisIndex: 1,
|
|
2467
|
-
|
|
2468
|
-
|
|
2649
|
+
showSymbol: false,
|
|
2650
|
+
symbol: "none",
|
|
2651
|
+
...(paretoLineColor
|
|
2652
|
+
? {
|
|
2653
|
+
itemStyle: { color: paretoLineColor },
|
|
2654
|
+
lineStyle: { color: paretoLineColor },
|
|
2655
|
+
}
|
|
2656
|
+
: {}),
|
|
2469
2657
|
});
|
|
2470
2658
|
}
|
|
2471
2659
|
legend.data = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,sBAAsB,GACjC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,yGA6DzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SAuBX,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,GAAG,SAAS,KACnD,OAAgD,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,aAAa,CAAC,OAAO,CAAC,KAC7B,MAAM,EAmBR,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;aASzC,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,sBAAsB,GACjC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,yGA6DzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SAuBX,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,GAAG,SAAS,KACnD,OAAgD,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,aAAa,CAAC,OAAO,CAAC,KAC7B,MAAM,EAmBR,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;aASzC,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAmBjE,CAAC"}
|
|
@@ -121,6 +121,8 @@ export const showXAxis = (type) => {
|
|
|
121
121
|
type === "histogram" ||
|
|
122
122
|
// A waterfall's steps are categories (Opening, Sales, …) (#1070).
|
|
123
123
|
type === "waterfall" ||
|
|
124
|
+
// A box & whisker's slots are categories (or the series names) (#1092).
|
|
125
|
+
type === "boxplot" ||
|
|
124
126
|
// Stock is line-derived and lives on a category x-axis. Radar is
|
|
125
127
|
// axis-less (it uses a polar `radar` indicator instead).
|
|
126
128
|
type === "stock") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/basic-chart.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/basic-chart.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA8ZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAmGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAkqFjB,CAAC"}
|