Tuesday, 19 January 2021

How to show boxes properly? (Algorithm, UI)

I am currently trying to generate a View with React Typescript where I can show all Appointments of a day (similar to outlook calendar). But I am facing an issue. When Appointments are overlapping how can I determine the with and position? So I guess that I probably need an algorithm accomplish this issue.

Here an example how it could be structured: Example Calendar

Every box stands for an Appointment

The classes of the Object looks like this:

class AppointmentStructured {
  appointment: Appointment
  width?: number
  left?: number
}

class Appointment {
start: Date
end: Date
subject:string
duration: number
}

I am looking for a solution where I can determine the width (max. 1 = 100%) of the specific appointment and additionally I need the position. The easiest would be to how far it is from the left (max. 1). When there are more than 2 appointments starting at the same time, the appointments are ordered by the appointment with the longest duration.

Example the third box from the image would be:

  • width: 0.333
  • left: 0.666

Out of this data I can use CSS to design them. The generated CSS then would look more or less like this:

 position: absolute;
 width: calc(100% * 0.33333);
 top: 20rem; //doesn't matter for the algorithm
 left: calc(100% * 0.66666)

--Edit This is how far i came. It starts with the method generateAppointmentTile([])

export interface AppointmentStructured {
    appointment: Appointment
    width: string | number;
    left: string | number
}

export const generateAppointmentTile = (appointments: Appointment[]): AppointmentStructured[] => {
    var appointmentsStructured = initAppointmentStructured(appointments);
    var response: AppointmentStructured[] = [];

    for (var i = 0; i < appointmentsStructured.length; ++i) {
        var width = 1;

        var previous = getPreviousOverlapping(appointmentsStructured, i);
        var previousWidth = previous
            .map((item: AppointmentStructured): number => item.width as number)
            .reduce((a: number, b: number): number => a + b, 0)

        var forward = getForwardOverlapping(appointmentsStructured, i);
        var forwardOverlays = appointmentOverlays(forward);

        var previousHasOverlayWithForward = false;
        previous.forEach((structured: AppointmentStructured) => checkAppointmentOverlaySingle(structured, forward) !== 0 ? previousHasOverlayWithForward = true :  null);


        width = (width - previousWidth) / (!previousHasOverlayWithForward && forwardOverlays !== 0 ? forwardOverlays : 1);
        appointmentsStructured[i].width = width;
        response.push(appointmentsStructured[i]);
    }

    response.forEach((value: AppointmentStructured): void => {
        value.width = `calc((100% - 8rem) * ${value.width})`;
    });

    return response
}
const appointmentOverlays = (reading: AppointmentStructured[]): number => {
    var highestNumber = 0;

    reading.forEach((structured: AppointmentStructured): void => {
        var start = checkAppointmentOverlaySingle(structured, reading) + 1;
        highestNumber = start > highestNumber ? start : highestNumber;
    });

    return highestNumber;
}

const checkAppointmentOverlaySingle = (structured: AppointmentStructured, reading: AppointmentStructured[]): number => {
    var start = 0;

    reading.forEach((item: AppointmentStructured): void => {
        if (item.appointment.id !== structured.appointment.id) {
            if ((structured.appointment.start <= item.appointment.start && structured.appointment.end >= item.appointment.start)
                || (structured.appointment.start >= item.appointment.start && structured.appointment.start <= item.appointment.end)) {
                start += 1;
            }
        }
    });

    return start;
}

const getPreviousOverlapping = (appointmentsStructured: AppointmentStructured[], index: number): AppointmentStructured[] => {
    var response: AppointmentStructured[] = [];

    for (var i = index - 1; i >= 0; --i) {
        if (appointmentsStructured[index].appointment.start >= appointmentsStructured[i].appointment.start
            && appointmentsStructured[index].appointment.start <= appointmentsStructured[i].appointment.end) {
            response.push(appointmentsStructured[i]);
        }
    }
    return response;
}

const getForwardOverlapping = (appointmentsStructured: AppointmentStructured[], index: number): AppointmentStructured[] => {
    var response: AppointmentStructured[] = [];

    for (var i = index; i < appointmentsStructured.length; ++i) {
        if (appointmentsStructured[index].appointment.start >= appointmentsStructured[i].appointment.start
            && appointmentsStructured[index].appointment.start <= appointmentsStructured[i].appointment.end) {
            response.push(appointmentsStructured[i]);
        }
    }
    return response;
}

const initAppointmentStructured = (appointments: Appointment[]): AppointmentStructured[] => {
    var appointmentsStructured: AppointmentStructured[] = appointments
        .sort((a: Appointment, b: Appointment): number => a.start.getTime() - b.start.getTime())
        .map((appointment: Appointment): AppointmentStructured => ({ appointment, width: 100, left: 0 }));
    var response: AppointmentStructured[] = [];

    // sort in a intelligent way
    for (var i = 0; i < appointmentsStructured.length; ++i) {
        var duration = appointmentsStructured[i].appointment.end.getTime() - appointmentsStructured[i].appointment.start.getTime();
        var sameStartAppointments = findAppointmentWithSameStart(appointmentsStructured[i], appointmentsStructured);
        var hasLongerAppointment: boolean = false;
        sameStartAppointments.forEach((structured: AppointmentStructured) => (structured.appointment.end.getTime() - structured.appointment.start.getTime()) > duration ? hasLongerAppointment = true : null);

        if (!hasLongerAppointment) {
            response.push(appointmentsStructured[i]);
            appointmentsStructured.splice(i, 1);
            i = -1;
        }
    }

    return response.sort((a: AppointmentStructured, b: AppointmentStructured): number => a.appointment.start.getTime() - b.appointment.start.getTime());
}

const findAppointmentWithSameStart = (structured: AppointmentStructured, all: AppointmentStructured[]): AppointmentStructured[] => {
    var response: AppointmentStructured[] = [];
    all.forEach((appointmentStructured: AppointmentStructured) => appointmentStructured.appointment.start === structured.appointment.start
        && appointmentStructured.appointment.id !== structured.appointment.id ? response.push(appointmentStructured) : null)
    return response;
}

Even some pseudo code would help me a lot.



from How to show boxes properly? (Algorithm, UI)

No comments:

Post a Comment