Render range-intervals using CSS polygons
This commit is contained in:
parent
be0d236c68
commit
73d7852309
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useCallback } from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
import { WorkoutPlot } from "./components/WorkoutPlot";
|
import { WorkoutPlot } from "./components/WorkoutPlot";
|
||||||
import { WorkoutStats } from "./components/WorkoutStats";
|
import { WorkoutStats } from "./components/WorkoutStats";
|
||||||
import { parse, chunkRangeIntervals, Duration } from "zwiftout";
|
import { parse } from "zwiftout";
|
||||||
import { ErrorMessage } from "./components/ErrorMessage";
|
import { ErrorMessage } from "./components/ErrorMessage";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { CodeEditor } from "./components/CodeEditor";
|
import { CodeEditor } from "./components/CodeEditor";
|
||||||
|
|
@ -29,9 +29,6 @@ const AppContainer = styled.div`
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Split range-intervals into 1 minute chunks
|
|
||||||
const chunkSize = new Duration(60);
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [text, setText] = useState(defaultWorkout);
|
const [text, setText] = useState(defaultWorkout);
|
||||||
const [workout, setWorkout] = useState(parse(defaultWorkout));
|
const [workout, setWorkout] = useState(parse(defaultWorkout));
|
||||||
|
|
@ -54,7 +51,7 @@ export function App() {
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
<AppTitle />
|
<AppTitle />
|
||||||
<CodeEditor onValueChange={onChange} value={text} />
|
<CodeEditor onValueChange={onChange} value={text} />
|
||||||
<WorkoutPlot intervals={chunkRangeIntervals(workout.intervals, chunkSize)} />
|
<WorkoutPlot intervals={workout.intervals} />
|
||||||
{error && <ErrorMessage>{error}</ErrorMessage>}
|
{error && <ErrorMessage>{error}</ErrorMessage>}
|
||||||
<WorkoutStats workout={workout} />
|
<WorkoutStats workout={workout} />
|
||||||
<ZwoOutput workout={workout} />
|
<ZwoOutput workout={workout} />
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ZoneType } from "zwiftout";
|
import { ZoneType } from "zwiftout";
|
||||||
|
|
||||||
|
|
@ -30,3 +31,60 @@ export const Bar = styled.div<BarProps>`
|
||||||
background: ${(props) => zoneColorsMap[props.zone]};
|
background: ${(props) => zoneColorsMap[props.zone]};
|
||||||
transition: width 0.1s, height 0.1s, background-color 0.1s;
|
transition: width 0.1s, height 0.1s, background-color 0.1s;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export type RangeBarProps = {
|
||||||
|
startZone: ZoneType;
|
||||||
|
endZone: ZoneType;
|
||||||
|
// Percentage of total workout length
|
||||||
|
durationPercentage: number;
|
||||||
|
// Percentage of maximum intensity in workout
|
||||||
|
intensityStartPercentage: number;
|
||||||
|
intensityEndPercentage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUpPolygon = (middle: number) => `polygon(0% 100%, 100% 100%, 100% 0%, 0% ${middle}%)`;
|
||||||
|
const createDownPolygon = (middle: number) => `polygon(0% 0%, 0% 100%, 100% 100%, 100% ${middle}%)`;
|
||||||
|
|
||||||
|
export const RangeContainer = styled.div<{
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
startZone: ZoneType;
|
||||||
|
endZone: ZoneType;
|
||||||
|
middle: number;
|
||||||
|
direction: "up" | "down";
|
||||||
|
}>`
|
||||||
|
display: inline-block;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin-right: 0.1%;
|
||||||
|
/* exclude 0.1% margin from bar width */
|
||||||
|
width: ${(props) => props.width - 0.1}%;
|
||||||
|
height: ${(props) => props.height}%;
|
||||||
|
clip-path: ${({ direction, middle }) => (direction === "up" ? createUpPolygon(middle) : createDownPolygon(middle))};
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
${(props) => zoneColorsMap[props.startZone]},
|
||||||
|
${(props) => zoneColorsMap[props.endZone]}
|
||||||
|
);
|
||||||
|
transition: width 0.1s, height 0.1s, background-color 0.1s;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RangeBar: React.FC<RangeBarProps> = (props) => {
|
||||||
|
const minHeightPercentage = Math.min(props.intensityStartPercentage, props.intensityEndPercentage);
|
||||||
|
const maxHeightPercentage = Math.max(props.intensityStartPercentage, props.intensityEndPercentage);
|
||||||
|
const bottomHeight = (minHeightPercentage / maxHeightPercentage) * 100;
|
||||||
|
const topHeight = 100 - bottomHeight;
|
||||||
|
const direction = props.intensityStartPercentage < props.intensityEndPercentage ? "up" : "down";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RangeContainer
|
||||||
|
height={maxHeightPercentage}
|
||||||
|
width={props.durationPercentage}
|
||||||
|
middle={topHeight}
|
||||||
|
direction={direction}
|
||||||
|
startZone={props.startZone}
|
||||||
|
endZone={props.endZone}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Interval, Intensity, Duration, totalDuration, maximumIntensity } from "zwiftout";
|
import {
|
||||||
import { BarProps, Bar } from "./Bar";
|
Interval,
|
||||||
|
Intensity,
|
||||||
|
Duration,
|
||||||
|
totalDuration,
|
||||||
|
maximumIntensity,
|
||||||
|
RangeIntensity,
|
||||||
|
intensityValueToZoneType,
|
||||||
|
} from "zwiftout";
|
||||||
|
import { BarProps, Bar, RangeBar, RangeBarProps } from "./Bar";
|
||||||
|
|
||||||
const toBarProps = (interval: Interval, workoutDuration: Duration, maxIntensity: Intensity): BarProps => ({
|
const toBarProps = (interval: Interval, workoutDuration: Duration, maxIntensity: Intensity): BarProps => ({
|
||||||
zone: interval.intensity.zone,
|
zone: interval.intensity.zone,
|
||||||
|
|
@ -18,17 +26,27 @@ const Plot = styled.div`
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const toRangeBarProps = (interval: Interval, workoutDuration: Duration, maxIntensity: Intensity): RangeBarProps => ({
|
||||||
|
startZone: intensityValueToZoneType(interval.intensity.start),
|
||||||
|
endZone: intensityValueToZoneType(interval.intensity.end),
|
||||||
|
durationPercentage: (interval.duration.seconds / workoutDuration.seconds) * 100,
|
||||||
|
intensityStartPercentage: (interval.intensity.start / maxIntensity.value) * 100,
|
||||||
|
intensityEndPercentage: (interval.intensity.end / maxIntensity.value) * 100,
|
||||||
|
});
|
||||||
|
|
||||||
export const WorkoutPlot: React.FC<{ intervals: Interval[] }> = ({ intervals }) => {
|
export const WorkoutPlot: React.FC<{ intervals: Interval[] }> = ({ intervals }) => {
|
||||||
const workoutDuration = totalDuration(intervals);
|
const workoutDuration = totalDuration(intervals);
|
||||||
const maxIntensity = maximumIntensity(intervals);
|
const maxIntensity = maximumIntensity(intervals);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Plot>
|
<Plot>
|
||||||
{intervals
|
{intervals.map((interval, i) => {
|
||||||
.map((interval) => toBarProps(interval, workoutDuration, maxIntensity))
|
if (interval.intensity instanceof RangeIntensity) {
|
||||||
.map((props, i) => (
|
return <RangeBar key={i} {...toRangeBarProps(interval, workoutDuration, maxIntensity)} />;
|
||||||
<Bar key={i} {...props} />
|
} else {
|
||||||
))}
|
return <Bar key={i} {...toBarProps(interval, workoutDuration, maxIntensity)} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
</Plot>
|
</Plot>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue