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 { WorkoutPlot } from "./components/WorkoutPlot";
|
||||
import { WorkoutStats } from "./components/WorkoutStats";
|
||||
import { parse, chunkRangeIntervals, Duration } from "zwiftout";
|
||||
import { parse } from "zwiftout";
|
||||
import { ErrorMessage } from "./components/ErrorMessage";
|
||||
import styled from "styled-components";
|
||||
import { CodeEditor } from "./components/CodeEditor";
|
||||
|
|
@ -29,9 +29,6 @@ const AppContainer = styled.div`
|
|||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
// Split range-intervals into 1 minute chunks
|
||||
const chunkSize = new Duration(60);
|
||||
|
||||
export function App() {
|
||||
const [text, setText] = useState(defaultWorkout);
|
||||
const [workout, setWorkout] = useState(parse(defaultWorkout));
|
||||
|
|
@ -54,7 +51,7 @@ export function App() {
|
|||
<AppContainer>
|
||||
<AppTitle />
|
||||
<CodeEditor onValueChange={onChange} value={text} />
|
||||
<WorkoutPlot intervals={chunkRangeIntervals(workout.intervals, chunkSize)} />
|
||||
<WorkoutPlot intervals={workout.intervals} />
|
||||
{error && <ErrorMessage>{error}</ErrorMessage>}
|
||||
<WorkoutStats workout={workout} />
|
||||
<ZwoOutput workout={workout} />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { ZoneType } from "zwiftout";
|
||||
|
||||
|
|
@ -30,3 +31,60 @@ export const Bar = styled.div<BarProps>`
|
|||
background: ${(props) => zoneColorsMap[props.zone]};
|
||||
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 styled from "styled-components";
|
||||
import { Interval, Intensity, Duration, totalDuration, maximumIntensity } from "zwiftout";
|
||||
import { BarProps, Bar } from "./Bar";
|
||||
import {
|
||||
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 => ({
|
||||
zone: interval.intensity.zone,
|
||||
|
|
@ -18,17 +26,27 @@ const Plot = styled.div`
|
|||
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 }) => {
|
||||
const workoutDuration = totalDuration(intervals);
|
||||
const maxIntensity = maximumIntensity(intervals);
|
||||
|
||||
return (
|
||||
<Plot>
|
||||
{intervals
|
||||
.map((interval) => toBarProps(interval, workoutDuration, maxIntensity))
|
||||
.map((props, i) => (
|
||||
<Bar key={i} {...props} />
|
||||
))}
|
||||
{intervals.map((interval, i) => {
|
||||
if (interval.intensity instanceof RangeIntensity) {
|
||||
return <RangeBar key={i} {...toRangeBarProps(interval, workoutDuration, maxIntensity)} />;
|
||||
} else {
|
||||
return <Bar key={i} {...toBarProps(interval, workoutDuration, maxIntensity)} />;
|
||||
}
|
||||
})}
|
||||
</Plot>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue