@torpor/ui 0.3.6 → 0.3.7
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/Chart/BarChart.torp +159 -0
- package/dist/Chart/DataSeries.ts +5 -0
- package/dist/Chart/GridLines.torp +62 -0
- package/dist/Chart/LineChart.torp +196 -0
- package/dist/Chart/PieChart.torp +145 -0
- package/dist/Chart/ScatterChart.torp +215 -0
- package/dist/Chart/SparkLine.torp +71 -0
- package/dist/Chart/XAxis.torp +60 -0
- package/dist/Chart/YAxis.torp +41 -0
- package/dist/Chart/index.d.ts +220 -0
- package/dist/Chart/index.js +34 -0
- package/dist/Chart/index.js.map +1 -0
- package/dist/Form/TextArea.torp +3 -0
- package/dist/Form/index.d.ts +2 -0
- package/dist/metafile-esm.json +1 -1
- package/dist/utils/chartColors.ts +17 -0
- package/dist/utils/chartUtils.ts +71 -0
- package/package.json +7 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import XAxis from "./XAxis.torp";
|
|
3
|
+
import YAxis from "./YAxis.torp";
|
|
4
|
+
import GridLines from "./GridLines.torp";
|
|
5
|
+
import {
|
|
6
|
+
calculateMaxValue,
|
|
7
|
+
calculateStepValue,
|
|
8
|
+
calculateStepLabels,
|
|
9
|
+
calculateItemWidth,
|
|
10
|
+
calculateValueHeight,
|
|
11
|
+
calculateChartBottom,
|
|
12
|
+
calculateChartLeft
|
|
13
|
+
} from "../utils/chartUtils";
|
|
14
|
+
import { chartColors } from "../utils/chartColors";
|
|
15
|
+
|
|
16
|
+
import type DataSeries from "./DataSeries";
|
|
17
|
+
|
|
18
|
+
interface BarChartProps {
|
|
19
|
+
/** An ID for the root element */
|
|
20
|
+
id?: string;
|
|
21
|
+
/** Classes for the root element */
|
|
22
|
+
class?: ClassValue,
|
|
23
|
+
/** Styles for the root element */
|
|
24
|
+
style?: StyleValue,
|
|
25
|
+
series: DataSeries[],
|
|
26
|
+
height?: number,
|
|
27
|
+
width?: number,
|
|
28
|
+
xLabel?: string,
|
|
29
|
+
yLabel?: string,
|
|
30
|
+
maxValue?: number,
|
|
31
|
+
stepCount?: number,
|
|
32
|
+
stepValue?: number,
|
|
33
|
+
hideXAxis?: boolean,
|
|
34
|
+
hideYAxis?: boolean,
|
|
35
|
+
showHLines?: boolean,
|
|
36
|
+
showVLines?: boolean,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function BarChart($props: BarChartProps) {
|
|
40
|
+
const defaultHeight = 200;
|
|
41
|
+
const defaultWidth = 400;
|
|
42
|
+
|
|
43
|
+
let container: HTMLElement;
|
|
44
|
+
let measurer: SVGTextElement;
|
|
45
|
+
|
|
46
|
+
let $state = $watch({
|
|
47
|
+
textWidth: 0,
|
|
48
|
+
textHeight: 0,
|
|
49
|
+
get labels() {
|
|
50
|
+
return $props.series.map((s) => s.name);
|
|
51
|
+
},
|
|
52
|
+
get calculatedHeight() {
|
|
53
|
+
return $props.height || defaultHeight;
|
|
54
|
+
},
|
|
55
|
+
get calculatedWidth() {
|
|
56
|
+
return $props.width || container?.clientWidth || defaultWidth;
|
|
57
|
+
},
|
|
58
|
+
get maxValue () {
|
|
59
|
+
return $props.maxValue || calculateMaxValue($props.series);
|
|
60
|
+
},
|
|
61
|
+
get calculatedStepValue() {
|
|
62
|
+
return $props.stepValue || calculateStepValue($state.maxValue, $props.stepCount);
|
|
63
|
+
},
|
|
64
|
+
get stepLabels() {
|
|
65
|
+
return calculateStepLabels($props.stepCount || 2, $state.calculatedStepValue);
|
|
66
|
+
},
|
|
67
|
+
get itemWidth() {
|
|
68
|
+
return calculateItemWidth($state.calculatedWidth, $state.chartLeft, $state.labels);
|
|
69
|
+
},
|
|
70
|
+
get valueHeight() {
|
|
71
|
+
return calculateValueHeight($state.chartBottom, $state.textHeight, $state.calculatedStepValue, $props.stepCount || 2);
|
|
72
|
+
},
|
|
73
|
+
get chartBottom() {
|
|
74
|
+
return calculateChartBottom($props.xLabel ?? "", $state.calculatedHeight, $state.textHeight);
|
|
75
|
+
},
|
|
76
|
+
get chartLeft() {
|
|
77
|
+
return calculateChartLeft($state.stepLabels, $props.yLabel ?? "", $state.textHeight, $state.textWidth);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
$mount(() => {
|
|
82
|
+
// HACK: Wait until the measurer is really available
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
const bbox = measurer.getBBox();
|
|
85
|
+
$state.textWidth = bbox.width;
|
|
86
|
+
$state.textHeight = bbox.height * 1.5;
|
|
87
|
+
measurer.remove();
|
|
88
|
+
}, 1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
@render {
|
|
92
|
+
<div
|
|
93
|
+
{$props?.id}
|
|
94
|
+
class={["torp-bar-chart", $props?.class]}
|
|
95
|
+
{$props?.style}
|
|
96
|
+
&ref={container}
|
|
97
|
+
>
|
|
98
|
+
<svg version="1.1" width={$state.calculatedWidth} height={$state.calculatedHeight} xmlns="http://www.w3.org/2000/svg">
|
|
99
|
+
<g>
|
|
100
|
+
<text &ref={measurer} x="-999" y="-999">8</text>
|
|
101
|
+
|
|
102
|
+
@if ($state.textWidth && $state.textHeight) {
|
|
103
|
+
<XAxis
|
|
104
|
+
showAxis={!$props.hideXAxis}
|
|
105
|
+
height={$state.calculatedHeight}
|
|
106
|
+
width={$state.calculatedWidth}
|
|
107
|
+
{$state.itemWidth}
|
|
108
|
+
{$state.textHeight}
|
|
109
|
+
xLabel={$props.xLabel ?? ""}
|
|
110
|
+
stepLabels={[]}
|
|
111
|
+
stepValue={0}
|
|
112
|
+
{$state.labels}
|
|
113
|
+
{$state.chartLeft}
|
|
114
|
+
{$state.chartBottom}
|
|
115
|
+
/>
|
|
116
|
+
<YAxis
|
|
117
|
+
showAxis={!$props.hideYAxis}
|
|
118
|
+
{$state.valueHeight}
|
|
119
|
+
{$state.textHeight}
|
|
120
|
+
yLabel={$props.yLabel ?? ""}
|
|
121
|
+
{$state.stepLabels}
|
|
122
|
+
stepValue={$state.calculatedStepValue}
|
|
123
|
+
{$state.chartLeft}
|
|
124
|
+
{$state.chartBottom}
|
|
125
|
+
/>
|
|
126
|
+
<GridLines
|
|
127
|
+
{$props.showHLines}
|
|
128
|
+
{$props.showVLines}
|
|
129
|
+
width={$state.calculatedWidth}
|
|
130
|
+
{$state.labels}
|
|
131
|
+
xStepValue={0}
|
|
132
|
+
xStepLabels={[]}
|
|
133
|
+
yStepValue={$state.calculatedStepValue}
|
|
134
|
+
yStepLabels={$state.stepLabels}
|
|
135
|
+
{$state.itemWidth}
|
|
136
|
+
{$state.valueHeight}
|
|
137
|
+
{$state.chartLeft}
|
|
138
|
+
{$state.chartBottom}
|
|
139
|
+
type="bar"
|
|
140
|
+
/>
|
|
141
|
+
@for (let [i, series] of $props.series.entries()) {
|
|
142
|
+
<rect
|
|
143
|
+
x={$state.chartLeft +
|
|
144
|
+
i * $state.itemWidth +
|
|
145
|
+
$state.itemWidth / 4}
|
|
146
|
+
y={$state.chartBottom - Math.max(1, series.data[0] * $state.valueHeight)}
|
|
147
|
+
width={Math.max(1, $state.itemWidth / 2)}
|
|
148
|
+
height={Math.max(1, series.data[0] * $state.valueHeight)}
|
|
149
|
+
fill={series.color || chartColors[i % chartColors.length]}
|
|
150
|
+
>
|
|
151
|
+
<title>{`${[series.name, series.data[0]].filter(Boolean).join("\n")}`}</title>
|
|
152
|
+
</rect>
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
</g>
|
|
156
|
+
</svg>
|
|
157
|
+
</div>
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
|
|
3
|
+
interface GridLinesProps {
|
|
4
|
+
/** An ID for the root element */
|
|
5
|
+
id?: string;
|
|
6
|
+
/** Classes for the root element */
|
|
7
|
+
class?: ClassValue,
|
|
8
|
+
/** Styles for the root element */
|
|
9
|
+
style?: StyleValue,
|
|
10
|
+
showHLines?: boolean,
|
|
11
|
+
showVLines?: boolean,
|
|
12
|
+
width: number;
|
|
13
|
+
labels: string[];
|
|
14
|
+
xStepValue: number;
|
|
15
|
+
xStepLabels: number[];
|
|
16
|
+
yStepValue: number;
|
|
17
|
+
yStepLabels: number[];
|
|
18
|
+
itemWidth: number;
|
|
19
|
+
valueHeight: number;
|
|
20
|
+
chartLeft: number;
|
|
21
|
+
chartBottom: number;
|
|
22
|
+
type: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function GridLines($props: GridLinesProps) {
|
|
26
|
+
@render {
|
|
27
|
+
<svg>
|
|
28
|
+
@if ($props.showHLines && $props.yStepLabels) {
|
|
29
|
+
@for (let [i, label] of $props.yStepLabels!.entries()) {
|
|
30
|
+
<line
|
|
31
|
+
x1={$props.chartLeft ?? 0}
|
|
32
|
+
y1={$props.chartBottom - i * $props.yStepValue * $props.valueHeight}
|
|
33
|
+
x2={$props.width ?? 0}
|
|
34
|
+
y2={$props.chartBottom - i * $props.yStepValue * $props.valueHeight}
|
|
35
|
+
/>
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@if ($props.showVLines) {
|
|
40
|
+
@if ($props.xStepLabels.length && $props.xStepValue) {
|
|
41
|
+
@for (let [i, label] of $props.xStepLabels.entries()) {
|
|
42
|
+
<line
|
|
43
|
+
x1={$props.chartLeft + i * $props.xStepValue * $props.itemWidth}
|
|
44
|
+
y1={$props.chartBottom}
|
|
45
|
+
x2={$props.chartLeft + i * $props.xStepValue * $props.itemWidth}
|
|
46
|
+
y2={0}
|
|
47
|
+
/>
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
@for (let [i, label] of $props.labels.entries()) {
|
|
51
|
+
<line
|
|
52
|
+
x1={$props.chartLeft + i * $props.itemWidth + ($props.type === "bar" ? 0 : $props.itemWidth / 2)}
|
|
53
|
+
y1={$props.chartBottom}
|
|
54
|
+
x2={$props.chartLeft + i * $props.itemWidth + ($props.type === "bar" ? 0 : $props.itemWidth / 2)}
|
|
55
|
+
y2={0}
|
|
56
|
+
/>
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</svg>
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
|
|
3
|
+
import XAxis from "./XAxis.torp";
|
|
4
|
+
import YAxis from "./YAxis.torp";
|
|
5
|
+
import GridLines from "./GridLines.torp";
|
|
6
|
+
import {
|
|
7
|
+
calculateMaxValue,
|
|
8
|
+
calculateStepValue,
|
|
9
|
+
calculateStepLabels,
|
|
10
|
+
calculateItemWidth,
|
|
11
|
+
calculateValueHeight,
|
|
12
|
+
calculateChartBottom,
|
|
13
|
+
calculateChartLeft
|
|
14
|
+
} from "../utils/chartUtils";
|
|
15
|
+
import { chartColors } from "../utils/chartColors";
|
|
16
|
+
|
|
17
|
+
import type DataSeries from "./DataSeries";
|
|
18
|
+
|
|
19
|
+
interface LineChartProps {
|
|
20
|
+
/** An ID for the root element */
|
|
21
|
+
id?: string;
|
|
22
|
+
/** Classes for the root element */
|
|
23
|
+
class?: ClassValue,
|
|
24
|
+
/** Styles for the root element */
|
|
25
|
+
style?: StyleValue,
|
|
26
|
+
series: DataSeries[],
|
|
27
|
+
height?: number,
|
|
28
|
+
width?: number,
|
|
29
|
+
xLabel?: string,
|
|
30
|
+
yLabel?: string,
|
|
31
|
+
maxValue?: number,
|
|
32
|
+
stepCount?: number,
|
|
33
|
+
stepValue?: number,
|
|
34
|
+
showPoints?: boolean,
|
|
35
|
+
hideXAxis?: boolean,
|
|
36
|
+
hideYAxis?: boolean,
|
|
37
|
+
showHLines?: boolean,
|
|
38
|
+
showVLines?: boolean,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function LineChart($props: LineChartProps) {
|
|
42
|
+
const defaultHeight = 200;
|
|
43
|
+
const defaultWidth = 400;
|
|
44
|
+
|
|
45
|
+
let container: HTMLElement;
|
|
46
|
+
let measurer: SVGTextElement;
|
|
47
|
+
|
|
48
|
+
let $state = $watch({
|
|
49
|
+
textWidth: 0,
|
|
50
|
+
textHeight: 0,
|
|
51
|
+
get labels() {
|
|
52
|
+
return $props.series.map((s) => s.name);
|
|
53
|
+
},
|
|
54
|
+
get calculatedHeight() {
|
|
55
|
+
return $props.height || defaultHeight;
|
|
56
|
+
},
|
|
57
|
+
get calculatedWidth() {
|
|
58
|
+
return $props.width || container?.clientWidth || defaultWidth;
|
|
59
|
+
},
|
|
60
|
+
get maxValue() {
|
|
61
|
+
return $props.maxValue || calculateMaxValue($props.series);
|
|
62
|
+
},
|
|
63
|
+
get calculatedStepValue() {
|
|
64
|
+
return $props.stepValue || calculateStepValue($state.maxValue, $props.stepCount);
|
|
65
|
+
},
|
|
66
|
+
get stepLabels() {
|
|
67
|
+
return calculateStepLabels($props.stepCount || 2, $state.calculatedStepValue);
|
|
68
|
+
},
|
|
69
|
+
get itemWidth() {
|
|
70
|
+
return calculateItemWidth($state.calculatedWidth, $state.chartLeft, $state.labels);
|
|
71
|
+
},
|
|
72
|
+
get valueHeight() {
|
|
73
|
+
return calculateValueHeight($state.chartBottom, $state.textHeight, $state.calculatedStepValue, $props.stepCount || 2);
|
|
74
|
+
},
|
|
75
|
+
get chartBottom() {
|
|
76
|
+
return calculateChartBottom($props.xLabel ?? "", $state.calculatedHeight, $state.textHeight);
|
|
77
|
+
},
|
|
78
|
+
get chartLeft() {
|
|
79
|
+
return calculateChartLeft($state.stepLabels, $props.yLabel ?? "", $state.textHeight, $state.textWidth);
|
|
80
|
+
},
|
|
81
|
+
get pointSeries() {
|
|
82
|
+
return buildPointSeries(
|
|
83
|
+
$props.series,
|
|
84
|
+
$state.chartLeft,
|
|
85
|
+
$state.chartBottom,
|
|
86
|
+
$state.itemWidth,
|
|
87
|
+
$state.valueHeight
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
$mount(() => {
|
|
93
|
+
// HACK: Wait until the measurer is really available
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
const bbox = measurer.getBBox();
|
|
96
|
+
$state.textWidth = bbox.width;
|
|
97
|
+
$state.textHeight = bbox.height * 1.5;
|
|
98
|
+
measurer.remove();
|
|
99
|
+
}, 1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function buildPointSeries(
|
|
103
|
+
theSeries: DataSeries[],
|
|
104
|
+
theChartLeft: number,
|
|
105
|
+
theChartBottom: number,
|
|
106
|
+
theItemWidth: number,
|
|
107
|
+
theValueHeight: number
|
|
108
|
+
): { x: number; y: number }[][] {
|
|
109
|
+
return theSeries.map((series) => {
|
|
110
|
+
return series.data.map((value, i) => {
|
|
111
|
+
const x = +(theChartLeft + i * theItemWidth + theItemWidth / 2).toFixed(2);
|
|
112
|
+
const y = +(theChartBottom - value * theValueHeight).toFixed(2);
|
|
113
|
+
return { x, y };
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@render {
|
|
119
|
+
<div
|
|
120
|
+
{$props?.id}
|
|
121
|
+
class={["torp-line-chart", $props?.class]}
|
|
122
|
+
{$props?.style}
|
|
123
|
+
&ref={container}
|
|
124
|
+
>
|
|
125
|
+
<svg version="1.1" width={$state.calculatedWidth} height={$state.calculatedHeight}>
|
|
126
|
+
<g>
|
|
127
|
+
<text &ref={measurer} x="-999" y="-999">8</text>
|
|
128
|
+
|
|
129
|
+
@if ($state.textWidth && $state.textHeight) {
|
|
130
|
+
<XAxis
|
|
131
|
+
showAxis={!$props.hideXAxis}
|
|
132
|
+
height={$state.calculatedHeight}
|
|
133
|
+
width={$state.calculatedWidth}
|
|
134
|
+
{$state.itemWidth}
|
|
135
|
+
{$state.textHeight}
|
|
136
|
+
xLabel={$props.xLabel ?? ""}
|
|
137
|
+
{$state.labels}
|
|
138
|
+
stepLabels={[]}
|
|
139
|
+
stepValue={0}
|
|
140
|
+
{$state.chartLeft}
|
|
141
|
+
{$state.chartBottom}
|
|
142
|
+
/>
|
|
143
|
+
<YAxis
|
|
144
|
+
showAxis={!$props.hideYAxis}
|
|
145
|
+
{$state.valueHeight}
|
|
146
|
+
{$state.textHeight}
|
|
147
|
+
yLabel={$props.yLabel ?? ""}
|
|
148
|
+
{$state.stepLabels}
|
|
149
|
+
stepValue={$state.calculatedStepValue}
|
|
150
|
+
{$state.chartLeft}
|
|
151
|
+
{$state.chartBottom}
|
|
152
|
+
/>
|
|
153
|
+
<GridLines
|
|
154
|
+
{$props.showHLines}
|
|
155
|
+
{$props.showVLines}
|
|
156
|
+
width={$state.calculatedWidth}
|
|
157
|
+
{$state.labels}
|
|
158
|
+
xStepValue={0}
|
|
159
|
+
xStepLabels={[]}
|
|
160
|
+
yStepValue={$state.calculatedStepValue}
|
|
161
|
+
yStepLabels={[]}
|
|
162
|
+
{$state.stepLabels}
|
|
163
|
+
{$state.itemWidth}
|
|
164
|
+
{$state.valueHeight}
|
|
165
|
+
{$state.chartLeft}
|
|
166
|
+
{$state.chartBottom}
|
|
167
|
+
type="line"
|
|
168
|
+
/>
|
|
169
|
+
@for (let [i, series] of $props.series.entries()) {
|
|
170
|
+
<polyline
|
|
171
|
+
points={$state.pointSeries[i].map((p) => `${p.x},${p.y}`).join(" ")}
|
|
172
|
+
stroke={series.color || chartColors[i % chartColors.length]}
|
|
173
|
+
stroke-width="2"
|
|
174
|
+
fill="none"
|
|
175
|
+
/>
|
|
176
|
+
@if ($props.showPoints) {
|
|
177
|
+
@for (let [j, point] of $state.pointSeries[i].entries()) {
|
|
178
|
+
<circle
|
|
179
|
+
cx={point.x}
|
|
180
|
+
cy={point.y}
|
|
181
|
+
r="4"
|
|
182
|
+
stroke={series.color || chartColors[i % chartColors.length]}
|
|
183
|
+
stroke-width="2"
|
|
184
|
+
fill="white"
|
|
185
|
+
>
|
|
186
|
+
<title>{`${[series.name, $state.labels[j], series.data[j]].filter(Boolean).join("\n")}`}</title>
|
|
187
|
+
</circle>
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
</g>
|
|
193
|
+
</svg>
|
|
194
|
+
</div>
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { chartColors } from "../utils/chartColors";
|
|
3
|
+
|
|
4
|
+
interface PieData {
|
|
5
|
+
name: string;
|
|
6
|
+
value: number;
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SegmentData {
|
|
11
|
+
name: string;
|
|
12
|
+
value: number;
|
|
13
|
+
color: string;
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PieChartProps {
|
|
18
|
+
/** An ID for the root element */
|
|
19
|
+
id?: string;
|
|
20
|
+
/** Classes for the root element */
|
|
21
|
+
class?: ClassValue,
|
|
22
|
+
/** Styles for the root element */
|
|
23
|
+
style?: StyleValue,
|
|
24
|
+
data: PieData[],
|
|
25
|
+
size?: number,
|
|
26
|
+
radius?: number,
|
|
27
|
+
cutout?: number,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function PieChart($props: PieChartProps) {
|
|
31
|
+
// Default values
|
|
32
|
+
$props.size ||= 200;
|
|
33
|
+
$props.radius ||= 50;
|
|
34
|
+
|
|
35
|
+
// NOTE: Take two pixels off the radius so that the stroke doesn't go outside the border
|
|
36
|
+
let $state = $watch({
|
|
37
|
+
get radius() {
|
|
38
|
+
return ($props.radius! / 100) * $props.size! - 2;
|
|
39
|
+
},
|
|
40
|
+
get cutout() {
|
|
41
|
+
return ($props.cutout! / 100) * $props.size! - 2;
|
|
42
|
+
},
|
|
43
|
+
get segments() {
|
|
44
|
+
return buildSegments($props.size!, $props.data, $props.radius!, $props.cutout!);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function buildSegments(
|
|
49
|
+
theSize: number,
|
|
50
|
+
theData: PieData[],
|
|
51
|
+
theRadiusSize: number,
|
|
52
|
+
theCutoutSize: number
|
|
53
|
+
): SegmentData[] {
|
|
54
|
+
const segments = [];
|
|
55
|
+
const x = theSize / 2;
|
|
56
|
+
const y = theSize / 2;
|
|
57
|
+
const totalValue = theData.reduce((a, b) => a + b.value, 0);
|
|
58
|
+
|
|
59
|
+
// Start at the top of the chart, at 180 + 360 so that we don't have to
|
|
60
|
+
// take wrapping angles into account
|
|
61
|
+
let startAngle = 540;
|
|
62
|
+
for (let i = 0; i < theData.length; i++) {
|
|
63
|
+
// Subtract the angle to go clockwise
|
|
64
|
+
const endAngle = startAngle - (theData[i].value / totalValue) * 360;
|
|
65
|
+
const largeArc = startAngle - endAngle > 180 ? 1 : 0;
|
|
66
|
+
segments.push({
|
|
67
|
+
name: theData[i].name,
|
|
68
|
+
value: theData[i].value,
|
|
69
|
+
color: theData[i].color,
|
|
70
|
+
path: buildPath(x, y, theRadiusSize, theCutoutSize, startAngle, endAngle, largeArc)
|
|
71
|
+
});
|
|
72
|
+
startAngle = endAngle;
|
|
73
|
+
}
|
|
74
|
+
return segments;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildPath(
|
|
78
|
+
x: number,
|
|
79
|
+
y: number,
|
|
80
|
+
theRadiusSize: number,
|
|
81
|
+
theCutoutSize: number,
|
|
82
|
+
startAngle: number,
|
|
83
|
+
endAngle: number,
|
|
84
|
+
largeArc: number
|
|
85
|
+
): string {
|
|
86
|
+
// HACK: Drawing an arc of 360 degrees doesn't work
|
|
87
|
+
if (startAngle - endAngle === 360) {
|
|
88
|
+
endAngle = 359.9999;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Use some trig to find the points on the circumference at the start and end angles
|
|
92
|
+
const startx = x + theRadiusSize * Math.sin((Math.PI * startAngle) / 180);
|
|
93
|
+
const starty = x + theRadiusSize * Math.cos((Math.PI * startAngle) / 180);
|
|
94
|
+
|
|
95
|
+
const endx = x + theRadiusSize * Math.sin((Math.PI * endAngle) / 180);
|
|
96
|
+
const endy = y + theRadiusSize * Math.cos((Math.PI * endAngle) / 180);
|
|
97
|
+
|
|
98
|
+
if ($props.cutout) {
|
|
99
|
+
// Use some trig to find the points on the cutout at the start and end angles
|
|
100
|
+
const cutoutstartx = x + theCutoutSize * Math.sin((Math.PI * startAngle) / 180);
|
|
101
|
+
const cutoutstarty = x + theCutoutSize * Math.cos((Math.PI * startAngle) / 180);
|
|
102
|
+
|
|
103
|
+
const cutoutendx = x + theCutoutSize * Math.sin((Math.PI * endAngle) / 180);
|
|
104
|
+
const cutoutendy = y + theCutoutSize * Math.cos((Math.PI * endAngle) / 180);
|
|
105
|
+
|
|
106
|
+
// Build the path - move to the cutout start, line to the start angle on the circumference,
|
|
107
|
+
// arc to the end angle on the circumference, line to the cutout end, and then arc to the
|
|
108
|
+
// cutout start
|
|
109
|
+
const path = `
|
|
110
|
+
M${cutoutstartx},${cutoutstarty}
|
|
111
|
+
L${startx},${starty}
|
|
112
|
+
A${theRadiusSize},${theRadiusSize} 0 ${largeArc} 1 ${endx},${endy}
|
|
113
|
+
L${cutoutendx},${cutoutendy}
|
|
114
|
+
A${theCutoutSize},${theCutoutSize} 0 ${largeArc} 0 ${cutoutstartx},${cutoutstarty}`.trim();
|
|
115
|
+
return path;
|
|
116
|
+
} else {
|
|
117
|
+
// Build the path - move to the origin, line to the start angle on the circumference,
|
|
118
|
+
// arc to the end angle on the circumference, and then close back to the start
|
|
119
|
+
const path = `M${x},${y} L${startx},${starty} A${theRadiusSize},${theRadiusSize} 0 ${largeArc} 1 ${endx},${endy} Z`;
|
|
120
|
+
return path;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@render {
|
|
125
|
+
<div
|
|
126
|
+
{$props?.id}
|
|
127
|
+
class={["torp-pie-chart", $props?.class]}
|
|
128
|
+
{$props?.style}
|
|
129
|
+
>
|
|
130
|
+
<svg
|
|
131
|
+
version="1.1"
|
|
132
|
+
width={$props.size}
|
|
133
|
+
height={$props.size}
|
|
134
|
+
>
|
|
135
|
+
<g>
|
|
136
|
+
@for (let [i, segment] of $state.segments.entries()) {
|
|
137
|
+
<path d={segment.path} fill={segment.color || chartColors[i % chartColors.length]}>
|
|
138
|
+
<title>{`${segment.name}\n${segment.value}`}</title>
|
|
139
|
+
</path>
|
|
140
|
+
}
|
|
141
|
+
</g>
|
|
142
|
+
</svg>
|
|
143
|
+
</div>
|
|
144
|
+
}
|
|
145
|
+
}
|