@torpor/ui 0.3.6 → 0.4.0
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/Accordion/Accordion.torp +26 -25
- package/dist/Accordion/AccordionItem.torp +3 -3
- package/dist/Accordion/AccordionTypes.ts +5 -5
- package/dist/Accordion/index.d.ts +2 -2
- package/dist/Calendar/CalendarTypes.ts +1 -1
- package/dist/Charts/BarChart.torp +159 -0
- package/dist/Charts/DataSeries.ts +5 -0
- package/dist/Charts/GridLines.torp +65 -0
- package/dist/Charts/LineChart.torp +196 -0
- package/dist/Charts/PieChart.torp +145 -0
- package/dist/Charts/ScatterChart.torp +215 -0
- package/dist/Charts/SparkLine.torp +71 -0
- package/dist/Charts/XAxis.torp +66 -0
- package/dist/Charts/YAxis.torp +47 -0
- package/dist/Charts/index.d.ts +220 -0
- package/dist/Charts/index.js +34 -0
- package/dist/Charts/index.js.map +1 -0
- package/dist/Contextual/Contextual.torp +8 -3
- package/dist/Contextual/ContextualTypes.ts +1 -1
- package/dist/Dialog/Dialog.torp +12 -16
- package/dist/Dialog/DialogButton.torp +3 -0
- package/dist/Dialog/DialogTypes.ts +2 -2
- package/dist/Dialog/index.d.ts +2 -0
- package/dist/Disclosure/Disclosure.torp +3 -3
- package/dist/Disclosure/DisclosureTypes.ts +1 -1
- package/dist/Drawer/Drawer.torp +2 -2
- package/dist/Drawer/DrawerTypes.ts +1 -1
- package/dist/Form/CheckBox.torp +1 -2
- package/dist/Form/Input.torp +1 -1
- package/dist/Form/MessageSummary.torp +38 -0
- package/dist/Form/TextArea.torp +3 -0
- package/dist/Form/index.d.ts +25 -1
- package/dist/Form/index.js +4 -0
- package/dist/ListBox/ListBox.torp +271 -0
- package/dist/ListBox/ListBoxItem.torp +107 -0
- package/dist/ListBox/ListBoxSeparator.torp +26 -0
- package/dist/ListBox/ListBoxTypes.ts +23 -0
- package/dist/ListBox/index.d.ts +97 -0
- package/dist/ListBox/index.js +14 -0
- package/dist/ListBox/index.js.map +1 -0
- package/dist/Menu/Menu.torp +19 -14
- package/dist/Menu/MenuTypes.ts +5 -4
- package/dist/MenuBar/MenuBarItem.torp +5 -8
- package/dist/MenuBar/MenuBarTypes.ts +2 -2
- package/dist/Modal/Modal.torp +7 -2
- package/dist/Modal/ModalTypes.ts +1 -1
- package/dist/NavMenu/NavMenu.torp +7 -0
- package/dist/NavMenu/NavMenuButton.torp +5 -2
- package/dist/NavMenu/NavMenuPopout.torp +1 -1
- package/dist/NavMenu/{NavMenuPopoutButton.torp → NavMenuPopoutTrigger.torp} +4 -4
- package/dist/NavMenu/NavMenuSeparator.torp +2 -1
- package/dist/NavMenu/NavMenuTypes.ts +4 -3
- package/dist/NavMenu/index.d.ts +22 -18
- package/dist/NavMenu/index.js +4 -4
- package/dist/Pagination/Pagination.torp +145 -0
- package/dist/Pagination/PaginationItem.torp +100 -0
- package/dist/Pagination/PaginationTypes.ts +25 -0
- package/dist/Pagination/buildPages.ts +57 -0
- package/dist/Pagination/index.d.ts +53 -0
- package/dist/Pagination/index.js +40 -0
- package/dist/Pagination/index.js.map +1 -0
- package/dist/Popover/Popover.torp +8 -3
- package/dist/Popover/PopoverTypes.ts +1 -1
- package/dist/SelectBox/SelectBox.torp +136 -0
- package/dist/SelectBox/SelectBoxContent.torp +81 -0
- package/dist/SelectBox/SelectBoxInput.torp +95 -0
- package/dist/SelectBox/SelectBoxTrigger.torp +41 -0
- package/dist/SelectBox/SelectBoxTypes.ts +18 -0
- package/dist/SelectBox/index.d.ts +127 -0
- package/dist/SelectBox/index.js +18 -0
- package/dist/SelectBox/index.js.map +1 -0
- package/dist/TabGroup/TabGroup.torp +3 -3
- package/dist/TabGroup/TabGroupTypes.ts +2 -2
- package/dist/ToolBar/ToolBar.torp +198 -0
- package/dist/ToolBar/ToolBarButton.torp +57 -0
- package/dist/ToolBar/ToolBarElement.torp +55 -0
- package/dist/ToolBar/ToolBarPopout.torp +97 -0
- package/dist/ToolBar/ToolBarPopoutContent.torp +92 -0
- package/dist/ToolBar/ToolBarPopoutTrigger.torp +138 -0
- package/dist/ToolBar/ToolBarSeparator.torp +25 -0
- package/dist/ToolBar/ToolBarTypes.ts +35 -0
- package/dist/ToolBar/index.d.ts +212 -0
- package/dist/ToolBar/index.js +30 -0
- package/dist/ToolBar/index.js.map +1 -0
- package/dist/metafile-esm.json +1 -1
- package/dist/utils/PopoutTypes.ts +35 -0
- package/dist/utils/chartColors.ts +17 -0
- package/dist/utils/chartUtils.ts +71 -0
- package/package.json +22 -2
|
@@ -11,11 +11,11 @@ interface AccordionProps {
|
|
|
11
11
|
/** Whether a single AccordionItem or multiple AccordionItems can be expanded at the same time */
|
|
12
12
|
type?: "single" | "multiple",
|
|
13
13
|
/** The value of the expanded AccordionItem, or an array of values if the type is "multiple" */
|
|
14
|
-
value?:
|
|
14
|
+
value?: any | any[],
|
|
15
15
|
/** If set to true, the user cannot interact with any items in the Accordion */
|
|
16
16
|
disabled?: boolean,
|
|
17
17
|
/** Raised when the expanded item(s) have changed */
|
|
18
|
-
onchange?: (value:
|
|
18
|
+
onchange?: (value: any | any[]) => void,
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -41,9 +41,15 @@ export default function Accordion($props: AccordionProps | undefined) {
|
|
|
41
41
|
let type = $props?.type ?? "single";
|
|
42
42
|
|
|
43
43
|
let $state = $watch({
|
|
44
|
-
value: $props?.value ?? (type === "single" ?
|
|
44
|
+
value: $props?.value ?? (type === "single" ? undefined : [])
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
// Update $state on $props change
|
|
48
|
+
$run(() => {
|
|
49
|
+
$state.value = $props?.value ?? (type === "single" ? undefined : [])
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Update $props on $state change
|
|
47
53
|
$run(() => {
|
|
48
54
|
if ($props !== undefined) {
|
|
49
55
|
$props.value = $state.value;
|
|
@@ -65,48 +71,43 @@ export default function Accordion($props: AccordionProps | undefined) {
|
|
|
65
71
|
$context[AccordionContextName] = context;
|
|
66
72
|
|
|
67
73
|
$run(() => {
|
|
68
|
-
//
|
|
69
|
-
let
|
|
74
|
+
// Get the value to ensure that this effect will be subscribed
|
|
75
|
+
let value = $state.value;
|
|
70
76
|
|
|
71
|
-
//
|
|
77
|
+
// And set the expanded item(s)
|
|
72
78
|
switch (type) {
|
|
73
79
|
case "single": {
|
|
74
|
-
// Ensure the value passed in was a string (in case e.g. the user has set values to ints)
|
|
75
|
-
newValue = newValue.toString();
|
|
76
80
|
for (let item of itemStates) {
|
|
77
|
-
item.expanded = item.value ===
|
|
81
|
+
item.expanded = item.value === value;
|
|
78
82
|
}
|
|
79
83
|
break;
|
|
80
84
|
}
|
|
81
85
|
case "multiple": {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
newValue = [newValue];
|
|
86
|
+
if (!Array.isArray(value)) {
|
|
87
|
+
value = [value];
|
|
85
88
|
}
|
|
86
89
|
for (let item of itemStates) {
|
|
87
|
-
item.expanded =
|
|
90
|
+
item.expanded = value.indexOf(item.value) !== -1;
|
|
88
91
|
}
|
|
89
92
|
break;
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
|
-
|
|
93
|
-
// Set the value
|
|
94
|
-
$state.value = newValue;
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
$run(() => {
|
|
98
|
-
|
|
98
|
+
let disabled = $props?.disabled;
|
|
99
|
+
itemStates.forEach((s) => s.parentDisabled = disabled === true);
|
|
99
100
|
});
|
|
100
101
|
|
|
101
102
|
// Dispatch the change event when the value changes
|
|
102
103
|
let firstChange = true;
|
|
103
104
|
$run(() => {
|
|
104
|
-
const
|
|
105
|
+
const onchange = $props?.onchange;
|
|
105
106
|
const value = $state.value;
|
|
106
107
|
if (firstChange) {
|
|
107
108
|
firstChange = false;
|
|
108
|
-
} else if (
|
|
109
|
-
|
|
109
|
+
} else if (onchange) {
|
|
110
|
+
onchange(value);
|
|
110
111
|
}
|
|
111
112
|
});
|
|
112
113
|
|
|
@@ -118,11 +119,11 @@ export default function Accordion($props: AccordionProps | undefined) {
|
|
|
118
119
|
|
|
119
120
|
// If a value wasn't set for the item, set it to the index
|
|
120
121
|
if (!itemState.value) {
|
|
121
|
-
itemState.value = itemState.index
|
|
122
|
+
itemState.value = itemState.index;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
// If we have a value, set whether the item should be expanded
|
|
125
|
-
if ($state.value) {
|
|
126
|
+
if ($state.value !== undefined && $state.value !== null) {
|
|
126
127
|
switch (type) {
|
|
127
128
|
case "single": {
|
|
128
129
|
itemState.expanded = $state.value === itemState.value;
|
|
@@ -166,7 +167,7 @@ export default function Accordion($props: AccordionProps | undefined) {
|
|
|
166
167
|
break;
|
|
167
168
|
}
|
|
168
169
|
case "multiple": {
|
|
169
|
-
let
|
|
170
|
+
let value = [];
|
|
170
171
|
for (let item of itemStates) {
|
|
171
172
|
// The item is expanded if it has the supplied value (i.e. if it was clicked) or if it
|
|
172
173
|
// was already expanded
|
|
@@ -174,10 +175,10 @@ export default function Accordion($props: AccordionProps | undefined) {
|
|
|
174
175
|
(item.value === toggleValue && !item.expanded) ||
|
|
175
176
|
(item.value !== toggleValue && item.expanded)
|
|
176
177
|
) {
|
|
177
|
-
|
|
178
|
+
value.push(item.value);
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
|
-
$state.value =
|
|
181
|
+
$state.value = value.sort();
|
|
181
182
|
break;
|
|
182
183
|
}
|
|
183
184
|
}
|
|
@@ -71,12 +71,12 @@ export default function AccordionItem($props: AccordionItemProps | undefined) {
|
|
|
71
71
|
// Dispatch the toggle event when the value of expanded changes
|
|
72
72
|
let firstToggle = true;
|
|
73
73
|
$run(() => {
|
|
74
|
-
const
|
|
74
|
+
const ontoggle = $props?.ontoggle;
|
|
75
75
|
const expanded = $state.expanded;
|
|
76
76
|
if (firstToggle) {
|
|
77
77
|
firstToggle = false;
|
|
78
|
-
} else if (
|
|
79
|
-
|
|
78
|
+
} else if (ontoggle) {
|
|
79
|
+
ontoggle(expanded);
|
|
80
80
|
}
|
|
81
81
|
});
|
|
82
82
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const AccordionContextName = "Accordion";
|
|
2
|
-
export const AccordionItemContextName = "AccordionItem";
|
|
1
|
+
export const AccordionContextName: unique symbol = Symbol.for("torp.Accordion");
|
|
2
|
+
export const AccordionItemContextName: unique symbol = Symbol.for("torp.AccordionItem");
|
|
3
3
|
|
|
4
4
|
export interface AccordionContext {
|
|
5
5
|
/** Called from an AccordionItem when it is added */
|
|
@@ -7,14 +7,14 @@ export interface AccordionContext {
|
|
|
7
7
|
/** Called from an AccordionItem when it is removed */
|
|
8
8
|
removeItem: (index: number) => void;
|
|
9
9
|
/** Called from an AccordionItem when it is toggled */
|
|
10
|
-
toggleItem: (value:
|
|
10
|
+
toggleItem: (value: any) => void;
|
|
11
11
|
/** Called from an AccordionHeader when it receives a keyboard event */
|
|
12
12
|
handleHeaderKey: (index: number, e: KeyboardEvent) => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface AccordionItemContext {
|
|
16
16
|
/** Raised when an AccordionItem is toggled */
|
|
17
|
-
toggleItem: (value:
|
|
17
|
+
toggleItem: (value: any) => void;
|
|
18
18
|
/** Raised when an AccordionHeader receives a keyboard event */
|
|
19
19
|
handleHeaderKey: (index: number, e: KeyboardEvent) => void;
|
|
20
20
|
/** The reactive state for an AccordionItem */
|
|
@@ -25,7 +25,7 @@ export interface AccordionItemContext {
|
|
|
25
25
|
|
|
26
26
|
export interface ItemState {
|
|
27
27
|
index: number;
|
|
28
|
-
value:
|
|
28
|
+
value: any;
|
|
29
29
|
expanded: boolean;
|
|
30
30
|
headerId: string;
|
|
31
31
|
contentId: string;
|
|
@@ -11,11 +11,11 @@ interface AccordionProps {
|
|
|
11
11
|
/** Whether a single AccordionItem or multiple AccordionItems can be expanded at the same time */
|
|
12
12
|
type?: "single" | "multiple",
|
|
13
13
|
/** The value of the expanded AccordionItem, or an array of values if the type is "multiple" */
|
|
14
|
-
value?:
|
|
14
|
+
value?: any | any[],
|
|
15
15
|
/** If set to true, the user cannot interact with any items in the Accordion */
|
|
16
16
|
disabled?: boolean,
|
|
17
17
|
/** Raised when the expanded item(s) have changed */
|
|
18
|
-
onchange?: (value:
|
|
18
|
+
onchange?: (value: any | any[]) => void,
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -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,65 @@
|
|
|
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
|
+
stroke="currentcolor"
|
|
36
|
+
/>
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@if ($props.showVLines) {
|
|
41
|
+
@if ($props.xStepLabels.length && $props.xStepValue) {
|
|
42
|
+
@for (let [i, label] of $props.xStepLabels.entries()) {
|
|
43
|
+
<line
|
|
44
|
+
x1={$props.chartLeft + i * $props.xStepValue * $props.itemWidth}
|
|
45
|
+
y1={$props.chartBottom}
|
|
46
|
+
x2={$props.chartLeft + i * $props.xStepValue * $props.itemWidth}
|
|
47
|
+
y2={0}
|
|
48
|
+
stroke="currentcolor"
|
|
49
|
+
/>
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
@for (let [i, label] of $props.labels.entries()) {
|
|
53
|
+
<line
|
|
54
|
+
x1={$props.chartLeft + i * $props.itemWidth + ($props.type === "bar" ? 0 : $props.itemWidth / 2)}
|
|
55
|
+
y1={$props.chartBottom}
|
|
56
|
+
x2={$props.chartLeft + i * $props.itemWidth + ($props.type === "bar" ? 0 : $props.itemWidth / 2)}
|
|
57
|
+
y2={0}
|
|
58
|
+
stroke="currentcolor"
|
|
59
|
+
/>
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
</svg>
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -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
|
+
}
|